Compare commits
5 Commits
0bc0fd8103
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e48eb8c91 | |||
| 72bf38a7ff | |||
| 97db96b105 | |||
| 5b6affd56a | |||
| d509fb7813 |
199
AGENTS.md
199
AGENTS.md
@@ -1,199 +0,0 @@
|
|||||||
# 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.
|
|
||||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -2348,12 +2348,6 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[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"
|
||||||
@@ -4133,14 +4127,6 @@ 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,21 +1,20 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
".",
|
".",
|
||||||
"api",
|
|
||||||
"typegen",
|
"typegen",
|
||||||
"ui-gpui",
|
"ui-gpui",
|
||||||
"ui-iced",
|
"ui-iced",
|
||||||
"store",
|
"crates/api",
|
||||||
"jello-types",
|
"crates/gst",
|
||||||
"gst",
|
|
||||||
"examples/hdr-gstreamer-wgpu",
|
|
||||||
"crates/iced-video",
|
"crates/iced-video",
|
||||||
|
"crates/store",
|
||||||
|
"examples/hdr-gstreamer-wgpu",
|
||||||
]
|
]
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
iced = { version = "0.14.0" }
|
iced = { version = "0.14.0" }
|
||||||
gst = { version = "0.1.0", path = "gst" }
|
gst = { version = "0.1.0", path = "crates/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" }
|
||||||
@@ -31,7 +30,7 @@ edition = "2024"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
api = { version = "0.1.0", path = "api" }
|
api = { version = "0.1.0", path = "crates/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
api/.gitignore → crates/api/.gitignore
vendored
0
api/.gitignore → crates/api/.gitignore
vendored
0
gst/.gitignore → crates/gst/.gitignore
vendored
0
gst/.gitignore → crates/gst/.gitignore
vendored
0
gst/Cargo.lock → crates/gst/Cargo.lock
generated
0
gst/Cargo.lock → crates/gst/Cargo.lock
generated
@@ -19,11 +19,3 @@ 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,12 +1,11 @@
|
|||||||
use crate::{Error, Result, ResultExt};
|
use crate::{Error, Result, ResultExt};
|
||||||
use gst::{
|
use gst::{
|
||||||
Bus, Gst, MessageType, MessageView, Sink, Source,
|
Bus, Gst, Sink,
|
||||||
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,6 +15,3 @@ 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
62
gst/.github/workflows/build.yaml
vendored
@@ -1,62 +0,0 @@
|
|||||||
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
38
gst/.github/workflows/docs.yaml
vendored
@@ -1,38 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[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"] }
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#[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::{Report, ResultExt};
|
pub use error_stack::ResultExt;
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
#[error("An error occurred")]
|
#[error("An error occurred")]
|
||||||
pub struct Error;
|
pub struct Error;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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 = { version = "0.1.0", path = "../api" }
|
api = { workspace = true }
|
||||||
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,7 +270,6 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Video(msg) => video::update(state, msg),
|
Message::Video(msg) => video::update(state, msg),
|
||||||
_ => todo!(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,51 +441,7 @@ fn card(item: &Item) -> Element<'_, Message> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn init() -> (State, Task<Message>) {
|
fn init() -> (State, Task<Message>) {
|
||||||
// Create a default config for initial state
|
(State::new(), Task::done(Message::Refresh))
|
||||||
|
|
||||||
// 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_form.update(server),
|
// SettingsMessage::Server(server) => state.settings.server_page.update(server),
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -30,40 +30,13 @@ 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)]
|
||||||
@@ -74,124 +47,9 @@ pub enum SettingsScreen {
|
|||||||
Servers,
|
Servers,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
pub struct ServerItem {
|
pub struct SettingsState {
|
||||||
pub id: uuid::Uuid,
|
pub screen: SettingsScreen,
|
||||||
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 {
|
||||||
@@ -261,6 +119,7 @@ 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)
|
||||||
@@ -270,7 +129,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(state.settings.server_form.view())
|
// .push(ServerPage::view(state))
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.pipe(container)
|
.pipe(container)
|
||||||
@@ -280,7 +139,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(state.settings.login_form.view())
|
// .push(LoginForm::view(&state.settings.login_form))
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.pipe(container)
|
.pipe(container)
|
||||||
|
|||||||
Reference in New Issue
Block a user