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

147
Cargo.lock generated
View File

@@ -726,6 +726,18 @@ dependencies = [
"core2", "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]] [[package]]
name = "blade-graphics" name = "blade-graphics"
version = "0.7.0" version = "0.7.0"
@@ -845,6 +857,28 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc" 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]] [[package]]
name = "bstr" name = "bstr"
version = "1.12.1" version = "1.12.1"
@@ -1646,6 +1680,15 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204"
[[package]]
name = "deranged"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.20" version = "0.99.20"
@@ -2270,6 +2313,12 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "futf" name = "futf"
version = "0.1.5" version = "0.1.5"
@@ -4596,6 +4645,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-derive" name = "num-derive"
version = "0.4.2" version = "0.4.2"
@@ -5462,6 +5517,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@@ -5683,6 +5744,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@@ -5859,6 +5926,16 @@ dependencies = [
"font-types", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.16" version = "0.2.16"
@@ -6357,6 +6434,16 @@ dependencies = [
"serde_derive", "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]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.228" version = "1.0.228"
@@ -6521,6 +6608,12 @@ dependencies = [
"quote", "quote",
] ]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]] [[package]]
name = "simplecss" name = "simplecss"
version = "0.2.2" version = "0.2.2"
@@ -6792,6 +6885,19 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "store"
version = "0.1.0"
dependencies = [
"bson",
"futures",
"parking_lot",
"redb",
"serde",
"tokio",
"uuid",
]
[[package]] [[package]]
name = "strict-num" name = "strict-num"
version = "0.1.1" version = "0.1.1"
@@ -7172,6 +7278,37 @@ dependencies = [
"zune-jpeg 0.4.21", "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]] [[package]]
name = "tiny-skia" name = "tiny-skia"
version = "0.11.4" version = "0.11.4"
@@ -7615,6 +7752,7 @@ dependencies = [
"iced_video_player", "iced_video_player",
"reqwest", "reqwest",
"tap", "tap",
"toml 0.9.8",
"tracing", "tracing",
"url", "url",
"uuid", "uuid",
@@ -9089,6 +9227,15 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]] [[package]]
name = "x11-dl" name = "x11-dl"
version = "2.21.0" version = "2.21.0"

View File

@@ -5,7 +5,7 @@ members = [
"typegen", "typegen",
"ui-gpui", "ui-gpui",
"ui-iced", "ui-iced",
"crates/iced_video_player", "crates/iced_video_player", "store",
] ]
[workspace.dependencies] [workspace.dependencies]
iced = { git = "https://github.com/iced-rs/iced", features = [ iced = { git = "https://github.com/iced-rs/iced", features = [
@@ -16,7 +16,6 @@ iced = { git = "https://github.com/iced-rs/iced", features = [
"tokio", "tokio",
"debug", "debug",
] } ] }
iced_wgpu = { git = "https://github.com/iced-rs/iced" }
iced_video_player = { path = "crates/iced_video_player" } iced_video_player = { path = "crates/iced_video_player" }
[package] [package]

View File

@@ -255,7 +255,6 @@ impl JellyfinClient {
"{}/Videos/{}/stream?static=true", "{}/Videos/{}/stream?static=true",
self.config.server_url.as_str(), self.config.server_url.as_str(),
item, item,
// item,
); );
Ok(url::Url::parse(&stream_url).expect("Failed to parse stream URL")) Ok(url::Url::parse(&stream_url).expect("Failed to parse stream URL"))
} }

View File

@@ -70,7 +70,10 @@
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
pkg-config pkg-config
]; ];
# LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [pkgs.wayland];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs; LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
# SYSTEM_DEPS_LINK = "static";
# PKG_CONFIG_ALL_STATIC = "1";
buildInputs = with pkgs; buildInputs = with pkgs;
[ [
@@ -84,11 +87,12 @@
glib glib
glib-networking glib-networking
libsysprof-capture # bzip2_1_1
pcre2 # libsysprof-capture
libunwind # pcre2
elfutils # libunwind
zstd # elfutils
# zstd
openssl openssl
vulkan-loader vulkan-loader
@@ -97,14 +101,18 @@
gst_all_1.gstreamermm gst_all_1.gstreamermm
gst_all_1.gst-vaapi gst_all_1.gst-vaapi
# util-linux
# libselinux
# libsepol
alsa-lib-with-plugins alsa-lib-with-plugins
libxkbcommon libxkbcommon
udev udev
wayland wayland
wayland-protocols wayland-protocols
xorg.libX11 # xorg.libX11
xorg.libXi # xorg.libXi
xorg.libXrandr # xorg.libXrandr
]) ])
++ (lib.optionals pkgs.stdenv.isDarwin [ ++ (lib.optionals pkgs.stdenv.isDarwin [
libiconv libiconv

13
store/Cargo.toml Normal file
View File

@@ -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"

217
store/src/lib.rs Normal file
View File

@@ -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<uuid::Uuid, Vec<u8>> = TableDefinition::new("users");
const SERVERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("servers");
const SETTINGS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("settings");
#[derive(Debug)]
pub struct TableInner<T> {
db: Arc<T>,
}
impl<T> Clone for TableInner<T> {
fn clone(&self) -> Self {
Self {
db: Arc::clone(&self.db),
}
}
}
impl<T> TableInner<T> {
fn new(db: Arc<T>) -> Self {
Self { db }
}
}
impl TableInner<DatabaseHandle> {
async fn get<'a, K: Key, V: Serialize + DeserializeOwned>(
&self,
table: TableDefinition<'static, K, Vec<u8>>,
key: impl Borrow<K::SelfType<'a>>,
) -> Result<Option<V>> {
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<u8>>,
key: impl Borrow<K::SelfType<'a>> + Send + 'b,
value: V,
) -> Result<Option<V>> {
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<Option<V>> {
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<K: Key, V: Serialize + DeserializeOwned> Table<K, V> for TableInner {
// async fn get(&self, key: K) -> Result<Option<Value>> {}
// async fn insert(&self, key: K, value: V) -> Result<Option<Value>> {}
// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result<bool> {}
// async fn remove(&self, key: K) -> Result<Option<Value>> {}
// }
#[derive(Debug)]
pub struct Users<T>(TableInner<T>);
impl<T> Clone for Users<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Users<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = USERS;
}
#[derive(Debug)]
pub struct Servers<T>(TableInner<T>);
impl<T> Clone for Servers<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Servers<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SERVERS;
}
#[derive(Debug)]
pub struct Settings<T>(TableInner<T>);
impl<T> Clone for Settings<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Settings<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SETTINGS;
}
#[derive(Debug, Clone)]
pub struct Database {
users: Users<DatabaseHandle>,
servers: Servers<DatabaseHandle>,
settings: Settings<DatabaseHandle>,
handle: Arc<DatabaseHandle>,
}
#[derive(Debug)]
pub struct DatabaseHandle {
database: redb::Database,
writing: AtomicBool,
wakers: RwLock<VecDeque<AtomicWaker>>,
}
#[derive(Debug)]
pub struct DatabaseWriterGuard<'a> {
handle: &'a DatabaseHandle,
dropper: Arc<AtomicBool>,
}
// 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<O, E = redb::Error> = core::result::Result<O, E>;
pub trait Table<K: Key> {
fn insert<V: Serialize + DeserializeOwned>(
&self,
key: K,
value: V,
) -> impl Future<Output = Result<Option<V>>> + Send;
fn modify<V: Serialize + DeserializeOwned, O: Serialize + DeserializeOwned>(
&self,
key: K,
v: impl FnOnce(V) -> O,
) -> impl Future<Output = Result<bool>> + Send;
fn remove<V: Serialize + DeserializeOwned>(
&self,
key: K,
) -> impl Future<Output = Result<Option<V>>> + Send;
fn get<V: Serialize + DeserializeOwned>(
&self,
key: K,
) -> impl Future<Output = Result<Option<V>>> + Send;
}
impl Database {
pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
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,
})
}
}

View File

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

View File

@@ -1,6 +1,8 @@
// mod settings;
mod shared_string; mod shared_string;
use iced_video_player::{Video, VideoPlayer}; use iced_video_player::{Video, VideoPlayer};
use shared_string::SharedString; use shared_string::SharedString;
use std::sync::Arc; use std::sync::Arc;
mod blur_hash; mod blur_hash;
@@ -157,6 +159,7 @@ pub enum Message {
PasswordChanged(String), PasswordChanged(String),
Login, Login,
LoginSuccess(String), LoginSuccess(String),
LoadedClient(api::JellyfinClient, bool),
Logout, Logout,
Video(VideoMessage), Video(VideoMessage),
} }
@@ -193,25 +196,12 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
Message::Login => { Message::Login => {
let username = state.username_input.clone(); let username = state.username_input.clone();
let password = state.password_input.clone(); let password = state.password_input.clone();
let config = (*state.jellyfin_client.config).clone();
// Update the client config with the new credentials
let mut config = (*state.jellyfin_client.config).clone();
config.username = username;
config.password = password;
Task::perform( Task::perform(
async move { async move { api::JellyfinClient::authenticate(username, password, config).await },
let mut client = api::JellyfinClient::new_with_config(config);
client.authenticate().await
},
|result| match result { |result| match result {
Ok(auth_result) => { Ok(client) => Message::LoadedClient(client, true),
if let Some(token) = auth_result.access_token {
Message::LoginSuccess(token)
} else {
Message::Error("Authentication failed: No token received".to_string())
}
}
Err(e) => Message::Error(format!("Login failed: {}", e)), Err(e) => Message::Error(format!("Login failed: {}", e)),
}, },
) )
@@ -232,6 +222,15 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|_| Message::Refresh, |_| 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 => { Message::Logout => {
state.is_authenticated = false; state.is_authenticated = false;
state.jellyfin_client.set_token(""); state.jellyfin_client.set_token("");
@@ -372,7 +371,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
.unwrap(); .unwrap();
state.video = Video::new(&url) state.video = Video::new(&url)
.inspect_err(|err| { .inspect_err(|err| {
dbg!(err); tracing::error!("{err:?}");
}) })
.ok() .ok()
.map(Arc::new); .map(Arc::new);
@@ -384,7 +383,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
fn view(state: &State) -> Element<'_, Message> { fn view(state: &State) -> Element<'_, Message> {
match state.screen { match state.screen {
Screen::Settings => settings(state), // Screen::Settings => settings::settings(state),
Screen::Home | _ => home(state), Screen::Home | _ => home(state),
} }
} }
@@ -508,123 +507,6 @@ fn footer(state: &State) -> Element<'_, Message> {
.into() .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> { fn card(item: &Item) -> Element<'_, Message> {
let name = item let name = item
.name .name
@@ -660,17 +542,47 @@ fn card(item: &Item) -> Element<'_, Message> {
.into() .into()
} }
// fn video(url: &str
fn init() -> (State, Task<Message>) { 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( Task::perform(
async move { jellyfin.authenticate_with_cached_token(".session").await }, async move {
|token| match token { // Load config from file
Ok(token) => Message::SetToken(token), let config_str = std::fs::read_to_string("config.toml")
Err(e) => Message::Error(format!("Authentication failed: {}", e)), .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)), .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)] #[derive(Debug, PartialEq, Eq, Hash)]
pub enum ArcCow<'a, T: ?Sized> { pub enum ArcCow<'a, T: ?Sized> {
Borrowed(&'a T), 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)
}
}