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:
147
Cargo.lock
generated
147
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
24
flake.nix
24
flake.nix
@@ -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
13
store/Cargo.toml
Normal 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
217
store/src/lib.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
56
ui-iced/src/settings.rs
Normal 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> {}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user