Compare commits
2 Commits
main
...
f016c56ba6
| Author | SHA1 | Date | |
|---|---|---|---|
| f016c56ba6 | |||
| 4b5dea576f |
199
AGENTS.md
Normal file
199
AGENTS.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# Agent Guidelines for Jello
|
||||||
|
|
||||||
|
This document provides guidelines for AI coding agents working on the Jello codebase.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Jello is a WIP video client for Jellyfin written in Rust, focusing on HDR video playback using:
|
||||||
|
- **iced** - Primary GUI toolkit
|
||||||
|
- **gstreamer** - Video + audio decoding library
|
||||||
|
- **wgpu** - Rendering video from GStreamer in iced
|
||||||
|
|
||||||
|
## Build, Test, and Lint Commands
|
||||||
|
|
||||||
|
### Building
|
||||||
|
```bash
|
||||||
|
# Build in release mode
|
||||||
|
cargo build --release
|
||||||
|
cargo build -r
|
||||||
|
|
||||||
|
# Build specific workspace member
|
||||||
|
cargo build -p api
|
||||||
|
cargo build -p gst
|
||||||
|
cargo build -p ui-iced
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
cargo run --release -- -vv
|
||||||
|
just jello # Uses justfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests in workspace
|
||||||
|
cargo test --workspace
|
||||||
|
|
||||||
|
# Run tests for a specific package
|
||||||
|
cargo test -p gst
|
||||||
|
cargo test -p api
|
||||||
|
cargo test -p iced-video
|
||||||
|
|
||||||
|
# Run a single test by name
|
||||||
|
cargo test test_appsink
|
||||||
|
cargo test -p gst test_appsink
|
||||||
|
|
||||||
|
# Run a specific test in a specific file
|
||||||
|
cargo test -p gst --test <test_file_name> <test_function_name>
|
||||||
|
|
||||||
|
# Run tests with output
|
||||||
|
cargo test -- --nocapture
|
||||||
|
cargo test -- --show-output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting and Formatting
|
||||||
|
```bash
|
||||||
|
# Check code without building
|
||||||
|
cargo check
|
||||||
|
cargo check --workspace
|
||||||
|
|
||||||
|
# Run clippy (linter)
|
||||||
|
cargo clippy
|
||||||
|
cargo clippy --workspace
|
||||||
|
cargo clippy --workspace -- -D warnings
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
cargo fmt
|
||||||
|
cargo fmt --all
|
||||||
|
|
||||||
|
# Check formatting without modifying files
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Tools
|
||||||
|
```bash
|
||||||
|
# Check for security vulnerabilities and license compliance
|
||||||
|
cargo deny check
|
||||||
|
|
||||||
|
# Generate Jellyfin type definitions
|
||||||
|
just typegen
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Rust Edition
|
||||||
|
- Use **Rust 2024 edition** (as specified in Cargo.toml files)
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
- Use `use` statements at the top of files
|
||||||
|
- Group imports: std library, external crates, then local modules
|
||||||
|
- Use `crate::` for absolute paths within the crate
|
||||||
|
- Common pattern: create a `priv_prelude` module for internal imports
|
||||||
|
- Use `pub use` to re-export commonly used items
|
||||||
|
- Use wildcard imports (`use crate::priv_prelude::*;`) within internal modules when a prelude exists
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```rust
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use reqwest::{Method, header::InvalidHeaderValue};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- **Types/Structs/Enums**: PascalCase (e.g., `JellyfinClient`, `Error`, `AppSink`)
|
||||||
|
- **Functions/Methods**: snake_case (e.g., `request_builder`, `stream_url`)
|
||||||
|
- **Variables**: snake_case (e.g., `access_token`, `device_id`)
|
||||||
|
- **Constants**: SCREAMING_SNAKE_CASE (e.g., `NEXT_ID`, `GST`)
|
||||||
|
- **Modules**: snake_case (e.g., `priv_prelude`, `error_stack`)
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Use **`error-stack`** for error handling with context propagation
|
||||||
|
- Use **`thiserror`** for defining error types
|
||||||
|
- Standard error type pattern:
|
||||||
|
```rust
|
||||||
|
pub use error_stack::{Report, ResultExt};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[error("An error occurred")]
|
||||||
|
pub struct Error;
|
||||||
|
|
||||||
|
pub type Result<T, E = error_stack::Report<Error>> = core::result::Result<T, E>;
|
||||||
|
```
|
||||||
|
- Attach context to errors using `.change_context(Error)` and `.attach("description")`
|
||||||
|
- Use `#[track_caller]` on functions that may panic or error for better error messages
|
||||||
|
- Error handling example:
|
||||||
|
```rust
|
||||||
|
self.inner
|
||||||
|
.set_state(gstreamer::State::Playing)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to set pipeline to Playing state")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Types
|
||||||
|
- Prefer explicit types over type inference when it improves clarity
|
||||||
|
- Use `impl Trait` for function parameters when appropriate (e.g., `impl AsRef<str>`)
|
||||||
|
- Use `Option<T>` and `Result<T, E>` idiomatically
|
||||||
|
- Use `Arc<T>` for shared ownership
|
||||||
|
- Use newtype patterns for semantic clarity (e.g., `ApiKey` wrapping `secrecy::SecretBox<String>`)
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
- Use 4 spaces for indentation
|
||||||
|
- Line length: aim for 100 characters, but not strictly enforced
|
||||||
|
- Use trailing commas in multi-line collections
|
||||||
|
- Follow standard Rust formatting conventions (enforced by `cargo fmt`)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Add doc comments (`///`) for public APIs
|
||||||
|
- Use inline comments (`//`) sparingly, prefer self-documenting code
|
||||||
|
- Include examples in doc comments when helpful
|
||||||
|
|
||||||
|
### Async/Await
|
||||||
|
- Use `tokio` as the async runtime
|
||||||
|
- Mark async functions with `async` keyword
|
||||||
|
- Use `.await` for async operations
|
||||||
|
- Common pattern: `tokio::fs` for file operations
|
||||||
|
|
||||||
|
### Module Structure
|
||||||
|
- Use `mod.rs` or inline modules as appropriate
|
||||||
|
- Keep related functionality together
|
||||||
|
- Use `pub(crate)` for internal APIs
|
||||||
|
- Re-export commonly used items at crate root
|
||||||
|
|
||||||
|
### Macros
|
||||||
|
- Custom macros used: `wrap_gst!`, `parent_child!`
|
||||||
|
- Use macros for reducing boilerplate, only in the `gst` crate
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Place tests in the same file with `#[test]` or `#[cfg(test)]`
|
||||||
|
- Use descriptive test function names (e.g., `test_appsink`, `unique_generates_different_ids`)
|
||||||
|
- Initialize tracing in tests when needed for debugging
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Prefer well-maintained crates from crates.io
|
||||||
|
- Use `workspace.dependencies` for shared dependencies across workspace members
|
||||||
|
- Pin versions when stability is important
|
||||||
|
|
||||||
|
### Workspace Structure
|
||||||
|
The project uses a Cargo workspace with multiple members:
|
||||||
|
- `.` - Main jello binary
|
||||||
|
- `api` - Jellyfin API client
|
||||||
|
- `gst` - GStreamer wrapper
|
||||||
|
- `ui-iced` - Iced UI implementation
|
||||||
|
- `ui-gpui` - GPUI UI implementation (optional)
|
||||||
|
- `store` - Secret/data/storage management
|
||||||
|
- `jello-types` - Shared type definitions
|
||||||
|
- `typegen` - Jellyfin type generator
|
||||||
|
- `crates/iced-video` - Custom iced video widget
|
||||||
|
- `examples/hdr-gstreamer-wgpu` - HDR example
|
||||||
|
|
||||||
|
### Project-Specific Patterns
|
||||||
|
- Use `LazyLock` for global initialization (e.g., GStreamer init)
|
||||||
|
- Use the builder pattern with method chaining (e.g., `request_builder()`)
|
||||||
|
- Use `tap` crate's `.pipe()` for functional transformations
|
||||||
|
- Prefer `BTreeMap`/`BTreeSet` over `HashMap`/`HashSet` when order matters
|
||||||
|
- Prefer a functional programming style instead of an imperative one.
|
||||||
|
- When building UIs keep the handler and view code in the same module (eg. settings view and settings handle in the same file)
|
||||||
|
|
||||||
|
## License
|
||||||
|
All code in this project is MIT licensed.
|
||||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -4127,6 +4127,14 @@ dependencies = [
|
|||||||
"ui-iced",
|
"ui-iced",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jello-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff"
|
name = "jiff"
|
||||||
version = "0.2.18"
|
version = "0.2.18"
|
||||||
|
|||||||
15
Cargo.toml
15
Cargo.toml
@@ -1,20 +1,21 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
".",
|
".",
|
||||||
|
"api",
|
||||||
"typegen",
|
"typegen",
|
||||||
"ui-gpui",
|
"ui-gpui",
|
||||||
"ui-iced",
|
"ui-iced",
|
||||||
"crates/api",
|
"store",
|
||||||
"crates/gst",
|
"jello-types",
|
||||||
"crates/iced-video",
|
"gst",
|
||||||
"crates/store",
|
|
||||||
"examples/hdr-gstreamer-wgpu",
|
"examples/hdr-gstreamer-wgpu",
|
||||||
|
"crates/iced-video",
|
||||||
]
|
]
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
iced = { version = "0.14.0" }
|
iced = { version = "0.14.0" }
|
||||||
gst = { version = "0.1.0", path = "crates/gst" }
|
gst = { version = "0.1.0", path = "gst" }
|
||||||
|
iced_wgpu = { version = "0.14.0" }
|
||||||
iced-video = { version = "0.1.0", path = "crates/iced-video" }
|
iced-video = { version = "0.1.0", path = "crates/iced-video" }
|
||||||
api = { version = "0.1.0", path = "crates/api" }
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
iced_wgpu = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
iced_wgpu = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
||||||
@@ -30,7 +31,7 @@ edition = "2024"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
api = { version = "0.1.0", path = "crates/api" }
|
api = { version = "0.1.0", path = "api" }
|
||||||
bytemuck = { version = "1.24.0", features = ["derive"] }
|
bytemuck = { version = "1.24.0", features = ["derive"] }
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] }
|
clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] }
|
||||||
|
|||||||
0
crates/api/.gitignore → api/.gitignore
vendored
0
crates/api/.gitignore → api/.gitignore
vendored
@@ -19,3 +19,11 @@ wgpu = { version = "27.0.1", features = ["vulkan"] }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
debug = true
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
# [patch.crates-io]
|
||||||
|
# iced_wgpu = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use crate::{Error, Result, ResultExt};
|
use crate::{Error, Result, ResultExt};
|
||||||
use gst::{
|
use gst::{
|
||||||
Bus, Gst, Sink,
|
Bus, Gst, MessageType, MessageView, Sink, Source,
|
||||||
app::AppSink,
|
app::AppSink,
|
||||||
caps::{Caps, CapsType},
|
caps::{Caps, CapsType},
|
||||||
element::ElementExt,
|
element::ElementExt,
|
||||||
pipeline::PipelineExt,
|
pipeline::PipelineExt,
|
||||||
playback::{PlayFlags, Playbin3},
|
playback::{PlayFlags, Playbin3},
|
||||||
|
videoconvertscale::VideoConvert,
|
||||||
};
|
};
|
||||||
use std::sync::{Arc, Mutex, atomic::AtomicBool};
|
use std::sync::{Arc, Mutex, atomic::AtomicBool};
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,6 @@ anyhow = "*"
|
|||||||
pollster = "0.4.0"
|
pollster = "0.4.0"
|
||||||
tracing = { version = "0.1.43", features = ["log"] }
|
tracing = { version = "0.1.43", features = ["log"] }
|
||||||
tracing-subscriber = "0.3.22"
|
tracing-subscriber = "0.3.22"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
|||||||
62
gst/.github/workflows/build.yaml
vendored
Normal file
62
gst/.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checks-matrix:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- id: set-matrix
|
||||||
|
name: Generate Nix Matrix
|
||||||
|
run: |
|
||||||
|
set -Eeu
|
||||||
|
matrix="$(nix eval --json '.#githubActions.matrix')"
|
||||||
|
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
checks-build:
|
||||||
|
needs: checks-matrix
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix: ${{fromJSON(needs.checks-matrix.outputs.matrix)}}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- run: nix build -L '.#${{ matrix.attr }}'
|
||||||
|
|
||||||
|
codecov:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: "write"
|
||||||
|
contents: "read"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
|
- name: Run codecov
|
||||||
|
run: nix build .#checks.x86_64-linux.hello-llvm-cov
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
uses: codecov/codecov-action@v4.0.1
|
||||||
|
with:
|
||||||
|
flags: unittests
|
||||||
|
name: codecov-hello
|
||||||
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: ./result
|
||||||
|
verbose: true
|
||||||
|
|
||||||
38
gst/.github/workflows/docs.yaml
vendored
Normal file
38
gst/.github/workflows/docs.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: "write"
|
||||||
|
contents: "read"
|
||||||
|
pages: "write"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: DeterminateSystems/flake-checker-action@main
|
||||||
|
|
||||||
|
- name: Generate docs
|
||||||
|
run: nix build .#checks.x86_64-linux.hello-docs
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: result/share/doc
|
||||||
|
|
||||||
|
- name: Deploy to gh-pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
|
|
||||||
0
crates/gst/.gitignore → gst/.gitignore
vendored
0
crates/gst/.gitignore → gst/.gitignore
vendored
0
crates/gst/Cargo.lock → gst/Cargo.lock
generated
0
crates/gst/Cargo.lock → gst/Cargo.lock
generated
8
jello-types/Cargo.toml
Normal file
8
jello-types/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "jello-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
uuid = { version = "1.18.1", features = ["serde"] }
|
||||||
6
jello-types/src/lib.rs
Normal file
6
jello-types/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
id: uuid::Uuid,
|
||||||
|
name: Option<String>,
|
||||||
|
primary_image_tag: Option<String>,
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
pub use error_stack::ResultExt;
|
pub use error_stack::{Report, ResultExt};
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
#[error("An error occurred")]
|
#[error("An error occurred")]
|
||||||
pub struct Error;
|
pub struct Error;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod errors;
|
mod errors;
|
||||||
|
use api::JellyfinConfig;
|
||||||
use errors::*;
|
use errors::*;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2024"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
api = { workspace = true }
|
api = { version = "0.1.0", path = "../api" }
|
||||||
blurhash = "0.2.3"
|
blurhash = "0.2.3"
|
||||||
bytes = "1.11.0"
|
bytes = "1.11.0"
|
||||||
gpui_util = "0.2.2"
|
gpui_util = "0.2.2"
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Video(msg) => video::update(state, msg),
|
Message::Video(msg) => video::update(state, msg),
|
||||||
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +442,51 @@ fn card(item: &Item) -> Element<'_, Message> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn init() -> (State, Task<Message>) {
|
fn init() -> (State, Task<Message>) {
|
||||||
(State::new(), Task::done(Message::Refresh))
|
// 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(),
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
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)),
|
||||||
|
_ => Message::Error("Login Unimplemented".to_string()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.chain(Task::done(Message::Refresh)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui() -> iced::Result {
|
pub fn ui() -> iced::Result {
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ pub fn update(state: &mut State, message: SettingsMessage) -> Task<Message> {
|
|||||||
SettingsMessage::Select(screen) => {
|
SettingsMessage::Select(screen) => {
|
||||||
tracing::trace!("Switching settings screen to {:?}", screen);
|
tracing::trace!("Switching settings screen to {:?}", screen);
|
||||||
state.settings.screen = screen;
|
state.settings.screen = screen;
|
||||||
} //
|
}
|
||||||
// SettingsMessage::User(user) => state.settings.login_form.update(user),
|
SettingsMessage::User(user) => state.settings.login_form.update(user),
|
||||||
//
|
|
||||||
// SettingsMessage::Server(server) => state.settings.server_page.update(server),
|
SettingsMessage::Server(server) => state.settings.server_form.update(server),
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -30,13 +30,40 @@ pub fn empty() -> Element<'static, Message> {
|
|||||||
column([]).into()
|
column([]).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct SettingsState {
|
||||||
|
login_form: LoginForm,
|
||||||
|
server_form: ServerForm,
|
||||||
|
screen: SettingsScreen,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum SettingsMessage {
|
pub enum SettingsMessage {
|
||||||
Open,
|
Open,
|
||||||
Close,
|
Close,
|
||||||
Select(SettingsScreen),
|
Select(SettingsScreen),
|
||||||
// User(UserMessage),
|
User(UserMessage),
|
||||||
// Server(ServerMessage),
|
Server(ServerMessage),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum UserMessage {
|
||||||
|
Add,
|
||||||
|
UsernameChanged(String),
|
||||||
|
PasswordChanged(String),
|
||||||
|
// Edit(uuid::Uuid),
|
||||||
|
// Delete(uuid::Uuid),
|
||||||
|
Clear,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ServerMessage {
|
||||||
|
Add,
|
||||||
|
NameChanged(String),
|
||||||
|
UrlChanged(String),
|
||||||
|
// Edit(uuid::Uuid),
|
||||||
|
// Delete(uuid::Uuid),
|
||||||
|
Clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
@@ -47,9 +74,124 @@ pub enum SettingsScreen {
|
|||||||
Servers,
|
Servers,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SettingsState {
|
pub struct ServerItem {
|
||||||
pub screen: SettingsScreen,
|
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, Default)]
|
||||||
|
pub struct LoginForm {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginForm {
|
||||||
|
pub fn update(&mut self, message: UserMessage) {
|
||||||
|
match message {
|
||||||
|
UserMessage::UsernameChanged(data) => {
|
||||||
|
self.username = data;
|
||||||
|
}
|
||||||
|
UserMessage::PasswordChanged(data) => {
|
||||||
|
self.password = data;
|
||||||
|
}
|
||||||
|
UserMessage::Add => {
|
||||||
|
// Handle adding user
|
||||||
|
}
|
||||||
|
UserMessage::Clear => {
|
||||||
|
self.username.clear();
|
||||||
|
self.password.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn view(&self) -> Element<'_, Message> {
|
||||||
|
iced::widget::column![
|
||||||
|
text("Login Form"),
|
||||||
|
text_input("Enter Username", &self.username).on_input(|data| {
|
||||||
|
Message::Settings(SettingsMessage::User(UserMessage::UsernameChanged(data)))
|
||||||
|
}),
|
||||||
|
text_input("Enter Password", &self.password)
|
||||||
|
.secure(true)
|
||||||
|
.on_input(|data| {
|
||||||
|
Message::Settings(SettingsMessage::User(UserMessage::PasswordChanged(data)))
|
||||||
|
}),
|
||||||
|
row![
|
||||||
|
button(text("Add User")).on_press_maybe(self.validate()),
|
||||||
|
button(text("Cancel"))
|
||||||
|
.on_press(Message::Settings(SettingsMessage::User(UserMessage::Clear))),
|
||||||
|
]
|
||||||
|
.spacing(10),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.padding([10, 0])
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Option<Message> {
|
||||||
|
(!self.username.is_empty() && !self.password.is_empty())
|
||||||
|
.then(|| Message::Settings(SettingsMessage::User(UserMessage::Add)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ServerForm {
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerForm {
|
||||||
|
pub fn update(&mut self, message: ServerMessage) {
|
||||||
|
match message {
|
||||||
|
ServerMessage::NameChanged(data) => {
|
||||||
|
self.name = data;
|
||||||
|
}
|
||||||
|
ServerMessage::UrlChanged(data) => {
|
||||||
|
self.url = data;
|
||||||
|
}
|
||||||
|
ServerMessage::Add => {
|
||||||
|
// Handle adding server
|
||||||
|
}
|
||||||
|
ServerMessage::Clear => {
|
||||||
|
self.name.clear();
|
||||||
|
self.url.clear();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn view(&self) -> Element<'_, Message> {
|
||||||
|
iced::widget::column![
|
||||||
|
text("Add New Server"),
|
||||||
|
text_input("Enter server name", &self.name).on_input(|data| {
|
||||||
|
Message::Settings(SettingsMessage::Server(ServerMessage::NameChanged(data)))
|
||||||
|
}),
|
||||||
|
text_input("Enter server URL", &self.url).on_input(|data| {
|
||||||
|
Message::Settings(SettingsMessage::Server(ServerMessage::UrlChanged(data)))
|
||||||
|
}),
|
||||||
|
row![
|
||||||
|
button(text("Add Server")).on_press_maybe(self.validate()),
|
||||||
|
button(text("Cancel")).on_press(Message::Settings(SettingsMessage::Server(
|
||||||
|
ServerMessage::Clear
|
||||||
|
))),
|
||||||
|
]
|
||||||
|
.spacing(10),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.padding([10, 0])
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Option<Message> {
|
||||||
|
(!self.name.is_empty() && !self.url.is_empty())
|
||||||
|
.then(|| Message::Settings(SettingsMessage::Server(ServerMessage::Add)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod screens {
|
mod screens {
|
||||||
@@ -119,7 +261,6 @@ mod screens {
|
|||||||
Column::new()
|
Column::new()
|
||||||
.push(text("Main Settings"))
|
.push(text("Main Settings"))
|
||||||
.push(toggler(true).label("HDR"))
|
.push(toggler(true).label("HDR"))
|
||||||
.push(toggler(true).label("Enable Notifications"))
|
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.pipe(container)
|
.pipe(container)
|
||||||
@@ -129,7 +270,7 @@ mod screens {
|
|||||||
pub fn server(state: &State) -> Element<'_, Message> {
|
pub fn server(state: &State) -> Element<'_, Message> {
|
||||||
Column::new()
|
Column::new()
|
||||||
.push(text("Server Settings"))
|
.push(text("Server Settings"))
|
||||||
// .push(ServerPage::view(state))
|
.push(state.settings.server_form.view())
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.pipe(container)
|
.pipe(container)
|
||||||
@@ -139,7 +280,7 @@ mod screens {
|
|||||||
pub fn user(state: &State) -> Element<'_, Message> {
|
pub fn user(state: &State) -> Element<'_, Message> {
|
||||||
Column::new()
|
Column::new()
|
||||||
.push(text("User Settings"))
|
.push(text("User Settings"))
|
||||||
// .push(LoginForm::view(&state.settings.login_form))
|
.push(state.settings.login_form.view())
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.pipe(container)
|
.pipe(container)
|
||||||
|
|||||||
Reference in New Issue
Block a user