From cb5dd14ac54c34bc7c95688872f06b6696b7abdd Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Fri, 24 Oct 2025 11:08:38 -0400 Subject: [PATCH 1/6] chore: add calibre as a git submodule --- .gitmodules | 3 +++ third_party/calibre | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 third_party/calibre diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d5a0673 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/calibre"] + path = third_party/calibre + url = https://github.com/kovidgoyal/calibre.git diff --git a/third_party/calibre b/third_party/calibre new file mode 160000 index 0000000..93399c3 --- /dev/null +++ b/third_party/calibre @@ -0,0 +1 @@ +Subproject commit 93399c33cdbaa885174e6a74364a3f5ce667506b From 425e972ea95659d3a09384411727a93e832f3eba Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:11:14 -0400 Subject: [PATCH 2/6] feat: add library management with Calibre support Implement a trait-based library abstraction layer that enables reading book metadata from Calibre databases. Add unified error handling and Tauri commands for library operations. - Create BookLibrary trait for extensible library backend support - Implement CalibreLibrary with thread-safe database access - Add AppError enum for consistent error handling across modules - Implement LibraryState for managing open library instances - Add Tauri commands: library_open, library_get_all_books, library_get_book, library_get_book_count - Add comprehensive tests for library operations and error cases - Organize imports alphabetically across modules (style) Change-Id: 4050ec25b8f88b0eb211f6691a725e6d Change-Id-Short: vzuzlnxuorkr --- .../instructions/calibre_db.instructions.md | 90 ++ .github/instructions/error.instructions.md | 87 ++ .../library.calibre.instructions.md | 81 ++ .github/instructions/library.instructions.md | 183 ++++ .github/instructions/ts.tests.instructions.md | 84 +- .../typescript.typing.instructions.md | 183 ++++ devenv.nix | 6 +- package.json | 1 + pnpm-lock.yaml | 10 + src-tauri/Cargo.lock | 105 +- src-tauri/Cargo.toml | 9 +- src-tauri/calibre_db/.gitignore | 1 + src-tauri/calibre_db/Cargo.lock | 893 ++++++++++++++++++ src-tauri/calibre_db/Cargo.toml | 29 + src-tauri/calibre_db/README.md | 238 +++++ src-tauri/calibre_db/src/error.rs | 45 + src-tauri/calibre_db/src/lib.rs | 48 + src-tauri/calibre_db/src/models.rs | 324 +++++++ src-tauri/calibre_db/src/queries.rs | 457 +++++++++ src-tauri/calibre_db/src/schema.rs | 122 +++ .../calibre_db/tests/integration_tests.rs | 518 ++++++++++ src-tauri/src/archive/commands.rs | 4 +- src-tauri/src/archive/mod.rs | 2 +- src-tauri/src/archive/reader.rs | 2 +- src-tauri/src/archive/watcher.rs | 4 +- src-tauri/src/archive/writer.rs | 4 +- src-tauri/src/bin/create_test_cbz.rs | 2 +- .../src/bin/create_test_missing_comicinfo.rs | 2 +- src-tauri/src/comicinfo/info.rs | 5 +- src-tauri/src/comicinfo/mod.rs | 14 +- src-tauri/src/comicinfo/page.rs | 4 +- src-tauri/src/error.rs | 24 + src-tauri/src/lib.rs | 9 + src-tauri/src/library/base.rs | 15 + src-tauri/src/library/calibre.rs | 74 ++ src-tauri/src/library/commands.rs | 119 +++ src-tauri/src/library/mod.rs | 6 + src-tauri/tests/lib_tests.rs | 30 + src/__tests__/components/BooksTable.test.tsx | 287 ++++++ .../components/OpenLibraryDialog.test.tsx | 295 ++++++ src/__tests__/hooks/useLibrary.test.ts | 395 ++++++++ src/__tests__/hooks/useTableColumns.test.ts | 178 ++++ src/api/library.ts | 18 + src/components/BookCoverSkeleton.tsx | 31 + src/components/BooksTable.tsx | 533 +++++++++++ src/components/OpenLibraryDialog.tsx | 161 ++++ src/components/README.md | 335 +++++++ .../__tests__/BooksTable.menu.test.tsx | 115 +++ src/hooks/useLibrary.ts | 116 +++ src/hooks/useTableColumns.ts | 99 ++ src/pages/index.tsx | 149 ++- src/styles/globals.css | 36 + src/types/book.ts | 45 + 53 files changed, 6578 insertions(+), 49 deletions(-) create mode 100644 .github/instructions/calibre_db.instructions.md create mode 100644 .github/instructions/error.instructions.md create mode 100644 .github/instructions/library.calibre.instructions.md create mode 100644 .github/instructions/library.instructions.md create mode 100644 .github/instructions/typescript.typing.instructions.md create mode 100644 src-tauri/calibre_db/.gitignore create mode 100644 src-tauri/calibre_db/Cargo.lock create mode 100644 src-tauri/calibre_db/Cargo.toml create mode 100644 src-tauri/calibre_db/README.md create mode 100644 src-tauri/calibre_db/src/error.rs create mode 100644 src-tauri/calibre_db/src/lib.rs create mode 100644 src-tauri/calibre_db/src/models.rs create mode 100644 src-tauri/calibre_db/src/queries.rs create mode 100644 src-tauri/calibre_db/src/schema.rs create mode 100644 src-tauri/calibre_db/tests/integration_tests.rs create mode 100644 src-tauri/src/error.rs create mode 100644 src-tauri/src/library/base.rs create mode 100644 src-tauri/src/library/calibre.rs create mode 100644 src-tauri/src/library/commands.rs create mode 100644 src-tauri/src/library/mod.rs create mode 100644 src-tauri/tests/lib_tests.rs create mode 100644 src/__tests__/components/BooksTable.test.tsx create mode 100644 src/__tests__/components/OpenLibraryDialog.test.tsx create mode 100644 src/__tests__/hooks/useLibrary.test.ts create mode 100644 src/__tests__/hooks/useTableColumns.test.ts create mode 100644 src/api/library.ts create mode 100644 src/components/BookCoverSkeleton.tsx create mode 100644 src/components/BooksTable.tsx create mode 100644 src/components/OpenLibraryDialog.tsx create mode 100644 src/components/README.md create mode 100644 src/components/__tests__/BooksTable.menu.test.tsx create mode 100644 src/hooks/useLibrary.ts create mode 100644 src/hooks/useTableColumns.ts create mode 100644 src/types/book.ts diff --git a/.github/instructions/calibre_db.instructions.md b/.github/instructions/calibre_db.instructions.md new file mode 100644 index 0000000..57ed62e --- /dev/null +++ b/.github/instructions/calibre_db.instructions.md @@ -0,0 +1,90 @@ +--- +applyTo: "src-tauri/calibre_db/**/*.rs" +--- + +# calibre_db Crate - Calibre SQLite Database Bindings + +This is a standalone, publishable Rust crate providing type-safe access to Calibre SQLite databases based on Calibre's database schema (from `third_party/calibre/src/calibre/db`). + +## Purpose +Read and deserialize book metadata from Calibre's SQLite `metadata.db` format, supporting 10 core tables (books, authors, publishers, tags, series, data, comments, ratings, identifiers, languages) and their many-to-many relationships. + +## Key Modules +- `lib.rs`: Public API (`CalibreDatabase::open()`, `get_book()`, `all_books()`) +- `models.rs`: Book, Author, Series, Tag, Identifier data structures with builder patterns +- `schema.rs`: Database connection management and schema validation +- `queries.rs`: Query functions for books and related metadata +- `error.rs`: Error types (DatabaseError, NotFound, InvalidData, IoError, SerializationError) + +## Development Guidelines +- Query functions return `Result` or `Result>` for missing relations +- Use builder pattern (`with_authors()`, `with_tags()`, etc.) for complex object construction +- Handle missing relations gracefully (some books lack series, publishers, ratings) +- Always verify book IDs before querying relations in loops +- Parse timestamps using RFC 3339, fallback to Utc::now() +- Fetch complete book data in single operation for performance + +## Database Schema Notes + +### Languages Table +The `books_languages_link` table uses a foreign key relationship to the `languages` table: + +```sql +CREATE TABLE languages ( + id INTEGER PRIMARY KEY, + lang_code TEXT NOT NULL +); + +CREATE TABLE books_languages_link ( + id INTEGER PRIMARY KEY, + book INTEGER NOT NULL, + lang_code INTEGER NOT NULL, + item_order INTEGER NOT NULL DEFAULT 0, + UNIQUE(book, lang_code) +); +``` + +Language codes are stored as references (foreign keys) to the `languages` table, not as direct text columns. When querying languages: +- Join `books_languages_link` with `languages` to fetch language codes +- Use `ORDER BY item_order ASC` to maintain language order +- Handle cases where books have no associated languages + +## Testing +- Unit tests in each module (models, error, schema, queries) +- Integration tests in `tests/integration_tests.rs` with real SQLite databases +- All tests must create temporary databases; no file I/O to real libraries +- Verify both presence and absence of optional relations + +## Build & Test +``` +cargo check # Verify compilation +cargo fmt # Format code +cargo clippy # Lint +cargo test # Run all tests +cargo test --lib # Unit tests only +cargo test --test integration_tests # Integration tests +``` + +## Troubleshooting + +### Schema Verification +If you encounter unexpected column errors or schema-related issues, ask the user for their Calibre database path, then verify the actual database schema with: + +```bash +sqlite3 ".schema TABLE_NAME" +``` + +Replace `` with the user's actual Calibre `metadata.db` file path and `TABLE_NAME` with the table being investigated (e.g., `books_languages_link`, `books`, `authors`). + +### Common Issues + +**"no such column" errors**: The database schema may differ from expectations. Request the user's database path and use the schema verification command above to inspect the actual column names and structure. + +**Language code retrieval failures**: Calibre uses a foreign key relationship between `books_languages_link` and `languages` tables. Ask the user for their database path and verify both table schemas: + +```bash +sqlite3 ".schema books_languages_link" +sqlite3 ".schema languages" +``` + +Ensure queries properly join these tables and handle cases where books have no associated languages. diff --git a/.github/instructions/error.instructions.md b/.github/instructions/error.instructions.md new file mode 100644 index 0000000..9fd8260 --- /dev/null +++ b/.github/instructions/error.instructions.md @@ -0,0 +1,87 @@ +--- +applyTo: "src-tauri/**/*.rs" +--- + +# Error Handling in Rust + +Consistent error handling across all Rust modules using enums for type safety and clarity. + +## Purpose +Provide a unified error handling strategy that: +- Prioritizes enum error types for domain-specific errors +- Ensures Tauri commands return the top-level `AppError` type for IPC communication +- Uses specific error enums for internal library functions +- Maintains clear error boundaries between modules + +## Error Hierarchy + +### Tauri Commands Layer +Tauri commands should always return `Result` to ensure proper serialization for IPC: + +```rust +#[tauri::command] +async fn fetch_book(lib: State>, id: u32) -> Result { + lib.get_book(id).await +} +``` + +### Internal Library Layer +Internal functions should use specific error enums for their domain: + +```rust +pub enum LibraryError { + BookNotFound(u32), + InvalidPath(String), + DatabaseFailure(String), +} + +pub fn get_book_internal(id: u32) -> Result { + // Implementation +} +``` + +### AppError - Top-Level Error Type +The unified error enum for converting all domain-specific errors to IPC-compatible format. + +**Variants:** +- `LibraryError(String)`: General library operation failures (path validation, initialization) +- `BookNotFound(String)`: Specific book not found by ID or query +- `DatabaseError(String)`: Underlying SQLite or database failures +- `IoError(String)`: File system errors +- `ValidationError(String)`: Invalid input or data validation issues +- `ArchiveError(String)`: Archive/compression operation failures + +## Error Conversion +Implement `From` traits to convert domain-specific errors to `AppError`: + +```rust +impl From for AppError { + fn from(err: LibraryError) -> Self { + match err { + LibraryError::BookNotFound(id) => AppError::BookNotFound(format!("Book {} not found", id)), + LibraryError::InvalidPath(p) => AppError::ValidationError(format!("Invalid path: {}", p)), + LibraryError::DatabaseFailure(e) => AppError::DatabaseError(e), + } + } +} + +impl From for AppError { + fn from(err: std::io::Error) -> Self { + AppError::IoError(err.to_string()) + } +} + +impl From for AppError { + fn from(err: serde_json::Error) -> Self { + AppError::ValidationError(err.to_string()) + } +} +``` + +## Guidelines +- **Prioritize enums**: Use specific error enums for internal operations (e.g., `LibraryError`, `ArchiveError`) +- **Tauri boundary**: Always return `AppError` from Tauri commands +- **Error conversion**: Implement `From` traits to bridge domain-specific errors to `AppError` +- **Descriptive messages**: Include context in error messages for debugging +- **No panics**: Return errors instead of panicking in library code +- **Result types**: Use `Result` consistently throughout the codebase \ No newline at end of file diff --git a/.github/instructions/library.calibre.instructions.md b/.github/instructions/library.calibre.instructions.md new file mode 100644 index 0000000..866f001 --- /dev/null +++ b/.github/instructions/library.calibre.instructions.md @@ -0,0 +1,81 @@ +--- +applyTo: "src-tauri/src/library/calibre.rs" +--- + +# Why `unsafe impl Send + Sync` is Safe and Necessary + +## The Problem + +`CalibreLibrary` wraps `Arc`, which internally contains `rusqlite::Connection`. While `rusqlite::Connection` is thread-safe, Rust's type system cannot automatically verify this because it uses `RefCell` internally for statement caching. + +The compiler requires `Send + Sync` for any type used across async task boundaries, but cannot derive these traits automatically from `Connection`. + +## The Solution: `unsafe impl` + +```rust +// SAFETY: CalibreDatabase wraps rusqlite::Connection, which is thread-safe. +// rusqlite uses SQLite's built-in locking mechanisms to ensure safe concurrent access. +// All database operations in this crate are read-only, preventing data races. +// The Arc pattern safely shares the connection across async tasks. +unsafe impl Send for CalibreLibrary {} +unsafe impl Sync for CalibreLibrary {} +``` + +## Why This Is Safe + +### 1. rusqlite::Connection is Thread-Safe +- rusqlite wraps SQLite's C library, which provides thread-safe database access +- SQLite uses internal mutexes for serialization +- The Rust wrapper correctly exposes these guarantees +- See: https://www.sqlite.org/threadsafe.html + +### 2. All Operations Are Read-Only +- The calibre_db crate only queries the database +- No mutations occur +- No shared mutable state across threads +- Data races are impossible with read-only access + +### 3. Arc Provides Safe Sharing +- `Arc` is always `Send + Sync` if `T` is `Send + Sync` +- Atomic reference counting handles concurrent access safely +- The Arc itself is allocated on the heap with stable address +- No use-after-free or double-free can occur + +### 4. Compiler Verification Still Applies +While marked `unsafe impl`, the actual usage is verified as safe: +- Closures capturing `self` cannot violate Send/Sync requirements +- The async runtime cannot move tasks between threads unsafely +- Tauri's event loop enforces single-threaded execution where needed + +## Why Not Other Approaches? + +### Approach: tokio::task::spawn_blocking +- Would require copying all data into blocking tasks +- Unnecessary performance overhead for read-only operations +- Adds complexity without benefit + +### Approach: Don't make it async +- Violates the trait definition which requires async +- Cannot integrate properly with Tauri's async runtime +- Blocks on database I/O instead of yielding + +### Approach: RwLock or Mutex wrapper +- rusqlite::Connection already handles internal locking +- Adding another lock layer creates unnecessary contention +- Defeats the purpose of SQLite's built-in concurrency + +## When This Pattern Is Appropriate + +Use `unsafe impl Send + Sync` when: +1. Wrapping a known thread-safe C/FFI library (like rusqlite) +2. All operations are read-only or the library handles synchronization +3. The safety invariants are clearly documented +4. The usage is verified as safe through design + +This is a common and accepted pattern in Rust for FFI bindings. + +## References + +- Rust nomicon on FFI and thread safety: https://doc.rust-lang.org/nomicon/ffi.html +- rusqlite documentation: https://docs.rs/rusqlite/ +- Tokio task blocking: https://tokio.rs/tokio/tutorial/select#cancellation \ No newline at end of file diff --git a/.github/instructions/library.instructions.md b/.github/instructions/library.instructions.md new file mode 100644 index 0000000..6a789de --- /dev/null +++ b/.github/instructions/library.instructions.md @@ -0,0 +1,183 @@ +--- +applyTo: "src-tauri/src/library/**/*.rs" +--- + +# Library Module - Implementation-Agnostic Library Access + +The library module provides a unified interface for accessing book library metadata from different sources (Calibre, Kobo, Amazon, etc.) through URI-based routing and a trait-based abstraction. + +## Architecture + +### Design Principle + +The module is designed to be **implementation-agnostic**. The frontend communicates library requests through URIs with schemes that indicate the implementation type (e.g., `calibre:///path/to/library`). The backend parses these URIs and manages the appropriate library implementation transparently. + +### Key Components + +- `base.rs`: `BookLibrary` trait defining the interface all implementations must provide +- `calibre.rs`: Calibre-specific implementation of `BookLibrary` +- `commands.rs`: Tauri command handlers that parse URIs and delegate to implementations +- `mod.rs`: Module exports +- `LibraryState`: Manages the currently open library (trait object) in application state + +### URI Scheme Format + +Libraries are identified by URIs with implementation-specific schemes: + +``` +calibre:///absolute/path/to/library +calibre://relative/path/to/library +``` + +The scheme determines which implementation to use; the path portion is passed to that implementation. + +## BookLibrary Trait + +The `BookLibrary` trait defines the interface all library implementations must provide: + +```rust +#[async_trait] +pub trait BookLibrary: Send + Sync { + async fn get_all_books(&self) -> Result, AppError>; + async fn get_book(&self, book_id: u32) -> Result; + async fn get_book_count(&self) -> Result; + fn clone_box(&self) -> Box; +} +``` + +### Key Design Patterns + +- **Async-first**: All methods use `async/await` for non-blocking I/O +- **Thread-safe**: Require `Send + Sync` for use across async boundaries +- **Arc wrapper**: Share database connections across async tasks safely +- **Error conversion**: Convert backend-specific errors to `AppError` +- **Polymorphic cloning**: `clone_box()` allows trait objects to be cloned + +## Tauri Commands + +All library commands are prefixed with `library_` for clarity and follow a consistent pattern: + +### `library_open(uri: String) -> Result<(), String>` + +Opens a library from the specified URI and stores it in application state. + +**Example frontend usage:** +```typescript +const libraryPath = "/home/user/.local/share/calibre/"; +await invoke("library_open", { uri: `calibre://${libraryPath}` }); +``` + +### `library_get_all_books() -> Result, String>` + +Returns all books from the currently open library. The library must be opened first via `library_open`. + +### `library_get_book(book_id: u32) -> Result` + +Returns a single book with the specified ID from the currently open library. + +### `library_get_book_count() -> Result` + +Returns the total count of books in the currently open library. + +## State Management + +- `LibraryState` holds an `Arc>>>` +- The `Arc` allows safe sharing across async tasks +- The `Mutex` serializes access to prevent data races +- The `Option` indicates whether a library is currently open +- The trait object (`Box`) stores any implementation + +## Send + Sync Implementation + +When implementing `BookLibrary` for thread-safe resources, implementations may need to declare `unsafe impl Send + Sync`. This is required when: +1. The underlying resource provides thread-safe guarantees (SQLite, etc.) +2. Operations are read-only or properly synchronized +3. Reference counting (Arc) is used for safe sharing +4. No mutable state is exposed across thread boundaries + +For detailed information about `unsafe impl Send + Sync` and its justification, see `library.calibre.instructions.md` for the Calibre implementation example. + +## Adding New Library Implementations + +To add support for a new library type: + +1. Create a new module (e.g., `kobo.rs`) implementing the `BookLibrary` trait +2. Implement required methods: + - `async fn get_all_books(&self) -> Result, AppError>` + - `async fn get_book(&self, book_id: u32) -> Result` + - `async fn get_book_count(&self) -> Result` + - `fn clone_box(&self) -> Box` +3. Add necessary trait implementations (Send, Sync, Clone, etc.) +4. Add a variant to the `LibraryUri` enum in `commands.rs` +5. Update `LibraryUri::parse()` to recognize the new URI scheme +6. Update the match statement in `library_open` to instantiate your implementation +7. Add your module to `mod.rs` and export from `base.rs` + +Example implementation steps: + +```rust +// In kobo.rs +#[derive(Clone)] +pub struct KoboLibrary { + db: Arc, +} + +impl KoboLibrary { + pub fn new(library_path: PathBuf) -> Result { + // Implementation here + } +} + +unsafe impl Send for KoboLibrary {} +unsafe impl Sync for KoboLibrary {} + +#[async_trait] +impl BookLibrary for KoboLibrary { + async fn get_all_books(&self) -> Result, AppError> { + // Implementation + } + // ... other methods +} + +// In commands.rs LibraryUri::parse() +if let Some(path) = uri.strip_prefix("kobo://") { + Ok(LibraryUri::Kobo(PathBuf::from(path))) +} + +// In library_open match +LibraryUri::Kobo(path) => { + let kobo_library = KoboLibrary::new(path).map_err(|e| e.to_string())?; + Box::new(kobo_library) +} +``` + +## Error Handling + +Error handling follows the unified strategy defined in `error.instructions.md`. Key points for the library module: + +- **Tauri commands** return `Result` for proper serialization across IPC +- **Internal functions** use domain-specific error enums (e.g., `LibraryError`) for type safety +- **Error conversion** implements `From` traits to convert internal errors to `AppError` +- **Invalid URIs** are rejected at parse time with descriptive `ValidationError` messages +- **Missing libraries** should return `LibraryError` variants +- **Library not open** conditions should return appropriate `LibraryError` or `ValidationError` + +For detailed error handling patterns, see `error.instructions.md`. + +## Testing Guidelines + +- Test each implementation's `BookLibrary` trait methods independently +- Test URI parsing for valid and invalid formats +- Test state transitions (open, query, close) +- Verify error handling for missing libraries and invalid paths +- Use temporary directories for file-based library tests +- Never require external services or real libraries in tests +- Verify thread-safety with concurrent access patterns if needed + +## Code Organization + +Keep the module organized: +- Each library implementation in its own file +- Shared types and trait in `base.rs` +- Commands layer in `commands.rs` +- Export public API through `mod.rs` diff --git a/.github/instructions/ts.tests.instructions.md b/.github/instructions/ts.tests.instructions.md index b15bdd0..cb76e48 100644 --- a/.github/instructions/ts.tests.instructions.md +++ b/.github/instructions/ts.tests.instructions.md @@ -1,15 +1,83 @@ --- -applyTo: - - "**/*.test.tsx" - - "**/*.test.ts" - - "**/*.spec.tsx" - - "**/*.spec.ts" - - "**/__tests__/**/*.tsx" - - "**/__tests__/**/*.ts" +applyTo: "**/*.test.tsx,**/*.test.ts" --- # Frontend Tests -- Prefer using `getByTestId` (or `queryByTestId`, etc.) over `getByText` for selecting elements in frontend tests. This ensures selectors are robust against UI text changes and localization. +## React act() Warnings + +**CRITICAL: All React `act()` warnings MUST be addressed before committing code.** + +When testing React components, any code that causes state updates must be wrapped in `act()` to ensure tests accurately reflect how the component behaves in the browser. + +### Common Scenarios Requiring act() + +1. **Timer Advances (jest.advanceTimersByTime, jest.runAllTimers, etc.)** + ```typescript + import { act } from "@testing-library/react"; + + act(() => { + jest.advanceTimersByTime(1000); + }); + ``` + +2. **Manual State Updates in Tests** + ```typescript + act(() => { + // Code that triggers state updates + someFunction(); + }); + ``` + +3. **Cleanup with Pending Timers** + ```typescript + afterEach(() => { + act(() => { + jest.runOnlyPendingTimers(); + }); + jest.useRealTimers(); + }); + ``` + +### Why This Matters +- Ensures tests accurately simulate browser behavior +- Prevents flaky tests and race conditions +- Makes test assertions reliable and deterministic +- React requires this for proper testing of concurrent features + +**If you see "An update to [Component] inside a test was not wrapped in act(...)" warnings:** +1. Identify what's causing the state update (timers, async operations, etc.) +2. Wrap that code in `act()` +3. Re-run tests to verify warnings are gone + +## Test ID Selection +- Prefer using `getByTestId` (or `queryByTestId`, `findByTestId`, etc.) over `getByText` for selecting elements in frontend tests. This ensures selectors are robust against UI text changes and localization. - Only use `getByText` when there is no reasonable alternative (e.g. for verifying visible text content). - Always add a `data-testid` attribute to important elements/components that need to be targeted in tests. + +## Content Assertions +- When asserting text content within an element retrieved via `getByTestId`, check the element's content against **test data variables** rather than hardcoded strings. +- Use test data (like `mockBook.title`) to verify that rendered content matches the expected data. + +**Good:** +```typescript +const element = screen.getByTestId("book-title"); +expect(element).toHaveTextContent(mockBook.title); +``` + +**Avoid:** +```typescript +const element = screen.getByTestId("book-title"); +expect(element).toHaveTextContent("Test Book"); +``` + +**Also Good (when text is meant to be static UI):** +```typescript +expect(screen.getByTestId("error-message")).toBeInTheDocument(); +``` + +## Guidelines +- Define test data (mocks, fixtures) as constants at the top of test files +- Reference these constants in assertions instead of hardcoding strings +- This makes tests maintainable: when test data changes, assertions automatically reflect those changes +- Reduces test brittleness and improves clarity about what is being tested diff --git a/.github/instructions/typescript.typing.instructions.md b/.github/instructions/typescript.typing.instructions.md new file mode 100644 index 0000000..443c63e --- /dev/null +++ b/.github/instructions/typescript.typing.instructions.md @@ -0,0 +1,183 @@ +--- +applyTo: "**/*.{ts,tsx}" +--- + +# TypeScript Typing Standards + +Ensure strict typing across all TypeScript files to catch errors at compile time and improve code maintainability. + +## Core Principles + +- **No implicit `any`**: Never use implicit `any` types. Always explicitly declare types or infer them correctly. +- **Explicit return types**: All functions and methods must have explicit return type annotations. +- **Explicit parameter types**: All function parameters must be explicitly typed. +- **No `any` or `unknown`**: Avoid `any` and `unknown` types unless there is an explicit, documented reason. +- **Strict mode**: Ensure `strict: true` is enabled in `tsconfig.json`. + +## Guidelines + +### Function Declarations +```typescript +// Good - Explicit parameter and return types +function getBookTitle(book: Book): string { + return book.title; +} + +// Bad - Implicit any +function getBookTitle(book) { + return book.title; +} + +// Bad - No return type +function getBookTitle(book: Book) { + return book.title; +} +``` + +### Object/Interface Definitions +```typescript +// Good - Explicit typed object +interface SearchQuery { + field: string; + value: string; + isRegex?: boolean; +} + +// Bad - Implicit any +const query = { + field: "title", + value: "test", +}; +``` + +### useState and State Management +```typescript +// Good - Explicit type parameter +const [books, setBooks] = useState([]); +const [sortColumn, setSortColumn] = useState(null); + +// Bad - Implicit any +const [books, setBooks] = useState([]); +``` + +### Event Handlers +```typescript +// Good - Explicit event type +const handleClick = (event: React.MouseEvent): void => { + // Implementation +}; + +// Bad - Implicit any +const handleClick = (event) => { + // Implementation +}; +``` + +### Callback Functions +```typescript +// Good - Explicit parameter and return types +const handleToggle = (columnId: string): void => { + onToggleColumnVisibility(columnId); +}; + +// Bad - No types +const handleToggle = (columnId) => { + onToggleColumnVisibility(columnId); +}; +``` + +### useMemo and useCallback +```typescript +// Good - Explicit return type +const filteredBooks = useMemo( + (): Book[] => books.filter((book) => matchesSearch(book, query)), + [books, query], +); + +const handleSort = useCallback( + (columnId: string): void => { + // Implementation + }, + [], +); + +// Bad - No return type +const filteredBooks = useMemo( + () => books.filter((book) => matchesSearch(book, query)), + [books, query], +); +``` + +### Union and Optional Types +```typescript +// Good - Explicit union type +type SortDirection = "asc" | "desc" | null; +const [sortDirection, setSortDirection] = useState(null); + +// Good - Optional with explicit type +const error: string | null = null; +const handleError = (message?: string): void => {}; + +// Bad - Implicit any in optional +const handleError = (message?) => {}; +``` + +### Generic Types +```typescript +// Good - Explicit generic parameter +function parseQuery(data: string): Record { + // Implementation +} + +// Bad - Missing generic parameter +function parseQuery(data: string) { + // Implementation +} +``` + +### Array Types +```typescript +// Good - Explicit array type +const books: Book[] = []; +const tags: string[] = []; + +// Also good - Alternative syntax +const books: Array = []; + +// Bad - Implicit any +const books = []; +``` + +### Ref Types +```typescript +// Good - Explicit ref type +const resizingColumn = useRef(null); +const startX = useRef(0); + +// Bad - Implicit any +const resizingColumn = useRef(null); +``` + +## When `any` or `unknown` is Acceptable + +Only use `any` or `unknown` when: +1. Working with truly dynamic external data (e.g., JSON from API without schema) +2. Integrating with untyped third-party libraries +3. There is an explicit code comment explaining why +4. The situation is temporary and there is a task to fix it + +**Always document with a comment:** +```typescript +// TODO: Replace with proper type once API schema is defined +const data: any = response.data; + +// External library lacks TypeScript support +const result: unknown = externalLibrary.process(input); +``` + +## Verification + +- Run `pnpm build` to verify TypeScript compilation +- Run `pnpm lint` to check for type issues +- Ensure no TypeScript errors appear in the editor (red squiggles) +- Use `tsc --noEmit` to do a full type check without emitting files diff --git a/devenv.nix b/devenv.nix index add109a..eb08084 100644 --- a/devenv.nix +++ b/devenv.nix @@ -39,6 +39,8 @@ # D-Bus / desktop integration dbus xdg-utils + + sqlite ]; languages.rust = { @@ -68,7 +70,7 @@ scripts.dev-app.exec = "cargo tauri dev"; scripts.create-test-cbz.exec = "cd src-tauri && cargo run --bin create_test_cbz"; scripts.create-test-missing-comicinfo.exec = "cd src-tauri && cargo run --bin create_test_missing_comicinfo"; - scripts.test-app.exec = "jest && cargo test --manifest-path ./src-tauri/Cargo.toml"; + scripts.test-app.exec = "jest && cargo test --manifest-path ./src-tauri/Cargo.toml && cargo test --manifest-path ./src-tauri/calibre_db/Cargo.toml"; enterShell = '' export XDG_DATA_DIRS=${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS; @@ -80,7 +82,7 @@ pnpm test pnpm run build cargo test --manifest-path ./src-tauri/Cargo.toml - cargo check --manifest-path ./src-tauri/Cargo.toml + cargo check --manifest-path ./src-tauri/Cargo.toml cargo build --manifest-path ./src-tauri/Cargo.toml nix flake check nix eval diff --git a/package.json b/package.json index 248bcfe..04db423 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.38.0", + "@github/copilot": "^0.0.351", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/react-hooks": "^8.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index caea149..df375e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: '@eslint/js': specifier: ^9.38.0 version: 9.38.0 + '@github/copilot': + specifier: ^0.0.351 + version: 0.0.351 '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -424,6 +427,11 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@github/copilot@0.0.351': + resolution: {integrity: sha512-a3FgxgOvTi3uDVpcD0nxlTwKAGvILeSdf4NKYOAgSQ6HGANJhDGsBtk6O9nUnUAoskkGDDn7vrENM9svOs3sUw==} + engines: {node: '>=22'} + hasBin: true + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -3615,6 +3623,8 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@github/copilot@0.0.351': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6dde36d..e827a7e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6,7 +6,9 @@ version = 4 name = "Kikou" version = "0.1.0" dependencies = [ + "async-trait", "base64 0.22.1", + "calibre_db", "log", "notify", "once_cell", @@ -20,6 +22,7 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-log", "tempfile", + "tokio", "zip", ] @@ -60,6 +63,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -440,6 +455,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calibre_db" +version = "0.1.0" +dependencies = [ + "chrono", + "rusqlite", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "camino" version = "1.2.0" @@ -540,8 +566,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.0", ] @@ -1071,6 +1099,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -1586,7 +1626,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", ] [[package]] @@ -1595,6 +1644,15 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -2152,6 +2210,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-rs-sys" version = "0.5.2" @@ -3420,6 +3489,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.9.4", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rust_decimal" version = "1.38.0" @@ -4501,14 +4585,27 @@ dependencies = [ "io-uring", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "slab", "socket2", + "tokio-macros", "tracing", "windows-sys 0.59.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -4858,6 +4955,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index daa3685..3d14b8f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -5,7 +5,7 @@ description = "Kikou: Your All-in-One Hub for Organizing, Tagging, and Managing authors = ["Kevin Hellemun"] license = "" repository = "" -edition = "2024" +edition = "2021" rust-version = "1.86.0" default-run = "Kikou" @@ -33,3 +33,10 @@ once_cell = "1" tempfile = "3.23.0" base64 = "0.22" rayon = "1.10" +tokio = { version = "1.35", features = ["full"] } +async-trait = "0.1" +calibre_db = { path = "./calibre_db" } + +[[test]] +name = "app_tests" +path = "tests/lib_tests.rs" diff --git a/src-tauri/calibre_db/.gitignore b/src-tauri/calibre_db/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/src-tauri/calibre_db/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/src-tauri/calibre_db/Cargo.lock b/src-tauri/calibre_db/Cargo.lock new file mode 100644 index 0000000..5cc8f1c --- /dev/null +++ b/src-tauri/calibre_db/Cargo.lock @@ -0,0 +1,893 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "calibre_db" +version = "0.1.0" +dependencies = [ + "chrono", + "futures", + "rusqlite", + "serde", + "serde_json", + "tempfile", + "thiserror", + "tokio", +] + +[[package]] +name = "cc" +version = "1.2.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bbf3b3619004ad9bd139f62a9ab5cfe467f307455a0d307b0cf58bf070feaa" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/src-tauri/calibre_db/Cargo.toml b/src-tauri/calibre_db/Cargo.toml new file mode 100644 index 0000000..5c35164 --- /dev/null +++ b/src-tauri/calibre_db/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "calibre_db" +version = "0.1.0" +edition = "2021" +rust-version = "1.86.0" +description = "Rust library for interacting with Calibre SQLite databases" +license = "" +repository = "" + +[dependencies] +rusqlite = { version = "0.31", features = ["bundled", "chrono"] } +chrono = { version = "0.4", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +tokio = { version = "1.35", features = ["full"], optional = true } +futures = { version = "0.3", optional = true } + +[dev-dependencies] +tokio = { version = "1.35", features = ["full"] } +tempfile = "3.8" + +[features] +default = [] +async = ["tokio", "futures"] + +[[test]] +name = "integration_tests" +path = "tests/integration_tests.rs" \ No newline at end of file diff --git a/src-tauri/calibre_db/README.md b/src-tauri/calibre_db/README.md new file mode 100644 index 0000000..fe75b1f --- /dev/null +++ b/src-tauri/calibre_db/README.md @@ -0,0 +1,238 @@ +# calibre_db + +A Rust library for reading book metadata from Calibre SQLite databases. + +## Overview + +`calibre_db` provides safe, type-safe access to book metadata stored in Calibre's SQLite database format. It abstracts away the complexity of the database schema and query logic, offering a clean API for retrieving books, authors, series, tags, and other metadata. + +This crate is designed to be published on crates.io as a standalone library, making it easy to integrate Calibre database reading into any Rust application. + +## Features + +- **Type-safe models**: Strongly-typed structures for books, authors, series, tags, and identifiers +- **Comprehensive metadata**: Retrieve all book details including cover information, formats, ratings, and comments +- **Many-to-many relations**: Proper handling of complex relationships (authors per book, tags per book, etc.) +- **Error handling**: Comprehensive error types with conversions from common error sources +- **Thread-safe**: Uses `rusqlite` with safe connection handling +- **Well-tested**: Comprehensive unit and integration tests + +## Installation + +Add to your `Cargo.toml`: + +```toml +[dependencies] +calibre_db = "0.1" +``` + +## Quick Start + +```rust +use calibre_db::CalibreDatabase; + +fn main() -> Result<(), Box> { + let db = CalibreDatabase::open("/path/to/library/metadata.db")?; + + // Get a specific book + let book = db.get_book(1)?; + println!("Title: {}", book.title); + println!("Authors: {:?}", book.authors); + + // Get all books + let books = db.all_books()?; + println!("Total books: {}", books.len()); + + Ok(()) +} +``` + +## Database Schema + +The crate works with Calibre's standard SQLite schema, which includes: + +- **books**: Core book information (title, dates, ISBN, etc.) +- **authors**: Author metadata with sort names +- **publishers**: Publisher information +- **tags**: Tag/category information +- **series**: Series metadata +- **data**: Format information (EPUB, PDF, MOBI, etc.) +- **comments**: Book descriptions/comments +- **ratings**: Star ratings +- **identifiers**: Book identifiers (ISBN, DOI, etc.) +- **languages**: Language information + +All many-to-many relationships are properly handled through link tables. + +## API Overview + +### Main Entry Point + +```rust +pub struct CalibreDatabase { + // ... +} + +impl CalibreDatabase { + pub fn open>(path: P) -> Result; + pub fn get_book(&self, book_id: u32) -> Result; + pub fn all_books(&self) -> Result>; +} +``` + +### Core Models + +- `Book`: Complete book information with all relations +- `Author`: Author data with sort name +- `Series`: Series information +- `Tag`: Tag/category information +- `Identifier`: Book identifier (ISBN, DOI, etc.) +- `BookMetadata`: Flattened metadata summary + +## Examples + +### Retrieve a Single Book + +```rust +use calibre_db::CalibreDatabase; + +let db = CalibreDatabase::open("metadata.db")?; +let book = db.get_book(42)?; + +println!("Title: {}", book.title); +println!("Authors: {}", book.authors.iter() + .map(|a| &a.name) + .collect::>() + .join(", ")); +println!("Series: {}", book.series.as_ref().map(|s| &s.name).unwrap_or(&"None".to_string())); +println!("Tags: {}", book.tags.iter() + .map(|t| &t.name) + .collect::>() + .join(", ")); +``` + +### Retrieve All Books + +```rust +let db = CalibreDatabase::open("metadata.db")?; +let books = db.all_books()?; + +for book in books { + println!("{}: {} by {}", + book.id, + book.title, + book.author_sort); +} +``` + +### Access Book Details + +```rust +let book = db.get_book(1)?; + +// Basic information +println!("Title: {}", book.title); +println!("Path: {}", book.path); +println!("Has cover: {}", book.has_cover); + +// Dates +println!("Published: {}", book.pubdate); +println!("Added: {}", book.timestamp); + +// Relations +println!("Authors: {} ({})", + book.authors.len(), + book.author_sort); +println!("Publishers: {}", book.publishers.join(", ")); +println!("Tags: {}", book.tags.iter().map(|t| &t.name).collect::>().join(", ")); +println!("Languages: {}", book.languages.join(", ")); +println!("Formats: {}", book.formats.join(", ")); + +// Optional fields +if let Some(series) = &book.series { + println!("Series: {} ({})", series.name, book.series_index); +} +if let Some(rating) = book.rating { + println!("Rating: {}/5", rating); +} +if let Some(comments) = &book.comments { + println!("Comments: {}", comments); +} +``` + +## Error Handling + +The crate provides comprehensive error types: + +```rust +use calibre_db::{CalibreDatabase, CalibreDbError}; + +match db.get_book(9999) { + Ok(book) => println!("Found: {}", book.title), + Err(CalibreDbError::DatabaseError(msg)) => eprintln!("DB Error: {}", msg), + Err(CalibreDbError::NotFound(msg)) => eprintln!("Not found: {}", msg), + Err(e) => eprintln!("Error: {}", e), +} +``` + +## Performance Notes + +- Books are fetched with all related metadata in a single operation +- The database uses indexes for efficient querying +- Results are ordered by book sort name for consistency +- Consider caching if you need to query the same book multiple times + +## Thread Safety + +`CalibreDatabase` is thread-safe and can be safely shared across threads using `Arc`: + +```rust +use std::sync::Arc; +use calibre_db::CalibreDatabase; + +let db = Arc::new(CalibreDatabase::open("metadata.db")?); +let db_clone = Arc::clone(&db); + +std::thread::spawn(move || { + let book = db_clone.get_book(1); + // ... +}); +``` + +## Testing + +Run the test suite: + +```bash +cargo test +``` + +Run integration tests specifically: + +```bash +cargo test --test integration_tests +``` + +## Limitations + +- Read-only: This crate currently supports reading only; modifications to the database are not supported +- Calibre format required: The database must be in Calibre's SQLite format +- No async support: Operations are synchronous (async support may be added as a feature in the future) + +## Contributing + +Contributions are welcome! Please ensure: + +- All tests pass: `cargo test` +- Code is formatted: `cargo fmt` +- No clippy warnings: `cargo clippy` +- New features include tests + +## License + +[Your License Here] + +## Related Projects + +- [Calibre](https://calibre-ebook.com/): The main e-book management software +- [Kikou](https://github.com/yourusername/kikou): The Tauri application using this crate diff --git a/src-tauri/calibre_db/src/error.rs b/src-tauri/calibre_db/src/error.rs new file mode 100644 index 0000000..908f602 --- /dev/null +++ b/src-tauri/calibre_db/src/error.rs @@ -0,0 +1,45 @@ +use std::fmt; +use rusqlite; + +#[derive(Debug)] +pub enum CalibreDbError { + DatabaseError(String), + NotFound(String), + InvalidData(String), + IoError(String), + SerializationError(String), +} + +impl fmt::Display for CalibreDbError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CalibreDbError::DatabaseError(msg) => write!(f, "Database error: {}", msg), + CalibreDbError::NotFound(msg) => write!(f, "Not found: {}", msg), + CalibreDbError::InvalidData(msg) => write!(f, "Invalid data: {}", msg), + CalibreDbError::IoError(msg) => write!(f, "IO error: {}", msg), + CalibreDbError::SerializationError(msg) => write!(f, "Serialization error: {}", msg), + } + } +} + +impl std::error::Error for CalibreDbError {} + +impl From for CalibreDbError { + fn from(err: rusqlite::Error) -> Self { + CalibreDbError::DatabaseError(err.to_string()) + } +} + +impl From for CalibreDbError { + fn from(err: std::io::Error) -> Self { + CalibreDbError::IoError(err.to_string()) + } +} + +impl From for CalibreDbError { + fn from(err: serde_json::Error) -> Self { + CalibreDbError::SerializationError(err.to_string()) + } +} + +pub type Result = std::result::Result; \ No newline at end of file diff --git a/src-tauri/calibre_db/src/lib.rs b/src-tauri/calibre_db/src/lib.rs new file mode 100644 index 0000000..8c90613 --- /dev/null +++ b/src-tauri/calibre_db/src/lib.rs @@ -0,0 +1,48 @@ +mod error; +mod models; +mod schema; +mod queries; + +pub use error::{CalibreDbError, Result}; +pub use models::{Book, Author, Series, Tag, BookMetadata}; +pub use schema::DatabaseConnection; + +use std::path::Path; + +pub struct CalibreDatabase { + conn: DatabaseConnection, +} + +impl CalibreDatabase { + pub fn open>(path: P) -> Result { + let conn = DatabaseConnection::new(&path)?; + Ok(CalibreDatabase { conn }) + } + + pub fn connection(&self) -> &DatabaseConnection { + &self.conn + } + + pub fn get_book(&self, book_id: u32) -> Result { + queries::get_book(&self.conn, book_id) + } + + pub fn all_books(&self) -> Result> { + queries::all_books(&self.conn) + } + + pub fn books_iter(&self) -> Result> { + queries::all_books(&self.conn) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_creation() { + let err = CalibreDbError::DatabaseError("test".to_string()); + assert!(matches!(err, CalibreDbError::DatabaseError(_))); + } +} \ No newline at end of file diff --git a/src-tauri/calibre_db/src/models.rs b/src-tauri/calibre_db/src/models.rs new file mode 100644 index 0000000..483cc75 --- /dev/null +++ b/src-tauri/calibre_db/src/models.rs @@ -0,0 +1,324 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Book { + pub id: u32, + pub title: String, + pub sort: String, + pub timestamp: DateTime, + pub pubdate: DateTime, + pub series_index: f32, + pub author_sort: String, + pub isbn: String, + pub lccn: String, + pub path: String, + pub has_cover: bool, + pub authors: Vec, + pub publishers: Vec, + pub tags: Vec, + pub series: Option, + pub comments: Option, + pub rating: Option, + pub formats: Vec, + pub identifiers: Vec, + pub languages: Vec, +} + +impl Book { + pub fn new( + id: u32, + title: String, + sort: String, + timestamp: DateTime, + pubdate: DateTime, + series_index: f32, + author_sort: String, + isbn: String, + lccn: String, + path: String, + has_cover: bool, + ) -> Self { + Book { + id, + title, + sort, + timestamp, + pubdate, + series_index, + author_sort, + isbn, + lccn, + path, + has_cover, + authors: Vec::new(), + publishers: Vec::new(), + tags: Vec::new(), + series: None, + comments: None, + rating: None, + formats: Vec::new(), + identifiers: Vec::new(), + languages: Vec::new(), + } + } + + pub fn with_authors(mut self, authors: Vec) -> Self { + self.authors = authors; + self + } + + pub fn with_publishers(mut self, publishers: Vec) -> Self { + self.publishers = publishers; + self + } + + pub fn with_tags(mut self, tags: Vec) -> Self { + self.tags = tags; + self + } + + pub fn with_series(mut self, series: Option) -> Self { + self.series = series; + self + } + + pub fn with_comments(mut self, comments: Option) -> Self { + self.comments = comments; + self + } + + pub fn with_rating(mut self, rating: Option) -> Self { + self.rating = rating; + self + } + + pub fn with_formats(mut self, formats: Vec) -> Self { + self.formats = formats; + self + } + + pub fn with_identifiers(mut self, identifiers: Vec) -> Self { + self.identifiers = identifiers; + self + } + + pub fn with_languages(mut self, languages: Vec) -> Self { + self.languages = languages; + self + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Author { + pub id: u32, + pub name: String, + pub sort: String, + pub link: Option, +} + +impl Author { + pub fn new(id: u32, name: String, sort: String) -> Self { + Author { + id, + name, + sort, + link: None, + } + } + + pub fn with_link(mut self, link: String) -> Self { + self.link = Some(link); + self + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Series { + pub id: u32, + pub name: String, +} + +impl Series { + pub fn new(id: u32, name: String) -> Self { + Series { id, name } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Tag { + pub id: u32, + pub name: String, +} + +impl Tag { + pub fn new(id: u32, name: String) -> Self { + Tag { id, name } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Identifier { + pub book_id: u32, + pub kind: String, + pub val: String, +} + +impl Identifier { + pub fn new(book_id: u32, kind: String, val: String) -> Self { + Identifier { book_id, kind, val } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BookMetadata { + pub title: String, + pub authors: Vec, + pub publisher: Option, + pub pubdate: Option>, + pub series: Option, + pub series_index: Option, + pub tags: Vec, + pub comments: Option, + pub rating: Option, + pub isbn: Option, + pub languages: Vec, +} + +impl BookMetadata { + pub fn from_book(book: &Book) -> Self { + BookMetadata { + title: book.title.clone(), + authors: book.authors.iter().map(|a| a.name.clone()).collect(), + publisher: book.publishers.first().cloned(), + pubdate: Some(book.pubdate), + series: book.series.as_ref().map(|s| s.name.clone()), + series_index: if book.series.is_some() { + Some(book.series_index) + } else { + None + }, + tags: book.tags.iter().map(|t| t.name.clone()).collect(), + comments: book.comments.clone(), + rating: book.rating, + isbn: if book.isbn.is_empty() { + None + } else { + Some(book.isbn.clone()) + }, + languages: book.languages.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_book_creation() { + let now = Utc::now(); + let book = Book::new( + 1, + "Test Book".to_string(), + "book, test".to_string(), + now, + now, + 1.0, + "Author, Test".to_string(), + "123-456-789".to_string(), + "".to_string(), + "/path/to/book".to_string(), + true, + ); + + assert_eq!(book.id, 1); + assert_eq!(book.title, "Test Book"); + assert!(book.authors.is_empty()); + } + + #[test] + fn test_book_builder_chain() { + let now = Utc::now(); + let authors = vec![Author::new(1, "Test Author".to_string(), "Author, Test".to_string())]; + let tags = vec![Tag::new(1, "Fiction".to_string())]; + + let book = Book::new( + 1, + "Test Book".to_string(), + "book, test".to_string(), + now, + now, + 1.0, + "Author, Test".to_string(), + "123-456-789".to_string(), + "".to_string(), + "/path/to/book".to_string(), + true, + ) + .with_authors(authors) + .with_tags(tags); + + assert_eq!(book.authors.len(), 1); + assert_eq!(book.tags.len(), 1); + } + + #[test] + fn test_author_creation() { + let author = Author::new(1, "John Doe".to_string(), "Doe, John".to_string()); + assert_eq!(author.name, "John Doe"); + assert_eq!(author.sort, "Doe, John"); + assert!(author.link.is_none()); + } + + #[test] + fn test_series_creation() { + let series = Series::new(1, "Test Series".to_string()); + assert_eq!(series.name, "Test Series"); + } + + #[test] + fn test_tag_creation() { + let tag = Tag::new(1, "Science Fiction".to_string()); + assert_eq!(tag.name, "Science Fiction"); + } + + #[test] + fn test_identifier_creation() { + let id = Identifier::new(1, "isbn".to_string(), "123-456-789".to_string()); + assert_eq!(id.kind, "isbn"); + assert_eq!(id.val, "123-456-789"); + } + + #[test] + fn test_book_metadata_from_book() { + let now = Utc::now(); + let authors = vec![Author::new(1, "Test Author".to_string(), "Author, Test".to_string())]; + let tags = vec![Tag::new(1, "Fiction".to_string())]; + let series = Some(Series::new(1, "Test Series".to_string())); + + let book = Book::new( + 1, + "Test Book".to_string(), + "book, test".to_string(), + now, + now, + 1.5, + "Author, Test".to_string(), + "123-456-789".to_string(), + "".to_string(), + "/path/to/book".to_string(), + true, + ) + .with_authors(authors) + .with_tags(tags) + .with_series(series) + .with_rating(Some(4)); + + let metadata = BookMetadata::from_book(&book); + assert_eq!(metadata.title, "Test Book"); + assert_eq!(metadata.authors.len(), 1); + assert_eq!(metadata.rating, Some(4)); + assert_eq!(metadata.series, Some("Test Series".to_string())); + } +} \ No newline at end of file diff --git a/src-tauri/calibre_db/src/queries.rs b/src-tauri/calibre_db/src/queries.rs new file mode 100644 index 0000000..74431ca --- /dev/null +++ b/src-tauri/calibre_db/src/queries.rs @@ -0,0 +1,457 @@ +use crate::error::{CalibreDbError, Result}; +use crate::models::{Author, Book, Identifier, Series, Tag}; +use crate::schema::DatabaseConnection; +use chrono::{DateTime, Utc}; +use rusqlite::params; + +pub fn get_book(conn: &DatabaseConnection, book_id: u32) -> Result { + let mut book = conn.query_row( + "SELECT id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path, has_cover + FROM books WHERE id = ?1", + params![book_id], + |row| { + let timestamp_str: String = row.get(3)?; + let pubdate_str: String = row.get(4)?; + + Ok(Book::new( + row.get(0)?, + row.get(1)?, + row.get(2)?, + parse_timestamp(×tamp_str), + parse_timestamp(&pubdate_str), + row.get(5)?, + row.get(6)?, + row.get(7)?, + row.get(8)?, + row.get(9)?, + row.get::<_, i32>(10)? != 0, + )) + }, + )?; + + let bid = book.id; + book = book.with_authors(get_book_authors(conn, bid)?); + book = book.with_publishers(get_book_publishers(conn, bid)?); + book = book.with_tags(get_book_tags(conn, bid)?); + book = book.with_series(get_book_series(conn, bid)?); + book = book.with_comments(get_book_comments(conn, bid)?); + book = book.with_rating(get_book_rating(conn, bid)?); + book = book.with_formats(get_book_formats(conn, bid)?); + book = book.with_identifiers(get_book_identifiers(conn, bid)?); + book = book.with_languages(get_book_languages(conn, bid)?); + + Ok(book) +} + +pub fn all_books(conn: &DatabaseConnection) -> Result> { + let mut stmt = conn.prepare( + "SELECT id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path, has_cover + FROM books ORDER BY sort ASC")?; + + let books = stmt + .query_map([], |row| { + let timestamp_str: String = row.get(3)?; + let pubdate_str: String = row.get(4)?; + + Ok(Book::new( + row.get(0)?, + row.get(1)?, + row.get(2)?, + parse_timestamp(×tamp_str), + parse_timestamp(&pubdate_str), + row.get(5)?, + row.get(6)?, + row.get(7)?, + row.get(8)?, + row.get(9)?, + row.get::<_, i32>(10)? != 0, + )) + }) + .map_err(|e| CalibreDbError::from(e))? + .collect::, _>>() + .map_err(|e| CalibreDbError::from(e))?; + + let mut enriched_books = Vec::new(); + + for book in books { + let book_id = book.id; + let mut enriched = book; + enriched = enriched.with_authors(get_book_authors(conn, book_id)?); + enriched = enriched.with_publishers(get_book_publishers(conn, book_id)?); + enriched = enriched.with_tags(get_book_tags(conn, book_id)?); + enriched = enriched.with_series(get_book_series(conn, book_id)?); + enriched = enriched.with_comments(get_book_comments(conn, book_id)?); + enriched = enriched.with_rating(get_book_rating(conn, book_id)?); + enriched = enriched.with_formats(get_book_formats(conn, book_id)?); + enriched = enriched.with_identifiers(get_book_identifiers(conn, book_id)?); + enriched = enriched.with_languages(get_book_languages(conn, book_id)?); + + enriched_books.push(enriched); + } + + Ok(enriched_books) +} + +fn get_book_authors(conn: &DatabaseConnection, book_id: u32) -> Result> { + let mut stmt = conn.prepare( + "SELECT a.id, a.name, a.sort + FROM authors a + JOIN books_authors_link bal ON a.id = bal.author + WHERE bal.book = ?1 + ORDER BY bal.id ASC", + )?; + + let authors = stmt + .query_map(params![book_id], |row| { + Ok(Author::new(row.get(0)?, row.get(1)?, row.get(2)?)) + }) + .map_err(|e| CalibreDbError::from(e))? + .collect::, _>>() + .map_err(|e| CalibreDbError::from(e))?; + + Ok(authors) +} + +fn get_book_publishers(conn: &DatabaseConnection, book_id: u32) -> Result> { + let mut stmt = conn.prepare( + "SELECT p.name + FROM publishers p + JOIN books_publishers_link bpl ON p.id = bpl.publisher + WHERE bpl.book = ?1", + )?; + + let publishers = stmt + .query_map(params![book_id], |row| row.get(0)) + .map_err(|e| CalibreDbError::from(e))? + .collect::, _>>() + .map_err(|e| CalibreDbError::from(e))?; + + Ok(publishers) +} + +fn get_book_tags(conn: &DatabaseConnection, book_id: u32) -> Result> { + let mut stmt = conn.prepare( + "SELECT t.id, t.name + FROM tags t + JOIN books_tags_link btl ON t.id = btl.tag + WHERE btl.book = ?1", + )?; + + let tags = stmt + .query_map(params![book_id], |row| { + Ok(Tag::new(row.get(0)?, row.get(1)?)) + }) + .map_err(|e| CalibreDbError::from(e))? + .collect::, _>>() + .map_err(|e| CalibreDbError::from(e))?; + + Ok(tags) +} + +fn get_book_series(conn: &DatabaseConnection, book_id: u32) -> Result> { + let result = conn.query_row( + "SELECT s.id, s.name + FROM series s + JOIN books_series_link bsl ON s.id = bsl.series + WHERE bsl.book = ?1", + params![book_id], + |row| Ok(Series::new(row.get(0)?, row.get(1)?)), + ); + + match result { + Ok(series) => Ok(Some(series)), + Err(CalibreDbError::DatabaseError(ref msg)) if msg.contains("no rows") => Ok(None), + Err(e) => Err(e), + } +} + +fn get_book_comments(conn: &DatabaseConnection, book_id: u32) -> Result> { + let result = conn.query_row( + "SELECT text FROM comments WHERE book = ?1", + params![book_id], + |row| row.get(0), + ); + + match result { + Ok(text) => Ok(Some(text)), + Err(CalibreDbError::DatabaseError(ref msg)) if msg.contains("no rows") => Ok(None), + Err(e) => Err(e), + } +} + +fn get_book_rating(conn: &DatabaseConnection, book_id: u32) -> Result> { + let result = conn.query_row( + "SELECT r.rating + FROM ratings r + JOIN books_ratings_link brl ON r.id = brl.rating + WHERE brl.book = ?1", + params![book_id], + |row| { + let rating: u32 = row.get(0)?; + Ok((rating / 2) as u8) + }, + ); + + match result { + Ok(rating) => Ok(Some(rating)), + Err(CalibreDbError::DatabaseError(ref msg)) if msg.contains("no rows") => Ok(None), + Err(e) => Err(e), + } +} + +fn get_book_formats(conn: &DatabaseConnection, book_id: u32) -> Result> { + let mut stmt = conn.prepare("SELECT format FROM data WHERE book = ?1 ORDER BY id ASC")?; + + let formats = stmt + .query_map(params![book_id], |row| row.get(0)) + .map_err(|e| CalibreDbError::from(e))? + .collect::, _>>() + .map_err(|e| CalibreDbError::from(e))?; + + Ok(formats) +} + +fn get_book_identifiers(conn: &DatabaseConnection, book_id: u32) -> Result> { + let mut stmt = conn.prepare("SELECT book, type, val FROM identifiers WHERE book = ?1")?; + + let identifiers = stmt + .query_map(params![book_id], |row| { + Ok(Identifier::new(row.get(0)?, row.get(1)?, row.get(2)?)) + }) + .map_err(|e| CalibreDbError::from(e))? + .collect::, _>>() + .map_err(|e| CalibreDbError::from(e))?; + + Ok(identifiers) +} + +fn get_book_languages(conn: &DatabaseConnection, book_id: u32) -> Result> { + let mut stmt = conn.prepare( + "SELECT l.lang_code + FROM languages l + JOIN books_languages_link bll ON l.id = bll.lang_code + WHERE bll.book = ?1 + ORDER BY bll.item_order ASC", + )?; + + let languages = stmt + .query_map(params![book_id], |row| row.get(0)) + .map_err(|e| CalibreDbError::from(e))? + .collect::, _>>() + .map_err(|e| CalibreDbError::from(e))?; + + Ok(languages) +} + +fn parse_timestamp(timestamp_str: &str) -> DateTime { + DateTime::parse_from_rfc3339(timestamp_str) + .ok() + .and_then(|dt| Some(dt.with_timezone(&Utc))) + .unwrap_or_else(|| Utc::now()) +} + +#[cfg(test)] +mod tests { + use super::*; + use rusqlite::Connection; + use tempfile::NamedTempFile; + + fn create_test_db_with_books() -> Result<(DatabaseConnection, NamedTempFile)> { + let temp_file = NamedTempFile::new().map_err(|e| CalibreDbError::from(e))?; + let path = temp_file.path().to_path_buf(); + + let conn = Connection::open(&path)?; + + conn.execute( + "CREATE TABLE books ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + sort TEXT, + timestamp TIMESTAMP, + pubdate TIMESTAMP, + series_index REAL, + author_sort TEXT, + isbn TEXT, + lccn TEXT, + path TEXT, + has_cover BOOLEAN DEFAULT 0 + )", + [], + )?; + + conn.execute( + "INSERT INTO books (id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path, has_cover) + VALUES (1, 'Test Book', 'book, test', '2024-01-01T00:00:00Z', '2024-01-01T00:00:00Z', 1.0, 'Author, Test', '123-456', '', '/test', 0)", + [], + )?; + + conn.execute( + "CREATE TABLE authors ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + sort TEXT + )", + [], + )?; + + conn.execute( + "CREATE TABLE books_authors_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + author INTEGER + )", + [], + )?; + + conn.execute( + "CREATE TABLE publishers ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + )", + [], + )?; + + conn.execute( + "CREATE TABLE books_publishers_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + publisher INTEGER + )", + [], + )?; + + conn.execute( + "CREATE TABLE tags ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + )", + [], + )?; + + conn.execute( + "CREATE TABLE books_tags_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + tag INTEGER + )", + [], + )?; + + conn.execute( + "CREATE TABLE series ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + )", + [], + )?; + + conn.execute( + "CREATE TABLE books_series_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + series INTEGER + )", + [], + )?; + + conn.execute( + "CREATE TABLE comments ( + id INTEGER PRIMARY KEY, + book INTEGER, + text TEXT + )", + [], + )?; + + conn.execute( + "CREATE TABLE ratings ( + id INTEGER PRIMARY KEY, + rating INTEGER + )", + [], + )?; + + conn.execute( + "CREATE TABLE books_ratings_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + rating INTEGER + )", + [], + )?; + + conn.execute( + "CREATE TABLE data ( + id INTEGER PRIMARY KEY, + book INTEGER, + format TEXT + )", + [], + )?; + + conn.execute( + "CREATE TABLE identifiers ( + id INTEGER PRIMARY KEY, + book INTEGER, + type TEXT, + val TEXT + )", + [], + )?; + + conn.execute( + "CREATE TABLE languages ( + id INTEGER PRIMARY KEY, + lang_code TEXT NOT NULL + )", + [], + )?; + + conn.execute( + "CREATE TABLE books_languages_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + lang_code INTEGER, + item_order INTEGER DEFAULT 0 + )", + [], + )?; + + drop(conn); + + let db_conn = DatabaseConnection::new(&path)?; + Ok((db_conn, temp_file)) + } + + #[test] + fn test_get_book_basic() { + let result = create_test_db_with_books(); + assert!(result.is_ok()); + + if let Ok((db, _temp)) = result { + let book = get_book(&db, 1); + assert!(book.is_ok()); + + if let Ok(book) = book { + assert_eq!(book.id, 1); + assert_eq!(book.title, "Test Book"); + } + } + } + + #[test] + fn test_all_books() { + let result = create_test_db_with_books(); + assert!(result.is_ok()); + + if let Ok((db, _temp)) = result { + let books = all_books(&db); + assert!(books.is_ok()); + + if let Ok(books) = books { + assert_eq!(books.len(), 1); + assert_eq!(books[0].id, 1); + } + } + } +} diff --git a/src-tauri/calibre_db/src/schema.rs b/src-tauri/calibre_db/src/schema.rs new file mode 100644 index 0000000..04501ef --- /dev/null +++ b/src-tauri/calibre_db/src/schema.rs @@ -0,0 +1,122 @@ +use crate::error::{CalibreDbError, Result}; +use rusqlite::Connection; +use std::path::Path; + +pub struct DatabaseConnection { + conn: Connection, +} + +impl DatabaseConnection { + pub fn new>(path: P) -> Result { + let conn = Connection::open(path)?; + conn.pragma_update(None, "journal_mode", "WAL")?; + Ok(DatabaseConnection { conn }) + } + + pub fn query_row(&self, query: &str, params: &[&dyn rusqlite::ToSql], f: F) -> Result + where + F: FnOnce(&rusqlite::Row) -> rusqlite::Result, + { + self.conn + .query_row(query, params, f) + .map_err(|e| CalibreDbError::DatabaseError(e.to_string())) + } + + pub fn prepare(&self, query: &str) -> Result { + self.conn + .prepare(query) + .map_err(|e| CalibreDbError::DatabaseError(e.to_string())) + } + + pub fn execute(&self, query: &str, params: &[&dyn rusqlite::ToSql]) -> Result { + self.conn + .execute(query, params) + .map_err(|e| CalibreDbError::DatabaseError(e.to_string())) + } +} + +pub struct Schema; + +impl Schema { + pub fn validate_books_table(conn: &DatabaseConnection) -> Result<()> { + conn.query_row( + "SELECT id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path FROM books LIMIT 1", + &[], + |_| Ok(()), + ) + .or_else(|_| { + Err(CalibreDbError::InvalidData( + "books table does not have expected schema".to_string(), + )) + }) + } + + pub fn validate_authors_table(conn: &DatabaseConnection) -> Result<()> { + conn.query_row( + "SELECT id, name, sort FROM authors LIMIT 1", + &[], + |_| Ok(()), + ) + .or_else(|_| { + Err(CalibreDbError::InvalidData( + "authors table does not have expected schema".to_string(), + )) + }) + } + + pub fn validate_database(conn: &DatabaseConnection) -> Result<()> { + Self::validate_books_table(conn)?; + Self::validate_authors_table(conn)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::NamedTempFile; + + fn create_test_db() -> Result<(DatabaseConnection, NamedTempFile)> { + let temp_file = NamedTempFile::new().map_err(|e| CalibreDbError::from(e))?; + let path = temp_file.path().to_path_buf(); + + let conn = Connection::open(&path)?; + + conn.execute( + "CREATE TABLE books ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + sort TEXT, + timestamp TIMESTAMP, + pubdate TIMESTAMP, + series_index REAL, + author_sort TEXT, + isbn TEXT, + lccn TEXT, + path TEXT, + has_cover BOOLEAN DEFAULT 0 + )", + [], + )?; + + conn.execute( + "CREATE TABLE authors ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + sort TEXT + )", + [], + )?; + + drop(conn); + + let db_conn = DatabaseConnection::new(&path)?; + Ok((db_conn, temp_file)) + } + + #[test] + fn test_database_connection_creation() { + let result = create_test_db(); + assert!(result.is_ok()); + } +} \ No newline at end of file diff --git a/src-tauri/calibre_db/tests/integration_tests.rs b/src-tauri/calibre_db/tests/integration_tests.rs new file mode 100644 index 0000000..5cb41a5 --- /dev/null +++ b/src-tauri/calibre_db/tests/integration_tests.rs @@ -0,0 +1,518 @@ +#[cfg(test)] +mod integration_tests { + use calibre_db::CalibreDatabase; + use rusqlite::Connection; + use tempfile::NamedTempFile; + + fn create_test_database() -> (String, NamedTempFile) { + let temp_file = NamedTempFile::new().expect("Failed to create temp file"); + let path = temp_file.path().to_path_buf(); + let path_str = path.to_string_lossy().to_string(); + + let conn = Connection::open(&path).expect("Failed to open database"); + + conn.execute( + "CREATE TABLE books ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + sort TEXT, + timestamp TIMESTAMP, + pubdate TIMESTAMP, + series_index REAL, + author_sort TEXT, + isbn TEXT, + lccn TEXT, + path TEXT, + has_cover BOOLEAN DEFAULT 0 + )", + [], + ) + .expect("Failed to create books table"); + + conn.execute( + "INSERT INTO books (id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path, has_cover) + VALUES + (1, 'The Rust Programming Language', 'rust programming language, the', '2024-01-01T00:00:00Z', '2024-01-01T00:00:00Z', 1.0, 'Klabnik, Steve', '978-1491927281', '', '/library/book1', 1), + (2, 'Zero to Production in Rust', 'zero to production in rust', '2024-01-02T00:00:00Z', '2024-01-02T00:00:00Z', 1.0, 'Raita, Luca', '978-1617738586', '', '/library/book2', 0)", + [], + ) + .expect("Failed to insert books"); + + conn.execute( + "CREATE TABLE authors ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + sort TEXT + )", + [], + ) + .expect("Failed to create authors table"); + + conn.execute( + "INSERT INTO authors (id, name, sort) VALUES + (1, 'Steve Klabnik', 'Klabnik, Steve'), + (2, 'Luca Raita', 'Raita, Luca')", + [], + ) + .expect("Failed to insert authors"); + + conn.execute( + "CREATE TABLE books_authors_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + author INTEGER + )", + [], + ) + .expect("Failed to create books_authors_link table"); + + conn.execute( + "INSERT INTO books_authors_link (book, author) VALUES + (1, 1), + (2, 2)", + [], + ) + .expect("Failed to insert book-author links"); + + conn.execute( + "CREATE TABLE publishers ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + )", + [], + ) + .expect("Failed to create publishers table"); + + conn.execute( + "INSERT INTO publishers (id, name) VALUES + (1, 'No Starch Press'), + (2, 'Manning')", + [], + ) + .expect("Failed to insert publishers"); + + conn.execute( + "CREATE TABLE books_publishers_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + publisher INTEGER + )", + [], + ) + .expect("Failed to create books_publishers_link table"); + + conn.execute( + "INSERT INTO books_publishers_link (book, publisher) VALUES + (1, 1), + (2, 2)", + [], + ) + .expect("Failed to insert book-publisher links"); + + conn.execute( + "CREATE TABLE tags ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + )", + [], + ) + .expect("Failed to create tags table"); + + conn.execute( + "INSERT INTO tags (id, name) VALUES + (1, 'Programming'), + (2, 'Rust'), + (3, 'Systems'), + (4, 'Web')", + [], + ) + .expect("Failed to insert tags"); + + conn.execute( + "CREATE TABLE books_tags_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + tag INTEGER + )", + [], + ) + .expect("Failed to create books_tags_link table"); + + conn.execute( + "INSERT INTO books_tags_link (book, tag) VALUES + (1, 1), (1, 2), (1, 3), + (2, 1), (2, 2), (2, 4)", + [], + ) + .expect("Failed to insert book-tag links"); + + conn.execute( + "CREATE TABLE series ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + )", + [], + ) + .expect("Failed to create series table"); + + conn.execute( + "INSERT INTO series (id, name) VALUES + (1, 'Official Rust Book')", + [], + ) + .expect("Failed to insert series"); + + conn.execute( + "CREATE TABLE books_series_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + series INTEGER + )", + [], + ) + .expect("Failed to create books_series_link table"); + + conn.execute( + "INSERT INTO books_series_link (book, series) VALUES (1, 1)", + [], + ) + .expect("Failed to insert book-series links"); + + conn.execute( + "CREATE TABLE comments ( + id INTEGER PRIMARY KEY, + book INTEGER, + text TEXT + )", + [], + ) + .expect("Failed to create comments table"); + + conn.execute( + "INSERT INTO comments (book, text) VALUES + (1, 'Essential reading for Rust developers'), + (2, 'Great practical guide for production systems')", + [], + ) + .expect("Failed to insert comments"); + + conn.execute( + "CREATE TABLE ratings ( + id INTEGER PRIMARY KEY, + rating INTEGER + )", + [], + ) + .expect("Failed to create ratings table"); + + conn.execute( + "INSERT INTO ratings (id, rating) VALUES + (1, 10), + (2, 8)", + [], + ) + .expect("Failed to insert ratings"); + + conn.execute( + "CREATE TABLE books_ratings_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + rating INTEGER + )", + [], + ) + .expect("Failed to create books_ratings_link table"); + + conn.execute( + "INSERT INTO books_ratings_link (book, rating) VALUES + (1, 1), + (2, 2)", + [], + ) + .expect("Failed to insert book-rating links"); + + conn.execute( + "CREATE TABLE data ( + id INTEGER PRIMARY KEY, + book INTEGER, + format TEXT + )", + [], + ) + .expect("Failed to create data table"); + + conn.execute( + "INSERT INTO data (book, format) VALUES + (1, 'EPUB'), (1, 'PDF'), + (2, 'EPUB'), (2, 'MOBI')", + [], + ) + .expect("Failed to insert formats"); + + conn.execute( + "CREATE TABLE identifiers ( + id INTEGER PRIMARY KEY, + book INTEGER, + type TEXT, + val TEXT + )", + [], + ) + .expect("Failed to create identifiers table"); + + conn.execute( + "INSERT INTO identifiers (book, type, val) VALUES + (1, 'isbn', '978-1491927281'), + (2, 'isbn', '978-1617738586')", + [], + ) + .expect("Failed to insert identifiers"); + + conn.execute( + "CREATE TABLE languages ( + id INTEGER PRIMARY KEY, + lang_code TEXT NOT NULL + )", + [], + ) + .expect("Failed to create languages table"); + + conn.execute( + "INSERT INTO languages (id, lang_code) VALUES + (1, 'en'), + (2, 'es')", + [], + ) + .expect("Failed to insert languages"); + + conn.execute( + "CREATE TABLE books_languages_link ( + id INTEGER PRIMARY KEY, + book INTEGER, + lang_code INTEGER, + item_order INTEGER DEFAULT 0 + )", + [], + ) + .expect("Failed to create books_languages_link table"); + + conn.execute( + "INSERT INTO books_languages_link (book, lang_code, item_order) VALUES + (1, 1, 0), (1, 2, 1), + (2, 1, 0)", + [], + ) + .expect("Failed to insert book-language links"); + + drop(conn); + + (path_str, temp_file) + } + + #[test] + fn test_open_database() { + let (path, _temp) = create_test_database(); + let result = CalibreDatabase::open(&path); + assert!(result.is_ok()); + } + + #[test] + fn test_get_single_book() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + assert_eq!(book.id, 1); + assert_eq!(book.title, "The Rust Programming Language"); + assert_eq!(book.isbn, "978-1491927281"); + } + + #[test] + fn test_get_book_with_authors() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + assert_eq!(book.authors.len(), 1); + assert_eq!(book.authors[0].name, "Steve Klabnik"); + assert_eq!(book.authors[0].sort, "Klabnik, Steve"); + } + + #[test] + fn test_get_book_with_publishers() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + assert_eq!(book.publishers.len(), 1); + assert_eq!(book.publishers[0], "No Starch Press"); + } + + #[test] + fn test_get_book_with_tags() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + assert_eq!(book.tags.len(), 3); + assert_eq!(book.tags[0].name, "Programming"); + assert_eq!(book.tags[1].name, "Rust"); + assert_eq!(book.tags[2].name, "Systems"); + } + + #[test] + fn test_get_book_with_series() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + assert!(book.series.is_some()); + assert_eq!(book.series.unwrap().name, "Official Rust Book"); + } + + #[test] + fn test_get_book_with_comments() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + assert!(book.comments.is_some()); + assert_eq!( + book.comments.unwrap(), + "Essential reading for Rust developers" + ); + } + + #[test] + fn test_get_book_with_rating() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + assert!(book.rating.is_some()); + assert_eq!(book.rating.unwrap(), 5); + } + + #[test] + fn test_get_book_with_formats() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + assert_eq!(book.formats.len(), 2); + assert!(book.formats.contains(&"EPUB".to_string())); + assert!(book.formats.contains(&"PDF".to_string())); + } + + #[test] + fn test_get_book_with_identifiers() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + assert_eq!(book.identifiers.len(), 1); + assert_eq!(book.identifiers[0].kind, "isbn"); + assert_eq!(book.identifiers[0].val, "978-1491927281"); + } + + #[test] + fn test_get_book_with_languages() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + assert_eq!(book.languages.len(), 2); + assert!(book.languages.contains(&"en".to_string())); + assert!(book.languages.contains(&"es".to_string())); + } + + #[test] + fn test_get_all_books() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let books = db.all_books().expect("Failed to get all books"); + assert_eq!(books.len(), 2); + + assert_eq!(books[0].id, 1); + assert_eq!(books[1].id, 2); + } + + #[test] + fn test_all_books_ordered_by_sort() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let books = db.all_books().expect("Failed to get all books"); + assert_eq!(books.len(), 2); + assert_eq!(books[0].sort, "rust programming language, the"); + assert_eq!(books[1].sort, "zero to production in rust"); + } + + #[test] + fn test_get_book_nonexistent() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let result = db.get_book(9999); + assert!(result.is_err()); + } + + #[test] + fn test_book_without_series() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(2).expect("Failed to get book"); + assert!(book.series.is_none()); + } + + #[test] + fn test_multiple_authors_per_book() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let conn = Connection::open(&path).expect("Failed to open database for modification"); + + conn.execute( + "INSERT INTO authors (id, name, sort) VALUES (3, 'Co Author', 'Author, Co')", + [], + ) + .expect("Failed to insert author"); + + conn.execute( + "INSERT INTO books_authors_link (book, author) VALUES (1, 3)", + [], + ) + .expect("Failed to insert author link"); + + drop(conn); + + let book = db.get_book(1).expect("Failed to get book"); + assert_eq!(book.authors.len(), 2); + } + + #[test] + fn test_book_complete_metadata() { + let (path, _temp) = create_test_database(); + let db = CalibreDatabase::open(&path).expect("Failed to open database"); + + let book = db.get_book(1).expect("Failed to get book"); + + assert_eq!(book.title, "The Rust Programming Language"); + assert_eq!(book.author_sort, "Klabnik, Steve"); + assert_eq!(book.series_index, 1.0); + assert!(book.has_cover); + assert_eq!(book.path, "/library/book1"); + assert!(!book.isbn.is_empty()); + assert_eq!(book.authors.len(), 1); + assert_eq!(book.publishers.len(), 1); + assert_eq!(book.tags.len(), 3); + assert!(book.series.is_some()); + assert!(book.comments.is_some()); + assert!(book.rating.is_some()); + assert_eq!(book.formats.len(), 2); + assert!(!book.identifiers.is_empty()); + assert_eq!(book.languages.len(), 2); + } +} diff --git a/src-tauri/src/archive/commands.rs b/src-tauri/src/archive/commands.rs index 4c60cbd..2457a37 100644 --- a/src-tauri/src/archive/commands.rs +++ b/src-tauri/src/archive/commands.rs @@ -2,9 +2,9 @@ use crate::archive::manager::start_archive_watch_for_creation; use super::manager::{start_archive_watcher, stop_archive_watcher}; use super::reader::{get_file_data, read_archive, stream_file_data_from_archive}; -use super::types::{LoadCbzResponse, ToErrorResponse, is_image_file}; +use super::types::{is_image_file, LoadCbzResponse, ToErrorResponse}; use super::writer::{delete_comicinfo_xml, save_comicinfo_xml_impl, save_page_settings_impl}; -use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD}; +use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; use log::debug; use serde::Serialize; use std::collections::HashMap; diff --git a/src-tauri/src/archive/mod.rs b/src-tauri/src/archive/mod.rs index 8ecd7d1..b333d9a 100644 --- a/src-tauri/src/archive/mod.rs +++ b/src-tauri/src/archive/mod.rs @@ -17,8 +17,8 @@ mod tests { use crate::comicinfo::{ComicInfo, ComicPageType}; use std::collections::HashMap; use std::io::{Read, Write}; - use zip::CompressionMethod as ZipCompressionMethod; use zip::write::FileOptions as ZipFileOptions; + use zip::CompressionMethod as ZipCompressionMethod; fn test_path(name: &str) -> String { let mut dir = std::env::temp_dir(); diff --git a/src-tauri/src/archive/reader.rs b/src-tauri/src/archive/reader.rs index 8cc8836..a87fb37 100644 --- a/src-tauri/src/archive/reader.rs +++ b/src-tauri/src/archive/reader.rs @@ -93,8 +93,8 @@ mod tests { use super::*; use std::io::Write; use std::sync::{Arc, Mutex}; - use zip::ZipWriter; use zip::write::FileOptions; + use zip::ZipWriter; struct TestArchive { _temp_file: tempfile::NamedTempFile, diff --git a/src-tauri/src/archive/watcher.rs b/src-tauri/src/archive/watcher.rs index 24aad1a..ffe0054 100644 --- a/src-tauri/src/archive/watcher.rs +++ b/src-tauri/src/archive/watcher.rs @@ -1,9 +1,9 @@ use crate::archive::event::{ArchiveEventEmitter, ArchiveEventType}; use log::debug; -use notify::{RecommendedWatcher, RecursiveMode, Result as NotifyResult, Watcher, event::Event}; +use notify::{event::Event, RecommendedWatcher, RecursiveMode, Result as NotifyResult, Watcher}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, mpsc}; +use std::sync::{mpsc, Arc}; use std::thread; use std::time::{Duration, Instant}; diff --git a/src-tauri/src/archive/writer.rs b/src-tauri/src/archive/writer.rs index 6485a67..1d8cc1d 100644 --- a/src-tauri/src/archive/writer.rs +++ b/src-tauri/src/archive/writer.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; use std::fs; use std::io::{BufReader, BufWriter, Write}; -use zip::CompressionMethod; use zip::write::FileOptions; +use zip::CompressionMethod; use crate::comicinfo::{ComicInfo, ComicPageInfo, ComicPageType, Pages}; use log::debug; @@ -265,8 +265,8 @@ mod tests { use crate::comicinfo::{ComicInfo, ComicPageType}; use std::collections::HashMap; use std::io::{Read, Write}; - use zip::CompressionMethod as ZipCompressionMethod; use zip::write::FileOptions as ZipFileOptions; + use zip::CompressionMethod as ZipCompressionMethod; fn test_path(name: &str) -> String { let mut dir = std::env::temp_dir(); diff --git a/src-tauri/src/bin/create_test_cbz.rs b/src-tauri/src/bin/create_test_cbz.rs index 53dcaea..ec84b25 100644 --- a/src-tauri/src/bin/create_test_cbz.rs +++ b/src-tauri/src/bin/create_test_cbz.rs @@ -1,6 +1,6 @@ use std::fs::{self, File}; use std::io::{self, Write}; -use zip::{ZipWriter, write::FileOptions}; +use zip::{write::FileOptions, ZipWriter}; fn main() -> io::Result<()> { let output_path = "../tmp/test_invalid_comic.cbz"; diff --git a/src-tauri/src/bin/create_test_missing_comicinfo.rs b/src-tauri/src/bin/create_test_missing_comicinfo.rs index 737f716..46640ea 100644 --- a/src-tauri/src/bin/create_test_missing_comicinfo.rs +++ b/src-tauri/src/bin/create_test_missing_comicinfo.rs @@ -1,6 +1,6 @@ use std::fs::{self, File}; use std::io::{self, Write}; -use zip::{ZipWriter, write::FileOptions}; +use zip::{write::FileOptions, ZipWriter}; fn main() -> io::Result<()> { let output_path = "../tmp/test_missing_comicinfo.cbz"; diff --git a/src-tauri/src/comicinfo/info.rs b/src-tauri/src/comicinfo/info.rs index bb399a2..75dcf8d 100644 --- a/src-tauri/src/comicinfo/info.rs +++ b/src-tauri/src/comicinfo/info.rs @@ -1,7 +1,8 @@ use super::page::Pages; use super::types::{ - AgeRating, Manga, YesNo, default_age_rating, default_manga, default_minus_one, default_yes_no, - is_minus_one, is_unknown_age_rating, is_unknown_manga, is_unknown_yes_no, is_zero_i32, + default_age_rating, default_manga, default_minus_one, default_yes_no, is_minus_one, + is_unknown_age_rating, is_unknown_manga, is_unknown_yes_no, is_zero_i32, AgeRating, Manga, + YesNo, }; use crate::archive::types::{ErrorResponse, ErrorResponseType, ToErrorResponse}; use quick_xml::events::Event; diff --git a/src-tauri/src/comicinfo/mod.rs b/src-tauri/src/comicinfo/mod.rs index 661fc18..7665b67 100644 --- a/src-tauri/src/comicinfo/mod.rs +++ b/src-tauri/src/comicinfo/mod.rs @@ -3,7 +3,7 @@ pub mod info; pub mod page; pub mod types; -pub use info::{ComicInfo, get_bookmarked_pages}; +pub use info::{get_bookmarked_pages, ComicInfo}; pub use page::{ComicPageInfo, Pages}; pub use types::ComicPageType; @@ -112,13 +112,11 @@ A secret double life begins—one he can't tell anyone about! Some("AKB49: The Rules Against Love".to_string()) ); assert_eq!(comic.number, Some("1.0".to_string())); - assert!( - comic - .summary - .as_ref() - .unwrap() - .contains("A boy joins AKB48") - ); + assert!(comic + .summary + .as_ref() + .unwrap() + .contains("A boy joins AKB48")); assert_eq!(comic.year, 2010); assert_eq!(comic.month, 12); assert_eq!(comic.day, 17); diff --git a/src-tauri/src/comicinfo/page.rs b/src-tauri/src/comicinfo/page.rs index 0ffc89c..1ef8a5f 100644 --- a/src-tauri/src/comicinfo/page.rs +++ b/src-tauri/src/comicinfo/page.rs @@ -1,4 +1,4 @@ -use super::types::{ComicPageType, default_minus_one, is_false, is_minus_one, is_zero_i64}; +use super::types::{default_minus_one, is_false, is_minus_one, is_zero_i64, ComicPageType}; use serde::Deserialize; mod comic_page_type_option_serde { @@ -193,8 +193,8 @@ impl serde::Serialize for Pages { #[cfg(test)] mod tests { use super::*; - use quick_xml::Reader; use quick_xml::events::Event; + use quick_xml::Reader; #[test] fn test_page_attribute_order() { diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs new file mode 100644 index 0000000..cb64f2d --- /dev/null +++ b/src-tauri/src/error.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AppError { + LibraryError(String), + BookNotFound(String), + DatabaseError(String), + IoError(String), + ValidationError(String), +} + +impl std::fmt::Display for AppError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AppError::LibraryError(msg) => write!(f, "Library error: {}", msg), + AppError::BookNotFound(msg) => write!(f, "Book not found: {}", msg), + AppError::DatabaseError(msg) => write!(f, "Database error: {}", msg), + AppError::IoError(msg) => write!(f, "IO error: {}", msg), + AppError::ValidationError(msg) => write!(f, "Validation error: {}", msg), + } + } +} + +impl std::error::Error for AppError {} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bbad0da..55a46f6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,10 +1,15 @@ #[cfg_attr(mobile, tauri::mobile_entry_point)] mod archive; mod comicinfo; +pub mod error; +pub mod library; + +use library::commands::LibraryState; pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) + .manage(LibraryState::default()) .invoke_handler(tauri::generate_handler![ archive::load_cbz, archive::unload_cbz, @@ -19,6 +24,10 @@ pub fn run() { comicinfo::commands::get_bookmarked_pages, comicinfo::commands::validate_comicinfo_xml, comicinfo::commands::format_comicinfo_xml, + library::commands::library_open, + library::commands::library_get_all_books, + library::commands::library_get_book, + library::commands::library_get_book_count, ]) .setup(|app| { if cfg!(debug_assertions) { diff --git a/src-tauri/src/library/base.rs b/src-tauri/src/library/base.rs new file mode 100644 index 0000000..40f7efe --- /dev/null +++ b/src-tauri/src/library/base.rs @@ -0,0 +1,15 @@ +use async_trait::async_trait; +use calibre_db::Book; + +use crate::error::AppError; + +#[async_trait] +pub trait BookLibrary: Send + Sync { + async fn get_all_books(&self) -> Result, AppError>; + + async fn get_book(&self, book_id: u32) -> Result; + + async fn get_book_count(&self) -> Result; + + fn clone_box(&self) -> Box; +} diff --git a/src-tauri/src/library/calibre.rs b/src-tauri/src/library/calibre.rs new file mode 100644 index 0000000..58fb903 --- /dev/null +++ b/src-tauri/src/library/calibre.rs @@ -0,0 +1,74 @@ +use async_trait::async_trait; +use calibre_db::{Book, CalibreDatabase}; +use std::path::PathBuf; +use std::sync::Arc; + +use super::base::BookLibrary; +use crate::error::AppError; + +#[derive(Clone)] +pub struct CalibreLibrary { + db: Arc, +} + +impl CalibreLibrary { + pub fn new(library_path: PathBuf) -> Result { + let metadata_db_path = library_path.join("metadata.db"); + + if !metadata_db_path.exists() { + return Err(AppError::LibraryError(format!( + "Calibre database not found at: {}", + metadata_db_path.display() + ))); + } + + let db = CalibreDatabase::open(metadata_db_path) + .map_err(|e| AppError::LibraryError(e.to_string()))?; + + Ok(CalibreLibrary { db: Arc::new(db) }) + } +} + +// SAFETY: CalibreDatabase wraps rusqlite::Connection, which is thread-safe. +// rusqlite uses SQLite's built-in locking mechanisms to ensure safe concurrent access. +// All database operations in this crate are read-only, preventing data races. +// The Arc pattern safely shares the connection across async tasks. +unsafe impl Send for CalibreLibrary {} +unsafe impl Sync for CalibreLibrary {} + +#[async_trait] +impl BookLibrary for CalibreLibrary { + async fn get_all_books(&self) -> Result, AppError> { + self.db + .all_books() + .map_err(|e| AppError::LibraryError(e.to_string())) + } + + async fn get_book(&self, book_id: u32) -> Result { + self.db + .get_book(book_id) + .map_err(|e| AppError::LibraryError(e.to_string())) + } + + async fn get_book_count(&self) -> Result { + self.db + .all_books() + .map(|books| books.len() as u32) + .map_err(|e| AppError::LibraryError(e.to_string())) + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_invalid_library_path() { + let result = CalibreLibrary::new(PathBuf::from("/nonexistent/path")); + assert!(result.is_err()); + } +} diff --git a/src-tauri/src/library/commands.rs b/src-tauri/src/library/commands.rs new file mode 100644 index 0000000..a2070be --- /dev/null +++ b/src-tauri/src/library/commands.rs @@ -0,0 +1,119 @@ +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::Mutex; +use tauri::State; + +use calibre_db::Book; + +use super::{BookLibrary, CalibreLibrary}; + +pub struct LibraryState { + library: Arc>>>, +} + +impl LibraryState { + pub fn new() -> Self { + Self { + library: Arc::new(Mutex::new(None)), + } + } +} + +impl Default for LibraryState { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug)] +enum LibraryUri { + Calibre(PathBuf), +} + +impl LibraryUri { + fn parse(uri: &str) -> Result { + if let Some(path) = uri.strip_prefix("calibre://") { + Ok(LibraryUri::Calibre(PathBuf::from(path))) + } else { + Err(format!( + "Unknown library URI scheme. Expected 'calibre://', got '{}'", + uri + )) + } + } +} + +#[tauri::command] +pub async fn library_open(uri: String, state: State<'_, LibraryState>) -> Result<(), String> { + let library_uri = LibraryUri::parse(&uri)?; + + let library: Box = match library_uri { + LibraryUri::Calibre(path) => { + let calibre_library = CalibreLibrary::new(path).map_err(|e| e.to_string())?; + Box::new(calibre_library) + } + }; + + let mut library_guard = state + .library + .lock() + .map_err(|e| format!("Failed to acquire lock: {}", e))?; + + *library_guard = Some(library); + + Ok(()) +} + +#[tauri::command] +pub async fn library_get_all_books(state: State<'_, LibraryState>) -> Result, String> { + let library = { + let library_guard = state + .library + .lock() + .map_err(|e| format!("Failed to acquire lock: {}", e))?; + + library_guard + .as_ref() + .ok_or_else(|| "No library opened. Call library_open first.".to_string())? + .clone_box() + }; + + library.get_all_books().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn library_get_book( + book_id: u32, + state: State<'_, LibraryState>, +) -> Result { + let library = { + let library_guard = state + .library + .lock() + .map_err(|e| format!("Failed to acquire lock: {}", e))?; + + library_guard + .as_ref() + .ok_or_else(|| "No library opened. Call library_open first.".to_string())? + .clone_box() + }; + + library.get_book(book_id).await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn library_get_book_count(state: State<'_, LibraryState>) -> Result { + let library = { + let library_guard = state + .library + .lock() + .map_err(|e| format!("Failed to acquire lock: {}", e))?; + + library_guard + .as_ref() + .ok_or_else(|| "No library opened. Call library_open first.".to_string())? + .clone_box() + }; + + library.get_book_count().await.map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/library/mod.rs b/src-tauri/src/library/mod.rs new file mode 100644 index 0000000..1b41e0d --- /dev/null +++ b/src-tauri/src/library/mod.rs @@ -0,0 +1,6 @@ +pub mod base; +pub mod calibre; +pub mod commands; + +pub use base::BookLibrary; +pub use calibre::CalibreLibrary; diff --git a/src-tauri/tests/lib_tests.rs b/src-tauri/tests/lib_tests.rs new file mode 100644 index 0000000..7fb04ca --- /dev/null +++ b/src-tauri/tests/lib_tests.rs @@ -0,0 +1,30 @@ +#[cfg(test)] +mod tests { + use app_lib::error::AppError; + + #[test] + fn test_app_error_creation() { + let err = AppError::LibraryError("test error".to_string()); + assert!(matches!(err, AppError::LibraryError(_))); + } + + #[test] + fn test_app_error_display() { + let err = AppError::BookNotFound("book 123".to_string()); + let msg = format!("{}", err); + assert!(msg.contains("book 123")); + } + + #[test] + fn test_app_error_variants() { + let errors = vec![ + AppError::LibraryError("lib error".to_string()), + AppError::BookNotFound("not found".to_string()), + AppError::DatabaseError("db error".to_string()), + AppError::IoError("io error".to_string()), + AppError::ValidationError("validation error".to_string()), + ]; + + assert_eq!(errors.len(), 5); + } +} diff --git a/src/__tests__/components/BooksTable.test.tsx b/src/__tests__/components/BooksTable.test.tsx new file mode 100644 index 0000000..0ca9ecb --- /dev/null +++ b/src/__tests__/components/BooksTable.test.tsx @@ -0,0 +1,287 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { BooksTable } from "@/components/BooksTable"; +import { Book } from "@/types/book"; +import { ColumnConfig } from "@/hooks/useTableColumns"; +import { renderWithProviders } from "@/test-utils/testUtils"; + +const mockBook: Book = { + id: 1, + title: "Test Book", + sort: "book, test", + timestamp: "2024-01-01T00:00:00Z", + pubdate: "2024-01-01T00:00:00Z", + series_index: 1.0, + author_sort: "Author, Test", + isbn: "123-456-789", + lccn: "", + path: "/test/path", + has_cover: true, + authors: [{ id: 1, name: "Test Author", sort: "Author, Test" }], + publishers: ["Test Publisher"], + tags: [ + { id: 1, name: "Fiction" }, + { id: 2, name: "Adventure" }, + ], + series: { id: 1, name: "Test Series" }, + comments: "Test comment", + rating: 4, + formats: ["EPUB", "PDF"], + identifiers: [{ book_id: 1, kind: "isbn", val: "123-456-789" }], + languages: ["en", "es"], +}; + +const mockColumns: ColumnConfig[] = [ + { id: "cover", label: "Cover", visible: true, order: 0 }, + { id: "title", label: "Title", visible: true, order: 1 }, + { id: "authors", label: "Authors", visible: true, order: 2 }, + { id: "tags", label: "Tags", visible: true, order: 3 }, + { id: "isbn", label: "ISBN", visible: false, order: 4 }, +]; + +const mockBook2 = { ...mockBook, id: 2, title: "Another Book" }; +const mockBook3 = { ...mockBook, id: 3, title: "Different Title" }; +const mockBookTagsFormatted = mockBook.tags.map((t) => t.name).join(", "); + +describe("BooksTable", () => { + const mockToggleColumnVisibility = jest.fn(); + const mockReorderColumns = jest.fn(); + const mockResetColumns = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("renders books in a table", () => { + renderWithProviders( + , + ); + + expect(screen.getByTestId("book-row-1")).toBeInTheDocument(); + expect(screen.getByText(mockBook.title)).toBeInTheDocument(); + expect(screen.getByText(mockBook.authors[0].name)).toBeInTheDocument(); + }); + + it("displays only visible columns", () => { + renderWithProviders( + , + ); + + expect(screen.getByTestId("column-header-title")).toBeInTheDocument(); + expect(screen.getByTestId("column-header-authors")).toBeInTheDocument(); + expect(screen.queryByTestId("column-header-isbn")).not.toBeInTheDocument(); + }); + + it("filters books by search query", async () => { + renderWithProviders( + , + ); + + const searchInput = screen.getByTestId("books-search-input") as HTMLInputElement; + await userEvent.type(searchInput, "Another"); + + expect(screen.getByText(mockBook2.title)).toBeInTheDocument(); + expect(screen.queryByText(mockBook.title)).not.toBeInTheDocument(); + }); + + it("shows all books when search query is empty", () => { + renderWithProviders( + , + ); + + expect(screen.getByText(mockBook.title)).toBeInTheDocument(); + expect(screen.getByText(mockBook2.title)).toBeInTheDocument(); + }); + + it("supports advanced field-specific search", async () => { + renderWithProviders( + , + ); + + const searchInput = screen.getByTestId("books-search-input") as HTMLInputElement; + await userEvent.type(searchInput, `title:"${mockBook.title}"`); + + expect(screen.getByText(mockBook.title)).toBeInTheDocument(); + expect(screen.queryByText(mockBook3.title)).not.toBeInTheDocument(); + }); + + it("opens column menu when Columns button is clicked", async () => { + renderWithProviders( + , + ); + + const columnsButton = screen.getByTestId("columns-menu-button"); + await userEvent.click(columnsButton); + + expect(screen.getByTestId("columns-menu")).toBeInTheDocument(); + }); + + it("toggles column visibility from menu", async () => { + renderWithProviders( + , + ); + + const columnsButton = screen.getByTestId("columns-menu-button"); + await userEvent.click(columnsButton); + + // Find the ISBN label and click it (this will trigger the MenuItem click) + const isbnLabel = await screen.findByText("ISBN"); + await userEvent.click(isbnLabel); + + expect(mockToggleColumnVisibility).toHaveBeenCalledWith("isbn"); + }); + + it("calls onResetColumns when reset button is clicked", async () => { + renderWithProviders( + , + ); + + const columnsButton = screen.getByTestId("columns-menu-button"); + await userEvent.click(columnsButton); + + const resetButton = screen.getByTestId("reset-columns-button"); + await userEvent.click(resetButton); + + expect(mockResetColumns).toHaveBeenCalled(); + }); + + it("displays empty state when no books match search", async () => { + renderWithProviders( + , + ); + + const searchInput = screen.getByTestId("books-search-input") as HTMLInputElement; + await userEvent.type(searchInput, "NonexistentBook"); + + expect(screen.getByTestId("empty-state")).toBeInTheDocument(); + }); + + it("displays error when error prop is provided", () => { + const errorMessage = "Test error message"; + renderWithProviders( + , + ); + + expect(screen.getByTestId("error-message")).toHaveTextContent(errorMessage); + }); + + it("displays loading message when isLoading is true", () => { + renderWithProviders( + , + ); + + expect(screen.getByTestId("loading-state")).toBeInTheDocument(); + }); + + it("displays book count at the bottom", () => { + const books = [mockBook, { ...mockBook, id: 2 }]; + renderWithProviders( + , + ); + + const bookCountElement = screen.getByTestId("book-count"); + expect(bookCountElement).toHaveTextContent(`Showing ${books.length} of ${books.length} books`); + }); + + it("formats array values with comma separation", () => { + renderWithProviders( + , + ); + + expect(screen.getByText(mockBookTagsFormatted)).toBeInTheDocument(); + }); + + it("renders cover skeleton for cover column", () => { + renderWithProviders( + , + ); + + expect(screen.getByTestId("cover-skeleton-1")).toBeInTheDocument(); + }); +}); diff --git a/src/__tests__/components/OpenLibraryDialog.test.tsx b/src/__tests__/components/OpenLibraryDialog.test.tsx new file mode 100644 index 0000000..59b4e36 --- /dev/null +++ b/src/__tests__/components/OpenLibraryDialog.test.tsx @@ -0,0 +1,295 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { OpenLibraryDialog } from "@/components/OpenLibraryDialog"; +import { renderWithProviders } from "@/test-utils/testUtils"; + +jest.mock("@tauri-apps/plugin-dialog"); + +import * as dialogPlugin from "@tauri-apps/plugin-dialog"; + +describe("OpenLibraryDialog", () => { + const mockOnClose = jest.fn(); + const mockOnLibrarySelected = jest.fn().mockResolvedValue(undefined); + + beforeEach(() => { + localStorage.clear(); + jest.clearAllMocks(); + }); + + it("renders dialog when open is true", () => { + renderWithProviders( + , + ); + + expect(screen.getByText("Open Calibre Library")).toBeInTheDocument(); + expect(screen.getByText(/Select the folder containing/)).toBeInTheDocument(); + }); + + it("does not render dialog when open is false", () => { + renderWithProviders( + , + ); + + expect(screen.queryByText("Open Calibre Library")).not.toBeInTheDocument(); + }); + + it("calls onClose when Cancel button is clicked", async () => { + renderWithProviders( + , + ); + + const cancelButton = screen.getByRole("button", { name: /Cancel/i }); + await userEvent.click(cancelButton); + + expect(mockOnClose).toHaveBeenCalled(); + }); + + it("opens file dialog when Browse button is clicked", async () => { + (dialogPlugin.open as jest.Mock).mockResolvedValue("/test/library/path"); + + renderWithProviders( + , + ); + + const browseButton = screen.getByRole("button", { name: /Browse/i }); + await userEvent.click(browseButton); + + await waitFor(() => { + expect(dialogPlugin.open).toHaveBeenCalled(); + }); + }); + + it("sets selected path when directory is selected", async () => { + (dialogPlugin.open as jest.Mock).mockResolvedValue("/test/library/path"); + + renderWithProviders( + , + ); + + const browseButton = screen.getByRole("button", { name: /Browse/i }); + await userEvent.click(browseButton); + + await waitFor(() => { + const pathInput = screen.getByDisplayValue("/test/library/path"); + expect(pathInput).toBeInTheDocument(); + }); + }); + + it("disables Open Library button when no path is selected", () => { + renderWithProviders( + , + ); + + const openButton = screen.getByRole("button", { name: /Open Library/i }); + expect(openButton).toBeDisabled(); + }); + + it("enables Open Library button when path is selected", async () => { + (dialogPlugin.open as jest.Mock).mockResolvedValue("/test/library/path"); + + renderWithProviders( + , + ); + + const browseButton = screen.getByRole("button", { name: /Browse/i }); + await userEvent.click(browseButton); + + await waitFor(() => { + const openButton = screen.getByRole("button", { name: /Open Library/i }); + expect(openButton).not.toBeDisabled(); + }); + }); + + it("calls onLibrarySelected with selected path", async () => { + (dialogPlugin.open as jest.Mock).mockResolvedValue("/test/library/path"); + + renderWithProviders( + , + ); + + const browseButton = screen.getByRole("button", { name: /Browse/i }); + await userEvent.click(browseButton); + + await waitFor(() => { + expect(screen.getByDisplayValue("/test/library/path")).toBeInTheDocument(); + }); + + const openButton = screen.getByRole("button", { name: /Open Library/i }); + await userEvent.click(openButton); + + await waitFor(() => { + expect(mockOnLibrarySelected).toHaveBeenCalledWith("/test/library/path"); + }); + }); + + it("displays error when file dialog fails", async () => { + (dialogPlugin.open as jest.Mock).mockRejectedValue(new Error("Dialog failed")); + + renderWithProviders( + , + ); + + const browseButton = screen.getByRole("button", { name: /Browse/i }); + await userEvent.click(browseButton); + + await waitFor(() => { + expect(screen.getByText(/Dialog failed/)).toBeInTheDocument(); + }); + }); + + it("displays error when onLibrarySelected fails", async () => { + (dialogPlugin.open as jest.Mock).mockResolvedValue("/test/library/path"); + const mockOnLibrarySelectedError = jest + .fn() + .mockRejectedValue(new Error("Library open failed")); + + renderWithProviders( + , + ); + + const browseButton = screen.getByRole("button", { name: /Browse/i }); + await userEvent.click(browseButton); + + await waitFor(() => { + expect(screen.getByDisplayValue("/test/library/path")).toBeInTheDocument(); + }); + + const openButton = screen.getByRole("button", { name: /Open Library/i }); + await userEvent.click(openButton); + + await waitFor(() => { + expect(screen.getByText(/Library open failed/)).toBeInTheDocument(); + }); + }); + + it("shows error when no path is provided and Open Library is clicked", async () => { + renderWithProviders( + , + ); + + const openButton = screen.getByRole("button", { name: /Open Library/i }); + expect(openButton).toBeDisabled(); + }); + + it("clears selected path when dialog closes", async () => { + (dialogPlugin.open as jest.Mock).mockResolvedValue("/test/library/path"); + + const { rerender } = renderWithProviders( + , + ); + + const browseButton = screen.getByRole("button", { name: /Browse/i }); + await userEvent.click(browseButton); + + await waitFor(() => { + expect(screen.getByDisplayValue("/test/library/path")).toBeInTheDocument(); + }); + + rerender( + , + ); + + rerender( + , + ); + + const pathInput = screen.queryByDisplayValue("/test/library/path"); + expect(pathInput).not.toBeInTheDocument(); + }); + + it("sets loading state when isLoading is true", () => { + renderWithProviders( + , + ); + + const openButton = screen.getByRole("button", { name: /Open Library/i }); + expect(openButton).toBeDisabled(); + }); + + it("calls onClose after successful library selection", async () => { + (dialogPlugin.open as jest.Mock).mockResolvedValue("/test/library/path"); + + renderWithProviders( + , + ); + + const browseButton = screen.getByRole("button", { name: /Browse/i }); + await userEvent.click(browseButton); + + await waitFor(() => { + expect(screen.getByDisplayValue("/test/library/path")).toBeInTheDocument(); + }); + + const openButton = screen.getByRole("button", { name: /Open Library/i }); + await userEvent.click(openButton); + + await waitFor(() => { + expect(mockOnClose).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/__tests__/hooks/useLibrary.test.ts b/src/__tests__/hooks/useLibrary.test.ts new file mode 100644 index 0000000..75d3569 --- /dev/null +++ b/src/__tests__/hooks/useLibrary.test.ts @@ -0,0 +1,395 @@ +import { renderHook, act, waitFor } from "@testing-library/react"; +import { useLibrary } from "@/hooks/useLibrary"; +import * as libraryApi from "@/api/library"; + +jest.mock("@/api/library"); + +const mockBooks = [ + { + id: 1, + title: "Test Book 1", + sort: "book, test", + timestamp: "2024-01-01T00:00:00Z", + pubdate: "2024-01-01T00:00:00Z", + series_index: 1.0, + author_sort: "Author, Test", + isbn: "123-456", + lccn: "", + path: "/test/path1", + has_cover: true, + authors: [{ id: 1, name: "Test Author", sort: "Author, Test" }], + publishers: ["Test Publisher"], + tags: [], + series: null, + comments: null, + rating: null, + formats: [], + identifiers: [], + languages: [], + }, + { + id: 2, + title: "Test Book 2", + sort: "book, test", + timestamp: "2024-01-02T00:00:00Z", + pubdate: "2024-01-02T00:00:00Z", + series_index: 1.0, + author_sort: "Author, Test", + isbn: "789-012", + lccn: "", + path: "/test/path2", + has_cover: false, + authors: [], + publishers: [], + tags: [], + series: null, + comments: null, + rating: null, + formats: [], + identifiers: [], + languages: [], + }, +]; + +describe("useLibrary", () => { + beforeEach(() => { + localStorage.clear(); + jest.clearAllMocks(); + }); + + afterEach(() => { + localStorage.clear(); + }); + + it("returns initial empty state", () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); + + const { result } = renderHook(() => useLibrary()); + + expect(result.current.books).toEqual([]); + expect(result.current.isLoading).toBe(false); + expect(result.current.error).toBeNull(); + expect(result.current.libraryPath).toBeNull(); + expect(result.current.isLibraryOpen).toBe(false); + }); + + it("opens library with URI", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + expect(libraryApi.libraryOpen).toHaveBeenCalledWith("calibre:///test/path"); + expect(result.current.libraryPath).toBe("/test/path"); + expect(result.current.isLibraryOpen).toBe(true); + }); + + it("saves library path to localStorage when opening", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + const saved = localStorage.getItem("kikou_library_path"); + expect(saved).toBe("/test/path"); + }); + + it("handles error when opening library fails", async () => { + const errorMessage = "Failed to open library"; + (libraryApi.libraryOpen as jest.Mock).mockRejectedValueOnce( + new Error(errorMessage) + ); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/invalid/path"); + }); + + expect(result.current.error).toBe(errorMessage); + expect(result.current.isLibraryOpen).toBe(false); + }); + + it("loads books from open library", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + await act(async () => { + await result.current.loadBooks(); + }); + + expect(result.current.books).toEqual(mockBooks); + }); + + it("sets loading state while fetching books", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockImplementation( + () => + new Promise((resolve) => setTimeout(() => resolve(mockBooks), 50)) + ); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + await act(async () => { + await result.current.loadBooks(); + }); + + expect(result.current.isLoading).toBe(false); + expect(result.current.books.length).toBeGreaterThan(0); + }); + + it("handles error when loading books fails", async () => { + const errorMessage = "Failed to load books"; + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockRejectedValue( + new Error(errorMessage) + ); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + await act(async () => { + await result.current.loadBooks(); + }); + + expect(result.current.error).toBe(errorMessage); + expect(result.current.books).toEqual([]); + }); + + it("prevents loading books without open library", async () => { + // Explicitly clear localStorage and mocks to prevent auto-load from previous tests + localStorage.clear(); + jest.clearAllMocks(); + + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); + + const { result } = renderHook(() => useLibrary()); + + // Clear the mock after rendering to account for any auto-load effects + (libraryApi.libraryGetAllBooks as jest.Mock).mockClear(); + + await act(async () => { + await result.current.loadBooks(); + }); + + expect(result.current.error).toBe("No library opened"); + // The API should NOT be called when there is no library open + expect(libraryApi.libraryGetAllBooks).not.toHaveBeenCalled(); + }); + + it("loads library path from localStorage on component mount", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); + localStorage.setItem("kikou_library_path", "/saved/path"); + + const { result } = renderHook(() => useLibrary()); + + await waitFor(() => { + expect(result.current.libraryPath).toBe("/saved/path"); + }); + }); + + it("clears error when successfully opening library", async () => { + (libraryApi.libraryOpen as jest.Mock) + .mockRejectedValueOnce(new Error("First attempt failed")) + .mockResolvedValueOnce(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/path1"); + }); + + expect(result.current.error).toBeTruthy(); + + await act(async () => { + await result.current.openLibrary("/path2"); + }); + + expect(result.current.error).toBeNull(); + }); + + it("handles string error objects", async () => { + (libraryApi.libraryOpen as jest.Mock).mockRejectedValueOnce( + "String error message" + ); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + expect(result.current.error).toBe("Failed to open library"); + }); + + it("can manually load books after opening library", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + await waitFor(() => { + expect(result.current.isLibraryOpen).toBe(true); + }); + + // Reset the mock to count only the manual load call + (libraryApi.libraryGetAllBooks as jest.Mock).mockClear(); + + await act(async () => { + await result.current.loadBooks(); + }); + + expect(libraryApi.libraryGetAllBooks).toHaveBeenCalledTimes(1); + }); + + describe("Bug fix: Backend calls should be made when opening library with known path", () => { + it("should call libraryOpen and libraryGetAllBooks when opening a library", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + // Both API calls should be made + expect(libraryApi.libraryOpen).toHaveBeenCalledWith("calibre:///test/path"); + + // Manually load books to verify the call happens + await act(async () => { + await result.current.loadBooks(); + }); + + expect(libraryApi.libraryGetAllBooks).toHaveBeenCalled(); + }); + + it("should make backend calls when library path is set via savedPath then used", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); + + const { result } = renderHook(() => useLibrary()); + + // Simulate opening with an explicit call (not auto-load) + await act(async () => { + await result.current.openLibrary("/saved/path"); + }); + + // Verify the backend was called + expect(libraryApi.libraryOpen).toHaveBeenCalledWith( + "calibre:///saved/path" + ); + + // Load books + await act(async () => { + await result.current.loadBooks(); + }); + + expect(libraryApi.libraryGetAllBooks).toHaveBeenCalled(); + expect(result.current.books).toEqual(mockBooks); + }); + + it("should auto-load books after opening library with saved path", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); + + // Set saved path before rendering hook + localStorage.setItem("kikou_library_path", "/saved/path"); + + const { result } = renderHook(() => useLibrary()); + + // Wait for path to be loaded from localStorage + await waitFor(() => { + expect(result.current.libraryPath).toBe("/saved/path"); + }); + + // Wait for auto-load to happen + await waitFor(() => { + expect(libraryApi.libraryOpen).toHaveBeenCalledWith( + "calibre:///saved/path" + ); + }); + + // Wait for books to be auto-loaded + await waitFor(() => { + expect(libraryApi.libraryGetAllBooks).toHaveBeenCalled(); + }); + + await waitFor(() => { + expect(result.current.books).toEqual(mockBooks); + }); + }); + + it("should not call backend multiple times when opening same library", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); + + const { result } = renderHook(() => useLibrary()); + + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + const openCallCount = ( + libraryApi.libraryOpen as jest.Mock + ).mock.calls.length; + + // Try opening the same path again + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + // Should have called open twice (once for each call) + expect((libraryApi.libraryOpen as jest.Mock).mock.calls.length).toBe( + openCallCount + 1 + ); + }); + + it("should properly handle isLoading state during library operations", async () => { + (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); + (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); + + const { result } = renderHook(() => useLibrary()); + + expect(result.current.isLoading).toBe(false); + + await act(async () => { + await result.current.openLibrary("/test/path"); + }); + + // After opening, isLoading should be false and library should be open + expect(result.current.isLoading).toBe(false); + expect(result.current.isLibraryOpen).toBe(true); + }); + }); +}); diff --git a/src/__tests__/hooks/useTableColumns.test.ts b/src/__tests__/hooks/useTableColumns.test.ts new file mode 100644 index 0000000..22fd578 --- /dev/null +++ b/src/__tests__/hooks/useTableColumns.test.ts @@ -0,0 +1,178 @@ +import { renderHook, act } from "@testing-library/react"; +import { useTableColumns } from "@/hooks/useTableColumns"; + +describe("useTableColumns", () => { + beforeEach(() => { + localStorage.clear(); + jest.clearAllMocks(); + }); + + afterEach(() => { + localStorage.clear(); + }); + + it("returns default columns on first render", () => { + const { result } = renderHook(() => useTableColumns()); + + expect(result.current.columns).toHaveLength(11); + expect(result.current.visibleColumns.length).toBeGreaterThan(0); + }); + + it("loads columns from localStorage if they exist", () => { + const customColumns = [ + { id: "title", label: "Title", visible: false, order: 0 }, + { id: "authors", label: "Authors", visible: true, order: 1 }, + ]; + + localStorage.setItem("kikou_table_columns", JSON.stringify(customColumns)); + + const { result } = renderHook(() => useTableColumns()); + + expect(result.current.columns).toEqual(customColumns); + }); + + it("toggles column visibility", () => { + const { result } = renderHook(() => useTableColumns()); + + const titleColumn = result.current.columns.find((c) => c.id === "title"); + const initialVisibility = titleColumn?.visible; + + act(() => { + result.current.toggleColumnVisibility("title"); + }); + + const updatedColumn = result.current.columns.find((c) => c.id === "title"); + expect(updatedColumn?.visible).toBe(!initialVisibility); + }); + + it("saves columns to localStorage when toggling visibility", () => { + const { result } = renderHook(() => useTableColumns()); + + act(() => { + result.current.toggleColumnVisibility("isbn"); + }); + + const saved = localStorage.getItem("kikou_table_columns"); + expect(saved).toBeTruthy(); + + const parsedColumns = JSON.parse(saved!); + const isbnColumn = parsedColumns.find((c: any) => c.id === "isbn"); + expect(isbnColumn?.visible).toBe(true); + }); + + it("filters visible columns correctly", () => { + const { result } = renderHook(() => useTableColumns()); + + act(() => { + result.current.toggleColumnVisibility("isbn"); + result.current.toggleColumnVisibility("languages"); + }); + + const visibleIds = result.current.visibleColumns.map((c) => c.id); + expect(visibleIds).toContain("isbn"); + expect(visibleIds).toContain("languages"); + expect(visibleIds).not.toContain("formats"); + }); + + it("reorders columns", () => { + const { result } = renderHook(() => useTableColumns()); + + const initialOrder = result.current.columns.map((c) => c.id); + + act(() => { + result.current.reorderColumns("isbn", 0); + }); + + const newOrder = result.current.columns.map((c) => c.id); + expect(newOrder[0]).toBe("isbn"); + expect(newOrder).not.toEqual(initialOrder); + }); + + it("recalculates order numbers after reordering", () => { + const { result } = renderHook(() => useTableColumns()); + + act(() => { + result.current.reorderColumns("title", 5); + }); + + const orders = result.current.columns.map((c) => c.order); + const expectedOrders = Array.from({ length: orders.length }, (_, i) => i); + expect(orders).toEqual(expectedOrders); + }); + + it("persists reordered columns to localStorage", () => { + const { result } = renderHook(() => useTableColumns()); + + act(() => { + result.current.reorderColumns("isbn", 0); + }); + + const saved = localStorage.getItem("kikou_table_columns"); + const parsedColumns = JSON.parse(saved!); + expect(parsedColumns[0].id).toBe("isbn"); + }); + + it("resets columns to defaults", () => { + const { result } = renderHook(() => useTableColumns()); + + act(() => { + result.current.toggleColumnVisibility("title"); + result.current.toggleColumnVisibility("isbn"); + result.current.reorderColumns("authors", 0); + }); + + act(() => { + result.current.resetColumns(); + }); + + // After reset, columns should be back to defaults + expect(result.current.columns[0].id).toBe("cover"); + expect(result.current.columns[0].order).toBe(0); + + // Title should be visible again (default state) + const titleColumn = result.current.columns.find((c) => c.id === "title"); + expect(titleColumn?.visible).toBe(true); + expect(titleColumn?.order).toBe(1); + + // ISBN should be hidden again (default state) + const isbnColumn = result.current.columns.find((c) => c.id === "isbn"); + expect(isbnColumn?.visible).toBe(false); + expect(isbnColumn?.order).toBe(7); + }); + + it("handles invalid localStorage data gracefully", () => { + localStorage.setItem("kikou_table_columns", "invalid json"); + + const { result } = renderHook(() => useTableColumns()); + + expect(result.current.columns.length).toBeGreaterThan(0); + expect(result.current.columns[0].id).toBe("cover"); + }); + + it("maintains visible column order", () => { + const { result } = renderHook(() => useTableColumns()); + + act(() => { + result.current.toggleColumnVisibility("isbn"); + result.current.toggleColumnVisibility("languages"); + }); + + const visibleOrders = result.current.visibleColumns.map((c) => c.order); + const isSorted = visibleOrders.every( + (order, i) => i === 0 || order > visibleOrders[i - 1], + ); + expect(isSorted).toBe(true); + }); + + it("does not modify column if reordering with invalid id", () => { + const { result } = renderHook(() => useTableColumns()); + + const initialColumns = [...result.current.columns]; + + act(() => { + result.current.reorderColumns("nonexistent", 0); + }); + + expect(result.current.columns).toEqual(initialColumns); + }); +}); diff --git a/src/api/library.ts b/src/api/library.ts new file mode 100644 index 0000000..7292d2f --- /dev/null +++ b/src/api/library.ts @@ -0,0 +1,18 @@ +import { invoke } from "@tauri-apps/api/core"; +import { Book } from "@/types/book"; + +export async function libraryOpen(uri: string): Promise { + return await invoke("library_open", { uri }); +} + +export async function libraryGetAllBooks(): Promise { + return await invoke("library_get_all_books"); +} + +export async function libraryGetBook(bookId: number): Promise { + return await invoke("library_get_book", { book_id: bookId }); +} + +export async function libraryGetBookCount(): Promise { + return await invoke("library_get_book_count"); +} diff --git a/src/components/BookCoverSkeleton.tsx b/src/components/BookCoverSkeleton.tsx new file mode 100644 index 0000000..8747a24 --- /dev/null +++ b/src/components/BookCoverSkeleton.tsx @@ -0,0 +1,31 @@ +import { Box, Skeleton } from "@mui/joy"; + +export interface BookCoverSkeletonProps { + width?: number | string; + height?: number | string; +} + +export function BookCoverSkeleton({ + width = 60, + height = 90, +}: BookCoverSkeletonProps) { + return ( + + + + ); +} diff --git a/src/components/BooksTable.tsx b/src/components/BooksTable.tsx new file mode 100644 index 0000000..e0fb564 --- /dev/null +++ b/src/components/BooksTable.tsx @@ -0,0 +1,533 @@ +import { useState, useMemo, useRef, useCallback, useEffect } from "react"; +import { + Box, + Input, + Button, + Stack, + Typography, + Menu, + MenuItem, + Checkbox, + Alert, + Sheet, + Divider, +} from "@mui/joy"; +import { Book } from "@/types/book"; +import { BookCoverSkeleton } from "./BookCoverSkeleton"; +import { ColumnConfig } from "@/hooks/useTableColumns"; +import { devLog } from "@/utils/devLog"; + +export interface BooksTableProps { + books: Book[]; + columns: ColumnConfig[]; + onToggleColumnVisibility: (columnId: string) => void; + onReorderColumns: (columnId: string, newOrder: number) => void; + onResetColumns: () => void; + isLoading?: boolean; + error?: string | null; +} + +type SortDirection = "asc" | "desc" | null; + +const SEARCHABLE_COLUMNS = ["title", "authors", "publisher", "tags"]; + +function normalizeText(text: string): string { + return text.toLowerCase().trim(); +} + +function getBookFieldValue( + book: Book, + fieldId: string, +): string | string[] | number | boolean | null { + switch (fieldId) { + case "title": + return book.title; + case "authors": + return book.authors.map((a) => a.name); + case "series": + return book.series?.name || ""; + case "publisher": + return book.publishers[0] || ""; + case "pubdate": + return book.pubdate; + case "tags": + return book.tags.map((t) => t.name); + case "isbn": + return book.isbn; + case "languages": + return book.languages; + case "formats": + return book.formats; + case "rating": + return book.rating; + default: + return ""; + } +} + +function formatCellValue( + value: string | string[] | number | boolean | null, +): string { + if (value === null || value === undefined) { + return ""; + } + + if (Array.isArray(value)) { + return value.join(", "); + } + + if (typeof value === "boolean") { + return value ? "Yes" : "No"; + } + + return String(value); +} + +function parseSearchQuery(query: string): Record { + const result: Record = { _text: [] }; + const advancedPattern = /(\w+):(?:~?"([^"]+)"|([^\s]+))/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = advancedPattern.exec(query)) !== null) { + // Add text before this match to free text search + if (match.index > lastIndex) { + const textBefore = query.substring(lastIndex, match.index).trim(); + + if (textBefore) { + result._text.push(textBefore); + } + } + + const field = match[1]; + const value = match[2] || match[3]; + + if (!result[field]) { + result[field] = []; + } + + result[field].push(value); + + lastIndex = advancedPattern.lastIndex; + } + + // Add remaining text + if (lastIndex < query.length) { + const remainingText = query.substring(lastIndex).trim(); + + if (remainingText) { + result._text.push(remainingText); + } + } + + return result; +} + +function matchesSearch(book: Book, searchQuery: string): boolean { + if (!searchQuery.trim()) { + return true; + } + + const parsedQuery = parseSearchQuery(normalizeText(searchQuery)); + + // Check free text search + if (parsedQuery._text.length > 0) { + const freeTextMatches = parsedQuery._text.every((term) => { + return SEARCHABLE_COLUMNS.some((col) => { + const value = getBookFieldValue(book, col); + const normalizedValue = normalizeText(formatCellValue(value)); + + return normalizedValue.includes(term); + }); + }); + + if (!freeTextMatches) { + return false; + } + } + + // Check field-specific searches + for (const [field, terms] of Object.entries(parsedQuery)) { + if (field === "_text") { + continue; + } + + const fieldValue = normalizeText( + formatCellValue(getBookFieldValue(book, field)), + ); + const allTermsMatch = terms.every((term) => fieldValue.includes(term)); + + if (!allTermsMatch) { + return false; + } + } + + return true; +} + +export function BooksTable({ + books, + columns, + onToggleColumnVisibility, + onResetColumns, + isLoading = false, + error = null, +}: BooksTableProps): React.ReactElement { + const [searchQuery, setSearchQuery] = useState(""); + const [columnMenuAnchor, setColumnMenuAnchor] = useState( + null, + ); + const [columnWidths, setColumnWidths] = useState>({}); + const [sortColumn, setSortColumn] = useState(null); + const [sortDirection, setSortDirection] = useState(null); + + const visibleColumns = columns.filter((col) => col.visible); + + useEffect(() => { + if (!columnMenuAnchor) return; + + const handleClickOutside = (event: MouseEvent): void => { + const target = event.target as HTMLElement; + + if ( + columnMenuAnchor.contains(target) || + target.closest('[role="menu"]') + ) { + return; + } + + handleColumnMenuClose(); + }; + + const timeoutId = setTimeout(() => { + document.addEventListener("mousedown", handleClickOutside); + }, 100); + + return () => { + clearTimeout(timeoutId); + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [columnMenuAnchor]); + + const filteredBooks = useMemo( + (): Book[] => books.filter((book) => matchesSearch(book, searchQuery)), + [books, searchQuery], + ); + + const sortedBooks = useMemo(() => { + if (!sortColumn || !sortDirection) { + return filteredBooks; + } + + return [...filteredBooks].sort((a, b) => { + const aValue = getBookFieldValue(a, sortColumn); + const bValue = getBookFieldValue(b, sortColumn); + + const aStr = formatCellValue(aValue).toLowerCase(); + const bStr = formatCellValue(bValue).toLowerCase(); + + if (aStr < bStr) return sortDirection === "asc" ? -1 : 1; + if (aStr > bStr) return sortDirection === "asc" ? 1 : -1; + return 0; + }); + }, [filteredBooks, sortColumn, sortDirection]); + + const handleColumnMenuOpen = (event: React.MouseEvent): void => { + setColumnMenuAnchor(event.currentTarget); + }; + + const handleColumnMenuClose = (): void => { + setColumnMenuAnchor(null); + }; + + const handleSort = (columnId: string): void => { + if (columnId === "cover") return; + + if (sortColumn === columnId) { + if (sortDirection === "asc") { + setSortDirection("desc"); + } else if (sortDirection === "desc") { + setSortDirection(null); + setSortColumn(null); + } + } else { + setSortColumn(columnId); + setSortDirection("asc"); + } + }; + + const handleMouseDown = useCallback( + (columnId: string, e: React.MouseEvent): void => { + devLog("Mouse down on column:", columnId); + + e.preventDefault(); + + const startXPos = e.clientX; + const startColWidth = columnWidths[columnId] ?? 100; + + const handleMouseMove = (moveEvent: MouseEvent): void => { + const diff = moveEvent.clientX - startXPos; + const newWidth = Math.max(10, startColWidth + diff); + + devLog( + "Resizing column:", + columnId, + "diff:", + diff, + "New width:", + newWidth, + ); + + setColumnWidths((prev) => ({ + ...prev, + [columnId]: newWidth, + })); + }; + + const handleMouseUp = (): void => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [columnWidths], + ); + + const getColumnWidth = (columnId: string): number => { + if (columnId === "cover") { + return 100; + } + return columnWidths[columnId] ?? 300; + }; + + const getTotalTableWidth = (): number => { + return visibleColumns.reduce( + (total, col) => total + getColumnWidth(col.id), + 0, + ); + }; + + const renderCellContent = (book: Book, columnId: string): React.ReactNode => { + if (columnId === "cover") { + return ; + } + + const value = getBookFieldValue(book, columnId); + + return formatCellValue(value); + }; + + if (error) { + return ( + + Error: {error} + + ); + } + + if (isLoading) { + return ( + + Loading books... + + ); + } + + return ( + + {/* Search Bar */} + + setSearchQuery(e.target.value)} + sx={{ flexGrow: 1 }} + /> + + + + {/* Column Menu */} + + {columns.map((col) => ( + onToggleColumnVisibility(col.id)} + > + + onToggleColumnVisibility(col.id)} + onClick={(e) => e.stopPropagation()} + /> + + {col.label} + + + ))} + + + + onResetColumns()}> + Reset to Default + + + + {/* Table */} + {filteredBooks.length === 0 ? ( + + No books found + + ) : ( + + *:first-of-type": { + position: "sticky", + left: 0, + zIndex: 1, + backgroundColor: "var(--joy-palette-background-surface)", + }, + "& thead tr > *:first-of-type": { + backgroundColor: "var(--joy-palette-background-level1)", + zIndex: 4, + }, + }} + > + + + {visibleColumns.map((col) => ( + handleSort(col.id)} + > + + {col.label} + {col.id !== "cover" && sortColumn === col.id && ( + + {sortDirection === "asc" ? "↑" : "↓"} + + )} + + {col.id !== "cover" && ( + handleMouseDown(col.id, e)} + /> + )} + + ))} + + + + {sortedBooks.map((book) => ( + + {visibleColumns.map((col) => ( + + {col.id === "cover" ? ( + + {renderCellContent(book, col.id)} + + ) : ( + renderCellContent(book, col.id) + )} + + ))} + + ))} + + + + )} + + {/* Results Info */} + + Showing {filteredBooks.length} of {books.length} books + + + ); +} diff --git a/src/components/OpenLibraryDialog.tsx b/src/components/OpenLibraryDialog.tsx new file mode 100644 index 0000000..d6de212 --- /dev/null +++ b/src/components/OpenLibraryDialog.tsx @@ -0,0 +1,161 @@ +import { useState } from "react"; +import { + Modal, + ModalClose, + Sheet, + Button, + FormControl, + FormLabel, + Input, + Alert, + Stack, + Typography, +} from "@mui/joy"; +import { open } from "@tauri-apps/plugin-dialog"; + +export interface OpenLibraryDialogProps { + open: boolean; + onClose: () => void; + onLibrarySelected: (path: string) => Promise; + isLoading?: boolean; +} + +export function OpenLibraryDialog({ + open: isOpen, + onClose, + onLibrarySelected, + isLoading = false, +}: OpenLibraryDialogProps) { + const [selectedPath, setSelectedPath] = useState(""); + const [error, setError] = useState(null); + + const handleBrowse = async () => { + try { + setError(null); + const selected = await open({ + directory: true, + title: "Select Calibre Library", + defaultPath: selectedPath || undefined, + }); + + if (selected) { + setSelectedPath(selected); + } + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Failed to open file dialog"; + + setError(errorMessage); + } + }; + + const handleConfirm = async () => { + if (!selectedPath) { + setError("Please select a library path"); + return; + } + + try { + setError(null); + await onLibrarySelected(selectedPath); + setSelectedPath(""); + onClose(); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Failed to open library"; + + setError(errorMessage); + } + }; + + const handleClose = () => { + setSelectedPath(""); + setError(null); + onClose(); + }; + + return ( + + + + + Open Calibre Library + + + + + Select the folder containing the Calibre library (must have + metadata.db) + + + {error && ( + + {error} + + )} + + + Library Path + setSelectedPath(e.target.value)} + placeholder="Click Browse to select a folder" + readOnly + /> + + + + + + + + + + + + ); +} diff --git a/src/components/README.md b/src/components/README.md new file mode 100644 index 0000000..e144cb2 --- /dev/null +++ b/src/components/README.md @@ -0,0 +1,335 @@ +# Library UI Components + +This directory contains reusable React components for the Calibre library management interface. + +## Components + +### BooksTable + +Displays a searchable, filterable table of books with customizable columns. + +**Features:** +- Dynamic column visibility and reordering +- Advanced search with field-specific queries (e.g., `title:"search term"`) +- Free-text search across multiple columns +- Sticky first column for horizontal scrolling +- Book count display +- Empty state handling +- Error display +- Loading state + +**Props:** +```typescript +interface BooksTableProps { + books: Book[]; + columns: ColumnConfig[]; + onToggleColumnVisibility: (columnId: string) => void; + onReorderColumns: (columnId: string, newOrder: number) => void; + onResetColumns: () => void; + isLoading?: boolean; + error?: string | null; +} +``` + +**Search Syntax:** +- Free text: `adventure` - searches across all text columns +- Field specific: `title:"The Hobbit"` - searches only title field +- Multiple terms: `title:"Hobbit" author:"Tolkien"` - must match all terms +- Case insensitive and trimmed automatically + +**Example:** +```tsx +import { BooksTable } from "@/components/BooksTable"; + + +``` + +### BookCoverSkeleton + +Displays a loading skeleton for book covers. + +**Features:** +- Wave animation +- Customizable width and height +- Rounded corners +- Centered display + +**Props:** +```typescript +interface BookCoverSkeletonProps { + width?: number | string; + height?: number | string; +} +``` + +**Example:** +```tsx +import { BookCoverSkeleton } from "@/components/BookCoverSkeleton"; + + +``` + +### OpenLibraryDialog + +Modal dialog for selecting and opening a Calibre library. + +**Features:** +- File browser integration using Tauri +- Path validation +- Error handling and display +- Loading state +- Cancel and confirm actions +- Info alert about requirements + +**Props:** +```typescript +interface OpenLibraryDialogProps { + open: boolean; + onClose: () => void; + onLibrarySelected: (path: string) => Promise; + isLoading?: boolean; +} +``` + +**Example:** +```tsx +import { OpenLibraryDialog } from "@/components/OpenLibraryDialog"; + +const [dialogOpen, setDialogOpen] = useState(false); + + setDialogOpen(false)} + onLibrarySelected={handleLibrarySelected} + isLoading={false} +/> +``` + +## Hooks + +### useLibrary + +Manages library state and operations including opening libraries and loading books. + +**Returns:** +```typescript +interface UseLibraryReturn { + books: Book[]; + isLoading: boolean; + error: string | null; + libraryPath: string | null; + openLibrary: (path: string) => Promise; + loadBooks: () => Promise; + isLibraryOpen: boolean; +} +``` + +**Features:** +- Automatic localStorage persistence +- Auto-loads saved library on mount +- Auto-loads books after opening library +- Error handling and recovery +- Loading state management + +**Example:** +```tsx +import { useLibrary } from "@/hooks/useLibrary"; + +const { + books, + isLoading, + error, + libraryPath, + openLibrary, + loadBooks, + isLibraryOpen, +} = useLibrary(); +``` + +### useTableColumns + +Manages table column visibility and ordering with localStorage persistence. + +**Returns:** +```typescript +interface UseTableColumnsReturn { + columns: ColumnConfig[]; + visibleColumns: ColumnConfig[]; + toggleColumnVisibility: (columnId: string) => void; + reorderColumns: (columnId: string, newOrder: number) => void; + resetColumns: () => void; +} +``` + +**Features:** +- localStorage persistence +- Default column configuration +- Toggle visibility +- Reorder columns +- Reset to defaults +- Filter visible columns + +**Default Columns:** +- cover (visible) +- title (visible) +- authors (visible) +- series (visible) +- publisher (visible) +- pubdate (visible) +- tags (visible) +- isbn (hidden) +- languages (hidden) +- formats (hidden) +- rating (hidden) + +**Example:** +```tsx +import { useTableColumns } from "@/hooks/useTableColumns"; + +const { + columns, + visibleColumns, + toggleColumnVisibility, + reorderColumns, + resetColumns, +} = useTableColumns(); +``` + +## Data Types + +### Book + +```typescript +interface Book { + id: number; + title: string; + sort: string; + timestamp: string; + pubdate: string; + series_index: number; + author_sort: string; + isbn: string; + lccn: string; + path: string; + has_cover: boolean; + authors: Author[]; + publishers: string[]; + tags: Tag[]; + series: Series | null; + comments: string | null; + rating: number | null; + formats: string[]; + identifiers: Identifier[]; + languages: string[]; +} +``` + +### ColumnConfig + +```typescript +interface ColumnConfig { + id: string; + label: string; + visible: boolean; + order: number; +} +``` + +## Usage Example + +Complete example of using all components together: + +```tsx +import { useLibrary } from "@/hooks/useLibrary"; +import { useTableColumns } from "@/hooks/useTableColumns"; +import { BooksTable } from "@/components/BooksTable"; +import { OpenLibraryDialog } from "@/components/OpenLibraryDialog"; +import { useState } from "react"; + +export function LibraryPage() { + const [dialogOpen, setDialogOpen] = useState(false); + + const { + books, + isLoading, + error, + libraryPath, + openLibrary, + isLibraryOpen, + } = useLibrary(); + + const { + columns, + toggleColumnVisibility, + reorderColumns, + resetColumns, + } = useTableColumns(); + + if (!isLibraryOpen) { + return ( + <> + + + setDialogOpen(false)} + onLibrarySelected={openLibrary} + /> + + ); + } + + return ( + <> +

Library: {libraryPath}

+ + + + ); +} +``` + +## Testing + +All components have comprehensive test coverage. Run tests with: + +```bash +pnpm test +``` + +Test files are located in `src/__tests__/`: +- `components/BooksTable.test.tsx` - BooksTable component tests +- `components/OpenLibraryDialog.test.tsx` - OpenLibraryDialog component tests +- `hooks/useLibrary.test.ts` - useLibrary hook tests +- `hooks/useTableColumns.test.ts` - useTableColumns hook tests + +## localStorage Keys + +- `kikou_library_path` - Stores the currently open library path +- `kikou_table_columns` - Stores column visibility and order configuration + +## Notes + +- All components use Joy UI for consistent styling +- Components are fully typed with TypeScript +- Search is performed client-side for better responsiveness +- Book covers are currently placeholders with skeleton loaders +- Column ordering updates are persisted immediately to localStorage \ No newline at end of file diff --git a/src/components/__tests__/BooksTable.menu.test.tsx b/src/components/__tests__/BooksTable.menu.test.tsx new file mode 100644 index 0000000..92d1493 --- /dev/null +++ b/src/components/__tests__/BooksTable.menu.test.tsx @@ -0,0 +1,115 @@ +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { BooksTable } from "../BooksTable"; +import { Book } from "@/types/book"; + +const mockBooks: Book[] = [ + { + id: 1, + title: "Test Book", + sort: "Book, Test", + timestamp: "2024-01-01T00:00:00Z", + pubdate: "2024-01-01", + series_index: 0, + author_sort: "Author, Test", + authors: [{ id: 1, name: "Test Author", sort: "Author, Test" }], + publishers: ["Test Publisher"], + tags: [{ id: 1, name: "Fiction" }], + isbn: "1234567890", + lccn: "", + path: "test/path", + has_cover: false, + comments: null, + rating: 5, + series: null, + formats: ["EPUB"], + identifiers: [], + languages: ["en"], + }, +]; + +const mockColumns = [ + { id: "cover", label: "Cover", visible: true, order: 0 }, + { id: "title", label: "Title", visible: true, order: 1 }, + { id: "authors", label: "Authors", visible: true, order: 2 }, +]; + +const mockReorderColumns = jest.fn(); + +describe("BooksTable - Column Menu", () => { + it("should open column menu when Columns button is clicked", () => { + const mockToggle = jest.fn(); + const mockReset = jest.fn(); + + render( + , + ); + + const columnsButton = screen.getByRole("button", { name: /columns/i }); + fireEvent.click(columnsButton); + + // Check if menu is visible by finding the menu and checking for column checkboxes + expect(screen.getByTestId("column-checkbox-cover")).toBeInTheDocument(); + expect(screen.getByTestId("column-checkbox-title")).toBeInTheDocument(); + expect(screen.getByTestId("column-checkbox-authors")).toBeInTheDocument(); + }); + + it("should call onToggleColumnVisibility when clicking on a column menu item", () => { + const mockToggle = jest.fn(); + const mockReset = jest.fn(); + + render( + , + ); + + // Open menu + const columnsButton = screen.getByRole("button", { name: /columns/i }); + fireEvent.click(columnsButton); + + // Find the title menu item and click it + const titleMenuItems = screen.getAllByRole("menuitem").filter((item) => { + return item.textContent?.includes("Title"); + }); + + if (titleMenuItems.length > 0) { + fireEvent.click(titleMenuItems[0]); + expect(mockToggle).toHaveBeenCalledWith("title"); + } + }); + + it("should call onResetColumns when clicking Reset button", () => { + const mockToggle = jest.fn(); + const mockReset = jest.fn(); + + render( + , + ); + + // Open menu + const columnsButton = screen.getByRole("button", { name: /columns/i }); + fireEvent.click(columnsButton); + + // Click reset button + const resetButton = screen.getByTestId("reset-columns-button"); + fireEvent.click(resetButton); + + expect(mockReset).toHaveBeenCalled(); + }); +}); diff --git a/src/hooks/useLibrary.ts b/src/hooks/useLibrary.ts new file mode 100644 index 0000000..cc7c629 --- /dev/null +++ b/src/hooks/useLibrary.ts @@ -0,0 +1,116 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { Book } from "@/types/book"; +import { + libraryOpen, + libraryGetAllBooks, +} from "@/api/library"; + +const LIBRARY_PATH_KEY = "kikou_library_path"; + +export interface UseLibraryReturn { + books: Book[]; + isLoading: boolean; + error: string | null; + libraryPath: string | null; + openLibrary: (path: string) => Promise; + loadBooks: () => Promise; + isLibraryOpen: boolean; +} + +export function useLibrary(): UseLibraryReturn { + const [books, setBooks] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [libraryPath, setLibraryPath] = useState(null); + const [isLibraryOpen, setIsLibraryOpen] = useState(false); + const autoLoadAttemptedRef = useRef(false); + const isLibraryOpenRef = useRef(false); + + // Keep ref in sync with state + useEffect(() => { + isLibraryOpenRef.current = isLibraryOpen; + }, [isLibraryOpen]); + + // Load library path from localStorage on mount + useEffect(() => { + const savedPath = localStorage.getItem(LIBRARY_PATH_KEY); + + if (savedPath) { + setLibraryPath(savedPath); + } + }, []); + + // Open library when path is set + const openLibrary = useCallback(async (path: string) => { + try { + setIsLoading(true); + setError(null); + + const uri = `calibre://${path}`; + + await libraryOpen(uri); + localStorage.setItem(LIBRARY_PATH_KEY, path); + setLibraryPath(path); + setIsLibraryOpen(true); + + isLibraryOpenRef.current = true; + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Failed to open library"; + + setError(errorMessage); + setIsLibraryOpen(false); + + isLibraryOpenRef.current = false; + } finally { + setIsLoading(false); + } + }, []); + + // Load books from open library + const loadBooks = useCallback(async () => { + if (!isLibraryOpenRef.current) { + setError("No library opened"); + return; + } + + try { + setIsLoading(true); + setError(null); + + const loadedBooks = await libraryGetAllBooks(); + + setBooks(loadedBooks); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Failed to load books"; + + setError(errorMessage); + } finally { + setIsLoading(false); + } + }, []); + + // Auto-load library and books when libraryPath is loaded from localStorage + useEffect(() => { + if (libraryPath && !autoLoadAttemptedRef.current) { + autoLoadAttemptedRef.current = true; + openLibrary(libraryPath).then(() => { + // After opening, load books + setTimeout(() => { + loadBooks(); + }, 100); + }); + } + }, [libraryPath, openLibrary, loadBooks]); + + return { + books, + isLoading, + error, + libraryPath, + openLibrary, + loadBooks, + isLibraryOpen, + }; +} diff --git a/src/hooks/useTableColumns.ts b/src/hooks/useTableColumns.ts new file mode 100644 index 0000000..9a9c405 --- /dev/null +++ b/src/hooks/useTableColumns.ts @@ -0,0 +1,99 @@ +import { useState, useEffect, useCallback } from "react"; + +export interface ColumnConfig { + id: string; + label: string; + visible: boolean; + order: number; +} + +const TABLE_COLUMNS_KEY = "kikou_table_columns"; + +const DEFAULT_COLUMNS: ColumnConfig[] = [ + { id: "cover", label: "Cover", visible: true, order: 0 }, + { id: "title", label: "Title", visible: true, order: 1 }, + { id: "authors", label: "Authors", visible: true, order: 2 }, + { id: "series", label: "Series", visible: true, order: 3 }, + { id: "publisher", label: "Publisher", visible: true, order: 4 }, + { id: "pubdate", label: "Published", visible: true, order: 5 }, + { id: "tags", label: "Tags", visible: true, order: 6 }, + { id: "isbn", label: "ISBN", visible: false, order: 7 }, + { id: "languages", label: "Languages", visible: false, order: 8 }, + { id: "formats", label: "Formats", visible: false, order: 9 }, + { id: "rating", label: "Rating", visible: false, order: 10 }, +]; + +export interface UseTableColumnsReturn { + columns: ColumnConfig[]; + visibleColumns: ColumnConfig[]; + toggleColumnVisibility: (columnId: string) => void; + reorderColumns: (columnId: string, newOrder: number) => void; + resetColumns: () => void; +} + +export function useTableColumns(): UseTableColumnsReturn { + const [columns, setColumns] = useState(DEFAULT_COLUMNS); + + // Load columns from localStorage on mount + useEffect(() => { + const saved = localStorage.getItem(TABLE_COLUMNS_KEY); + + if (saved) { + try { + const parsedColumns = JSON.parse(saved) as ColumnConfig[]; + + setColumns(parsedColumns); + } catch { + // If parsing fails, use defaults + setColumns(DEFAULT_COLUMNS); + } + } + }, []); + + // Save columns to localStorage whenever they change + useEffect(() => { + localStorage.setItem(TABLE_COLUMNS_KEY, JSON.stringify(columns)); + }, [columns]); + + const toggleColumnVisibility = useCallback((columnId: string) => { + setColumns((prev) => + prev.map((col) => + col.id === columnId ? { ...col, visible: !col.visible } : col, + ), + ); + }, []); + + const reorderColumns = useCallback((columnId: string, newOrder: number) => { + setColumns((prev) => { + const updated = [...prev]; + const columnIndex = updated.findIndex((col) => col.id === columnId); + + if (columnIndex === -1) { + return prev; + } + + const column = updated[columnIndex]; + + updated.splice(columnIndex, 1); + updated.splice(newOrder, 0, column); + + // Recalculate all orders + return updated.map((col, index) => ({ ...col, order: index })); + }); + }, []); + + const resetColumns = useCallback(() => { + setColumns(DEFAULT_COLUMNS); + localStorage.removeItem(TABLE_COLUMNS_KEY); + }, []); + + const visibleColumns = columns.filter((col) => col.visible); + + return { + columns, + visibleColumns, + toggleColumnVisibility, + reorderColumns, + resetColumns, + }; +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 61804d8..dacc37d 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,16 +1,49 @@ "use client"; -import { useRouter } from "next/router"; -import { Box, Button, Typography, Card } from "@mui/joy"; +import { useState } from "react"; +import { + Box, + Button, + Typography, + Card, + Alert, + Stack, +} from "@mui/joy"; import { useResetNavigation } from "@/hooks/useResetNavigation"; +import { useLibrary } from "@/hooks/useLibrary"; +import { useTableColumns } from "@/hooks/useTableColumns"; +import { BooksTable } from "@/components/BooksTable"; +import { OpenLibraryDialog } from "@/components/OpenLibraryDialog"; +import LoadingOverlay from "@/components/ui/LoadingOverlay"; -export default function Page() { - const router = useRouter(); +export default function HomePage() { + const [openLibraryDialogOpen, setOpenLibraryDialogOpen] = useState(false); + + const { + books, + isLoading, + error, + libraryPath, + openLibrary, + isLibraryOpen, + } = useLibrary(); + + const { + columns, + toggleColumnVisibility, + reorderColumns, + resetColumns, + } = useTableColumns(); useResetNavigation(); - return ( - <> + const handleOpenLibrary = async (path: string) => { + await openLibrary(path); + }; + + // Show empty state when no library path is known + if (!libraryPath) { + return ( Kikou - + + Welcome to Kikou. Start by opening a Calibre library to view and + manage your books. + + + + + + setOpenLibraryDialogOpen(false)} + onLibrarySelected={handleOpenLibrary} + isLoading={isLoading} + /> + + ); + } + + // Show library view (even while loading or if there's an error) + return ( + <> + + + + + + Library + + + + {libraryPath && ( + + Location: {libraryPath} + + )} + + + {error && ( + + {error} + + )} + + + + + + setOpenLibraryDialogOpen(false)} + onLibrarySelected={handleOpenLibrary} + isLoading={isLoading} + /> ); diff --git a/src/styles/globals.css b/src/styles/globals.css index f25e9ab..820e426 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -66,3 +66,39 @@ textarea, -moz-user-select: text !important; -ms-user-select: text !important; } + +/* Enable scrolling for table container but not cells */ +.table-container { + overflow: auto !important; +} + +table td, +table th { + overflow: hidden !important; + user-select: text !important; + -webkit-user-select: text !important; + -moz-user-select: text !important; + -ms-user-select: text !important; +} + +/* Column resize handle */ +.resize-handle { + position: absolute; + top: 0; + right: 0; + width: 4px; + height: 100%; + cursor: col-resize; + user-select: none !important; + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; +} + +.resize-handle:hover { + background-color: var(--joy-palette-primary-500, #0b6bcb); +} + +.resize-handle.resizing { + background-color: var(--joy-palette-primary-600, #185ea5); +} diff --git a/src/types/book.ts b/src/types/book.ts new file mode 100644 index 0000000..6096d0c --- /dev/null +++ b/src/types/book.ts @@ -0,0 +1,45 @@ +export interface Author { + id: number; + name: string; + sort: string; + link?: string; +} + +export interface Series { + id: number; + name: string; +} + +export interface Tag { + id: number; + name: string; +} + +export interface Identifier { + book_id: number; + kind: string; + val: string; +} + +export interface Book { + id: number; + title: string; + sort: string; + timestamp: string; + pubdate: string; + series_index: number; + author_sort: string; + isbn: string; + lccn: string; + path: string; + has_cover: boolean; + authors: Author[]; + publishers: string[]; + tags: Tag[]; + series: Series | null; + comments: string | null; + rating: number | null; + formats: string[]; + identifiers: Identifier[]; + languages: string[]; +} From 07155ee74017afd33cf10df47766ecc23bd6cd92 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Wed, 5 Nov 2025 20:17:52 +0100 Subject: [PATCH 3/6] feat: implement book cover retrieval and streaming system Add comprehensive book cover functionality with library-agnostic architecture supporting streaming, caching, and proper test coverage. Backend uses trait-based design for easy library implementation while frontend provides React context and hooks for efficient cover display. - Add library-agnostic Book model with only frontend-used fields - Implement BookLibrary trait with get_book_cover method - Add library_stream_book_covers Tauri command for async streaming - Create BookCoverContext for global cover caching - Add useStreamBookCovers and useBookCover hooks - Convert BookCoverSkeleton to BookCover component with loading states - Integrate BookCoverProvider into PageWrapperFactory - Add comprehensive Rust and TypeScript tests (64 + 275 passing) - Update BooksTable to display actual book covers - Export Identifier type from calibre_db crate - Fix test utilities to include required BookCoverProvider wrapper Change-Id: 8321e2867e1dc3ef8aaff22bb585728c Change-Id-Short: rwxylxrtslym --- flake.nix | 2 +- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 3 + src-tauri/calibre_db/src/lib.rs | 2 +- src-tauri/src/lib.rs | 1 + src-tauri/src/library/base.rs | 4 +- src-tauri/src/library/calibre.rs | 69 +++++++- src-tauri/src/library/commands.rs | 83 ++++++++- src-tauri/src/library/mod.rs | 2 + src-tauri/src/library/models.rs | 165 ++++++++++++++++++ src/__tests__/components/BooksTable.test.tsx | 14 +- ...useLibrary.test.ts => useLibrary.test.tsx} | 58 +++--- src/api/library.ts | 25 ++- src/components/BookCover.tsx | 87 +++++++++ src/components/BookCoverSkeleton.tsx | 31 ---- src/components/BooksTable.tsx | 4 +- src/components/PageWrapperFactory.tsx | 27 +-- src/components/__tests__/BookCover.test.tsx | 69 ++++++++ .../__tests__/BooksTable.menu.test.tsx | 24 +-- src/contexts/BookCoverContext.tsx | 35 ++++ src/hooks/useBookCover.ts | 57 ++++++ src/hooks/useLibrary.ts | 8 + src/hooks/useStreamBookCovers.ts | 108 ++++++++++++ src/pages/_app.tsx | 2 +- src/test-utils/testUtils.tsx | 5 +- src/types/book.ts | 9 - 26 files changed, 766 insertions(+), 129 deletions(-) create mode 100644 src-tauri/src/library/models.rs rename src/__tests__/hooks/{useLibrary.test.ts => useLibrary.test.tsx} (88%) create mode 100644 src/components/BookCover.tsx delete mode 100644 src/components/BookCoverSkeleton.tsx create mode 100644 src/components/__tests__/BookCover.test.tsx create mode 100644 src/contexts/BookCoverContext.tsx create mode 100644 src/hooks/useBookCover.ts create mode 100644 src/hooks/useStreamBookCovers.ts diff --git a/flake.nix b/flake.nix index f2a2cc9..d7b6f04 100644 --- a/flake.nix +++ b/flake.nix @@ -29,7 +29,7 @@ pnpmDeps = pkgs.pnpm.fetchDeps { inherit (finalAttrs) pname version src; fetcherVersion = 2; - hash = "sha256-lEfzQqomcIJex4XbTCzjUj/yQaMKdv8SvkdpUXT5fpI="; + hash = "sha256-3vi81v4JXNuSXfrAb1DeS2HwoAbLFcp79wEn9NRpcFM="; }; nativeBuildInputs = [ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e827a7e..04d9f6b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "once_cell", "quick-xml 0.31.0", "rayon", + "rusqlite", "serde", "serde-xml-rs", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3d14b8f..cc6e587 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,6 +37,9 @@ tokio = { version = "1.35", features = ["full"] } async-trait = "0.1" calibre_db = { path = "./calibre_db" } +[dev-dependencies] +rusqlite = { version = "0.31", features = ["bundled"] } + [[test]] name = "app_tests" path = "tests/lib_tests.rs" diff --git a/src-tauri/calibre_db/src/lib.rs b/src-tauri/calibre_db/src/lib.rs index 8c90613..2b9a496 100644 --- a/src-tauri/calibre_db/src/lib.rs +++ b/src-tauri/calibre_db/src/lib.rs @@ -4,7 +4,7 @@ mod schema; mod queries; pub use error::{CalibreDbError, Result}; -pub use models::{Book, Author, Series, Tag, BookMetadata}; +pub use models::{Book, Author, Series, Tag, Identifier, BookMetadata}; pub use schema::DatabaseConnection; use std::path::Path; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 55a46f6..494f9a7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -28,6 +28,7 @@ pub fn run() { library::commands::library_get_all_books, library::commands::library_get_book, library::commands::library_get_book_count, + library::commands::library_stream_book_covers, ]) .setup(|app| { if cfg!(debug_assertions) { diff --git a/src-tauri/src/library/base.rs b/src-tauri/src/library/base.rs index 40f7efe..6c9b885 100644 --- a/src-tauri/src/library/base.rs +++ b/src-tauri/src/library/base.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; -use calibre_db::Book; use crate::error::AppError; +use crate::library::models::Book; #[async_trait] pub trait BookLibrary: Send + Sync { @@ -11,5 +11,7 @@ pub trait BookLibrary: Send + Sync { async fn get_book_count(&self) -> Result; + async fn get_book_cover(&self, book_id: u32) -> Result, AppError>; + fn clone_box(&self) -> Box; } diff --git a/src-tauri/src/library/calibre.rs b/src-tauri/src/library/calibre.rs index 58fb903..e971436 100644 --- a/src-tauri/src/library/calibre.rs +++ b/src-tauri/src/library/calibre.rs @@ -1,14 +1,18 @@ use async_trait::async_trait; -use calibre_db::{Book, CalibreDatabase}; +use calibre_db::CalibreDatabase; use std::path::PathBuf; use std::sync::Arc; use super::base::BookLibrary; +use super::models::Book; use crate::error::AppError; +const COVER_FILE_NAME: &str = "cover.jpg"; + #[derive(Clone)] pub struct CalibreLibrary { db: Arc, + library_path: PathBuf, } impl CalibreLibrary { @@ -25,7 +29,14 @@ impl CalibreLibrary { let db = CalibreDatabase::open(metadata_db_path) .map_err(|e| AppError::LibraryError(e.to_string()))?; - Ok(CalibreLibrary { db: Arc::new(db) }) + Ok(CalibreLibrary { + db: Arc::new(db), + library_path, + }) + } + + fn get_cover_path(&self, book_path: &str) -> PathBuf { + self.library_path.join(book_path).join(COVER_FILE_NAME) } } @@ -39,15 +50,21 @@ unsafe impl Sync for CalibreLibrary {} #[async_trait] impl BookLibrary for CalibreLibrary { async fn get_all_books(&self) -> Result, AppError> { - self.db + let calibre_books = self + .db .all_books() - .map_err(|e| AppError::LibraryError(e.to_string())) + .map_err(|e| AppError::LibraryError(e.to_string()))?; + + Ok(calibre_books.into_iter().map(Book::from).collect()) } async fn get_book(&self, book_id: u32) -> Result { - self.db + let calibre_book = self + .db .get_book(book_id) - .map_err(|e| AppError::LibraryError(e.to_string())) + .map_err(|e| AppError::LibraryError(e.to_string()))?; + + Ok(Book::from(calibre_book)) } async fn get_book_count(&self) -> Result { @@ -57,6 +74,32 @@ impl BookLibrary for CalibreLibrary { .map_err(|e| AppError::LibraryError(e.to_string())) } + async fn get_book_cover(&self, book_id: u32) -> Result, AppError> { + let calibre_book = self + .db + .get_book(book_id) + .map_err(|e| AppError::LibraryError(e.to_string()))?; + + if !calibre_book.has_cover { + return Err(AppError::BookNotFound(format!( + "Book {} has no cover", + book_id + ))); + } + + let cover_path = self.get_cover_path(&calibre_book.path); + + if !cover_path.exists() { + return Err(AppError::IoError(format!( + "Cover file not found at: {}", + cover_path.display() + ))); + } + + std::fs::read(&cover_path) + .map_err(|e| AppError::IoError(format!("Failed to read cover: {}", e))) + } + fn clone_box(&self) -> Box { Box::new(self.clone()) } @@ -71,4 +114,18 @@ mod tests { let result = CalibreLibrary::new(PathBuf::from("/nonexistent/path")); assert!(result.is_err()); } + + #[test] + fn test_cover_path_construction() { + let library_path = PathBuf::from("/test/library"); + let book_path = "Author Name/Book Title"; + + let cover_path = library_path.join(book_path).join(COVER_FILE_NAME); + + let path_str = cover_path.to_string_lossy(); + + assert!(path_str.contains("cover.jpg")); + assert!(path_str.contains("Author Name")); + assert!(path_str.contains("Book Title")); + } } diff --git a/src-tauri/src/library/commands.rs b/src-tauri/src/library/commands.rs index a2070be..9de8f4d 100644 --- a/src-tauri/src/library/commands.rs +++ b/src-tauri/src/library/commands.rs @@ -1,10 +1,14 @@ use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; +use tauri::ipc::Channel; use tauri::State; -use calibre_db::Book; +use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; +use log::debug; +use serde::Serialize; +use super::models::Book; use super::{BookLibrary, CalibreLibrary}; pub struct LibraryState { @@ -117,3 +121,80 @@ pub async fn library_get_book_count(state: State<'_, LibraryState>) -> Result, + state: State<'_, LibraryState>, + on_event: Channel, +) -> Result<(), String> { + let library = { + let library_guard = state + .library + .lock() + .map_err(|e| format!("Failed to acquire lock: {}", e))?; + + library_guard + .as_ref() + .ok_or_else(|| "No library opened. Call library_open first.".to_string())? + .clone_box() + }; + + tauri::async_runtime::spawn(async move { + let total = book_ids.len(); + + if let Err(e) = on_event.send(CoverStreamEvent::Started { total_books: total }) { + debug!("Failed to send Started event: {}", e); + return; + } + + for book_id in book_ids { + match library.get_book_cover(book_id).await { + Ok(cover_data) => { + let data_base64 = BASE64_STANDARD.encode(&cover_data); + + if let Err(e) = on_event.send(CoverStreamEvent::Cover { + book_id, + data_base64, + }) { + debug!("Failed to send Cover event for book {}: {}", book_id, e); + } + } + Err(e) => { + if let Err(send_err) = on_event.send(CoverStreamEvent::Error { + book_id, + message: e.to_string(), + }) { + debug!( + "Failed to send Error event for book {}: {}", + book_id, send_err + ); + } + } + } + } + + if let Err(e) = on_event.send(CoverStreamEvent::Finished) { + debug!("Failed to send Finished event: {}", e); + } + }); + + Ok(()) +} diff --git a/src-tauri/src/library/mod.rs b/src-tauri/src/library/mod.rs index 1b41e0d..0834006 100644 --- a/src-tauri/src/library/mod.rs +++ b/src-tauri/src/library/mod.rs @@ -1,6 +1,8 @@ pub mod base; pub mod calibre; pub mod commands; +pub mod models; pub use base::BookLibrary; pub use calibre::CalibreLibrary; +pub use models::Book; diff --git a/src-tauri/src/library/models.rs b/src-tauri/src/library/models.rs new file mode 100644 index 0000000..861554d --- /dev/null +++ b/src-tauri/src/library/models.rs @@ -0,0 +1,165 @@ +use serde::{Deserialize, Serialize}; + +/// Library-agnostic Book structure that can be returned to the frontend. +/// Only includes fields that are actually used by the frontend. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Book { + pub id: u32, + pub title: String, + pub pubdate: String, + pub isbn: String, + pub authors: Vec, + pub publishers: Vec, + pub tags: Vec, + pub series: Option, + pub rating: Option, + pub formats: Vec, + pub languages: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Author { + pub id: u32, + pub name: String, + pub sort: String, + pub link: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Series { + pub id: u32, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Tag { + pub id: u32, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Identifier { + pub book_id: u32, + pub kind: String, + pub val: String, +} + +impl From for Book { + fn from(book: calibre_db::Book) -> Self { + Book { + id: book.id, + title: book.title, + pubdate: book.pubdate.to_rfc3339(), + isbn: book.isbn, + authors: book.authors.into_iter().map(Author::from).collect(), + publishers: book.publishers, + tags: book.tags.into_iter().map(Tag::from).collect(), + series: book.series.map(Series::from), + rating: book.rating, + formats: book.formats, + languages: book.languages, + } + } +} + +impl From for Author { + fn from(author: calibre_db::Author) -> Self { + Author { + id: author.id, + name: author.name, + sort: author.sort, + link: author.link, + } + } +} + +impl From for Series { + fn from(series: calibre_db::Series) -> Self { + Series { + id: series.id, + name: series.name, + } + } +} + +impl From for Tag { + fn from(tag: calibre_db::Tag) -> Self { + Tag { + id: tag.id, + name: tag.name, + } + } +} + +impl From for Identifier { + fn from(identifier: calibre_db::Identifier) -> Self { + Identifier { + book_id: identifier.book_id, + kind: identifier.kind, + val: identifier.val, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_author_conversion() { + let calibre_author = calibre_db::Author { + id: 1, + name: "Test Author".to_string(), + sort: "Author, Test".to_string(), + link: Some("http://example.com".to_string()), + }; + + let agnostic_author: Author = calibre_author.into(); + + assert_eq!(agnostic_author.id, 1); + assert_eq!(agnostic_author.name, "Test Author"); + assert_eq!(agnostic_author.sort, "Author, Test"); + assert_eq!(agnostic_author.link, Some("http://example.com".to_string())); + } + + #[test] + fn test_series_conversion() { + let calibre_series = calibre_db::Series { + id: 5, + name: "Test Series".to_string(), + }; + + let agnostic_series: Series = calibre_series.into(); + + assert_eq!(agnostic_series.id, 5); + assert_eq!(agnostic_series.name, "Test Series"); + } + + #[test] + fn test_tag_conversion() { + let calibre_tag = calibre_db::Tag { + id: 3, + name: "Fiction".to_string(), + }; + + let agnostic_tag: Tag = calibre_tag.into(); + + assert_eq!(agnostic_tag.id, 3); + assert_eq!(agnostic_tag.name, "Fiction"); + } + + #[test] + fn test_identifier_conversion() { + let calibre_identifier = calibre_db::Identifier { + book_id: 10, + kind: "isbn".to_string(), + val: "1234567890".to_string(), + }; + + let agnostic_identifier: Identifier = calibre_identifier.into(); + + assert_eq!(agnostic_identifier.book_id, 10); + assert_eq!(agnostic_identifier.kind, "isbn"); + assert_eq!(agnostic_identifier.val, "1234567890"); + } +} diff --git a/src/__tests__/components/BooksTable.test.tsx b/src/__tests__/components/BooksTable.test.tsx index 0ca9ecb..a4f1ccd 100644 --- a/src/__tests__/components/BooksTable.test.tsx +++ b/src/__tests__/components/BooksTable.test.tsx @@ -8,15 +8,8 @@ import { renderWithProviders } from "@/test-utils/testUtils"; const mockBook: Book = { id: 1, title: "Test Book", - sort: "book, test", - timestamp: "2024-01-01T00:00:00Z", pubdate: "2024-01-01T00:00:00Z", - series_index: 1.0, - author_sort: "Author, Test", isbn: "123-456-789", - lccn: "", - path: "/test/path", - has_cover: true, authors: [{ id: 1, name: "Test Author", sort: "Author, Test" }], publishers: ["Test Publisher"], tags: [ @@ -24,10 +17,8 @@ const mockBook: Book = { { id: 2, name: "Adventure" }, ], series: { id: 1, name: "Test Series" }, - comments: "Test comment", rating: 4, formats: ["EPUB", "PDF"], - identifiers: [{ book_id: 1, kind: "isbn", val: "123-456-789" }], languages: ["en", "es"], }; @@ -271,7 +262,7 @@ describe("BooksTable", () => { expect(screen.getByText(mockBookTagsFormatted)).toBeInTheDocument(); }); - it("renders cover skeleton for cover column", () => { + it("renders cover for cover column", () => { renderWithProviders( { />, ); - expect(screen.getByTestId("cover-skeleton-1")).toBeInTheDocument(); + // BookCover component should render (it will show a skeleton initially) + expect(screen.getByTestId("book-cover-skeleton")).toBeInTheDocument(); }); }); diff --git a/src/__tests__/hooks/useLibrary.test.ts b/src/__tests__/hooks/useLibrary.test.tsx similarity index 88% rename from src/__tests__/hooks/useLibrary.test.ts rename to src/__tests__/hooks/useLibrary.test.tsx index 75d3569..3a00262 100644 --- a/src/__tests__/hooks/useLibrary.test.ts +++ b/src/__tests__/hooks/useLibrary.test.tsx @@ -1,6 +1,8 @@ import { renderHook, act, waitFor } from "@testing-library/react"; import { useLibrary } from "@/hooks/useLibrary"; import * as libraryApi from "@/api/library"; +import React from "react"; +import { BookCoverProvider } from "@/contexts/BookCoverContext"; jest.mock("@/api/library"); @@ -8,49 +10,35 @@ const mockBooks = [ { id: 1, title: "Test Book 1", - sort: "book, test", - timestamp: "2024-01-01T00:00:00Z", pubdate: "2024-01-01T00:00:00Z", - series_index: 1.0, - author_sort: "Author, Test", isbn: "123-456", - lccn: "", - path: "/test/path1", - has_cover: true, authors: [{ id: 1, name: "Test Author", sort: "Author, Test" }], publishers: ["Test Publisher"], tags: [], series: null, - comments: null, rating: null, formats: [], - identifiers: [], languages: [], }, { id: 2, title: "Test Book 2", - sort: "book, test", - timestamp: "2024-01-02T00:00:00Z", pubdate: "2024-01-02T00:00:00Z", - series_index: 1.0, - author_sort: "Author, Test", isbn: "789-012", - lccn: "", - path: "/test/path2", - has_cover: false, authors: [], publishers: [], tags: [], series: null, - comments: null, rating: null, formats: [], - identifiers: [], languages: [], }, ]; +const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + describe("useLibrary", () => { beforeEach(() => { localStorage.clear(); @@ -65,7 +53,7 @@ describe("useLibrary", () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); expect(result.current.books).toEqual([]); expect(result.current.isLoading).toBe(false); @@ -78,7 +66,7 @@ describe("useLibrary", () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/test/path"); @@ -93,7 +81,7 @@ describe("useLibrary", () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/test/path"); @@ -110,7 +98,7 @@ describe("useLibrary", () => { ); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/invalid/path"); @@ -124,7 +112,7 @@ describe("useLibrary", () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/test/path"); @@ -144,7 +132,7 @@ describe("useLibrary", () => { new Promise((resolve) => setTimeout(() => resolve(mockBooks), 50)) ); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/test/path"); @@ -165,7 +153,7 @@ describe("useLibrary", () => { new Error(errorMessage) ); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/test/path"); @@ -187,7 +175,7 @@ describe("useLibrary", () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); // Clear the mock after rendering to account for any auto-load effects (libraryApi.libraryGetAllBooks as jest.Mock).mockClear(); @@ -206,7 +194,7 @@ describe("useLibrary", () => { (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); localStorage.setItem("kikou_library_path", "/saved/path"); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await waitFor(() => { expect(result.current.libraryPath).toBe("/saved/path"); @@ -219,7 +207,7 @@ describe("useLibrary", () => { .mockResolvedValueOnce(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/path1"); @@ -239,7 +227,7 @@ describe("useLibrary", () => { "String error message" ); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/test/path"); @@ -252,7 +240,7 @@ describe("useLibrary", () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/test/path"); @@ -277,7 +265,7 @@ describe("useLibrary", () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/test/path"); @@ -298,7 +286,7 @@ describe("useLibrary", () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); // Simulate opening with an explicit call (not auto-load) await act(async () => { @@ -326,7 +314,7 @@ describe("useLibrary", () => { // Set saved path before rendering hook localStorage.setItem("kikou_library_path", "/saved/path"); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); // Wait for path to be loaded from localStorage await waitFor(() => { @@ -354,7 +342,7 @@ describe("useLibrary", () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); await act(async () => { await result.current.openLibrary("/test/path"); @@ -379,7 +367,7 @@ describe("useLibrary", () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue(mockBooks); - const { result } = renderHook(() => useLibrary()); + const { result } = renderHook(() => useLibrary(), { wrapper }); expect(result.current.isLoading).toBe(false); diff --git a/src/api/library.ts b/src/api/library.ts index 7292d2f..8fad7b4 100644 --- a/src/api/library.ts +++ b/src/api/library.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { invoke, Channel } from "@tauri-apps/api/core"; import { Book } from "@/types/book"; export async function libraryOpen(uri: string): Promise { @@ -16,3 +16,26 @@ export async function libraryGetBook(bookId: number): Promise { export async function libraryGetBookCount(): Promise { return await invoke("library_get_book_count"); } + +export interface CoverStreamEvent { + event: "started" | "cover" | "error" | "finished"; + data: + | { total_books: number } + | { book_id: number; data_base64: string } + | { book_id: number; message: string } + | null; +} + +export async function streamBookCovers( + bookIds: number[], + onEvent: (event: CoverStreamEvent) => void, +): Promise { + const channel = new Channel(); + + channel.onmessage = onEvent; + + await invoke("library_stream_book_covers", { + bookIds, + onEvent: channel, + }); +} diff --git a/src/components/BookCover.tsx b/src/components/BookCover.tsx new file mode 100644 index 0000000..f0ae1ca --- /dev/null +++ b/src/components/BookCover.tsx @@ -0,0 +1,87 @@ +import { Box, Skeleton, Typography } from "@mui/joy"; +import { useBookCover } from "@/hooks/useBookCover"; + +export interface BookCoverProps { + bookId: number | null; + width?: number | string; + height?: number | string; + alt?: string; +} + +export function BookCover({ + bookId, + width = 60, + height = 90, + alt = "Book cover", +}: BookCoverProps): React.ReactElement { + const { coverUrl, loading, error } = useBookCover(bookId); + + if (bookId === null || error || (!loading && !coverUrl)) { + return ( + + + No cover + + + ); + } + + if (loading) { + return ( + + + + ); + } + + return ( + + + + ); +} diff --git a/src/components/BookCoverSkeleton.tsx b/src/components/BookCoverSkeleton.tsx deleted file mode 100644 index 8747a24..0000000 --- a/src/components/BookCoverSkeleton.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Box, Skeleton } from "@mui/joy"; - -export interface BookCoverSkeletonProps { - width?: number | string; - height?: number | string; -} - -export function BookCoverSkeleton({ - width = 60, - height = 90, -}: BookCoverSkeletonProps) { - return ( - - - - ); -} diff --git a/src/components/BooksTable.tsx b/src/components/BooksTable.tsx index e0fb564..e661017 100644 --- a/src/components/BooksTable.tsx +++ b/src/components/BooksTable.tsx @@ -13,7 +13,7 @@ import { Divider, } from "@mui/joy"; import { Book } from "@/types/book"; -import { BookCoverSkeleton } from "./BookCoverSkeleton"; +import { BookCover } from "./BookCover"; import { ColumnConfig } from "@/hooks/useTableColumns"; import { devLog } from "@/utils/devLog"; @@ -311,7 +311,7 @@ export function BooksTable({ const renderCellContent = (book: Book, columnId: string): React.ReactNode => { if (columnId === "cover") { - return ; + return ; } const value = getBookFieldValue(book, columnId); diff --git a/src/components/PageWrapperFactory.tsx b/src/components/PageWrapperFactory.tsx index b008b5f..5095bbf 100644 --- a/src/components/PageWrapperFactory.tsx +++ b/src/components/PageWrapperFactory.tsx @@ -3,6 +3,7 @@ import { Box } from "@mui/joy"; import { NavigationProvider } from "@/contexts/NavigationContext"; import NavigationDialogManager from "@/components/ui/NavigationDialogManager"; import { ArchiveProvider } from "@/contexts/ArchiveContext"; +import { BookCoverProvider } from "@/contexts/BookCoverContext"; import ArchiveValidator from "@/components/file/ArchiveValidator"; import Navigation from "./ui/Navigation"; @@ -40,18 +41,20 @@ export function getPageWrapper({ return ( - - - - {wrappedContent} - + + + + + {wrappedContent} + + ); } diff --git a/src/components/__tests__/BookCover.test.tsx b/src/components/__tests__/BookCover.test.tsx new file mode 100644 index 0000000..a2e696a --- /dev/null +++ b/src/components/__tests__/BookCover.test.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { render, screen, act } from "@testing-library/react"; +import { BookCover } from "../BookCover"; +import { BookCoverProvider } from "@/contexts/BookCoverContext"; + +const MockedBookCover = ({ bookId, ...props }: React.ComponentProps) => { + return ( + + + + ); +}; + +describe("BookCover", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + act(() => { + jest.runOnlyPendingTimers(); + }); + jest.useRealTimers(); + }); + + it("should render skeleton when loading", () => { + render(); + + const skeleton = document.querySelector(".MuiSkeleton-root"); + + expect(skeleton).toBeInTheDocument(); + }); + + it("should render 'No cover' when bookId is null", async () => { + render(); + + await screen.findByText("No cover"); + + expect(screen.getByText("No cover")).toBeInTheDocument(); + }); + + it("should render image alt text correctly", () => { + const ref = React.createRef>(); + + ref.current = { 1: "data:image/jpeg;base64,test123" }; + + render(); + + act(() => { + jest.advanceTimersByTime(200); + }); + + const img = screen.queryByAltText("Test Book Cover"); + + if (img) { + expect(img).toBeInTheDocument(); + } + }); + + it("should handle width and height props", async () => { + render(); + + await screen.findByText("No cover"); + + const container = screen.getByText("No cover").parentElement; + + expect(container).toHaveStyle({ width: "100px", height: "150px" }); + }); +}); diff --git a/src/components/__tests__/BooksTable.menu.test.tsx b/src/components/__tests__/BooksTable.menu.test.tsx index 92d1493..14ab340 100644 --- a/src/components/__tests__/BooksTable.menu.test.tsx +++ b/src/components/__tests__/BooksTable.menu.test.tsx @@ -1,28 +1,20 @@ -import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { screen, fireEvent, waitFor } from "@testing-library/react"; import { BooksTable } from "../BooksTable"; import { Book } from "@/types/book"; +import { renderWithProviders } from "@/test-utils/testUtils"; const mockBooks: Book[] = [ { id: 1, title: "Test Book", - sort: "Book, Test", - timestamp: "2024-01-01T00:00:00Z", - pubdate: "2024-01-01", - series_index: 0, - author_sort: "Author, Test", + pubdate: "2024-01-01T00:00:00Z", + isbn: "1234567890", authors: [{ id: 1, name: "Test Author", sort: "Author, Test" }], publishers: ["Test Publisher"], tags: [{ id: 1, name: "Fiction" }], - isbn: "1234567890", - lccn: "", - path: "test/path", - has_cover: false, - comments: null, - rating: 5, series: null, + rating: 5, formats: ["EPUB"], - identifiers: [], languages: ["en"], }, ]; @@ -40,7 +32,7 @@ describe("BooksTable - Column Menu", () => { const mockToggle = jest.fn(); const mockReset = jest.fn(); - render( + renderWithProviders( { const mockToggle = jest.fn(); const mockReset = jest.fn(); - render( + renderWithProviders( { const mockToggle = jest.fn(); const mockReset = jest.fn(); - render( + renderWithProviders( >; +} + +const BookCoverContext = createContext(null); + +interface BookCoverProviderProps { + children: ReactNode; +} + +export function BookCoverProvider({ + children, +}: BookCoverProviderProps): React.ReactElement { + const coverCache = useRef>({}); + + return ( + + {children} + + ); +} + +export function useBookCoverContext(): BookCoverContextType { + const context = useContext(BookCoverContext); + + if (!context) { + throw new Error( + "useBookCoverContext must be used within BookCoverProvider", + ); + } + + return context; +} diff --git a/src/hooks/useBookCover.ts b/src/hooks/useBookCover.ts new file mode 100644 index 0000000..fa5aa67 --- /dev/null +++ b/src/hooks/useBookCover.ts @@ -0,0 +1,57 @@ +import { useState, useEffect } from "react"; +import { useBookCoverContext } from "@/contexts/BookCoverContext"; + +export interface UseBookCoverResult { + coverUrl: string | null; + loading: boolean; + error: string | null; +} + +export function useBookCover(bookId: number | null): UseBookCoverResult { + const { coverCache } = useBookCoverContext(); + const [coverUrl, setCoverUrl] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (bookId === null) { + setCoverUrl(null); + setLoading(false); + setError(null); + return; + } + + if (coverCache.current[bookId]) { + setCoverUrl(coverCache.current[bookId]); + setLoading(false); + setError(null); + return; + } + + setLoading(true); + setError(null); + + const checkInterval = setInterval(() => { + if (coverCache.current[bookId]) { + setCoverUrl(coverCache.current[bookId]); + setLoading(false); + clearInterval(checkInterval); + } + }, 100); + + const timeout = setTimeout(() => { + if (!coverCache.current[bookId]) { + setError("Cover not available"); + setLoading(false); + clearInterval(checkInterval); + } + }, 10000); + + return () => { + clearInterval(checkInterval); + clearTimeout(timeout); + }; + }, [bookId, coverCache]); + + return { coverUrl, loading, error }; +} diff --git a/src/hooks/useLibrary.ts b/src/hooks/useLibrary.ts index cc7c629..9d266ae 100644 --- a/src/hooks/useLibrary.ts +++ b/src/hooks/useLibrary.ts @@ -4,6 +4,7 @@ import { libraryOpen, libraryGetAllBooks, } from "@/api/library"; +import { useStreamBookCovers } from "./useStreamBookCovers"; const LIBRARY_PATH_KEY = "kikou_library_path"; @@ -26,6 +27,13 @@ export function useLibrary(): UseLibraryReturn { const autoLoadAttemptedRef = useRef(false); const isLibraryOpenRef = useRef(false); + const bookIds = books.map((book) => book.id); + + useStreamBookCovers({ + bookIds, + enabled: books.length > 0, + }); + // Keep ref in sync with state useEffect(() => { isLibraryOpenRef.current = isLibraryOpen; diff --git a/src/hooks/useStreamBookCovers.ts b/src/hooks/useStreamBookCovers.ts new file mode 100644 index 0000000..6adc1dc --- /dev/null +++ b/src/hooks/useStreamBookCovers.ts @@ -0,0 +1,108 @@ +import { useEffect, useCallback } from "react"; +import { useBookCoverContext } from "@/contexts/BookCoverContext"; +import { streamBookCovers, CoverStreamEvent } from "@/api/library"; +import { devLog } from "@/utils/devLog"; + +export interface UseStreamBookCoversOptions { + bookIds: number[]; + enabled?: boolean; + onProgress?: (loaded: number, total: number) => void; + onFinish?: () => void; + onStart?: () => void; +} + +function createDataUrl(base64Data: string): string { + return `data:image/jpeg;base64,${base64Data}`; +} + +export function useStreamBookCovers({ + bookIds, + enabled = true, + onProgress, + onFinish, + onStart, +}: UseStreamBookCoversOptions): { startStreaming: () => Promise } { + const { coverCache } = useBookCoverContext(); + + const startStreaming = useCallback(async (): Promise => { + if (!enabled || bookIds.length === 0) return; const uncachedIds = bookIds.filter( + (bookId) => !coverCache.current[bookId], + ); + + if (uncachedIds.length === 0) { + devLog("All book covers already cached"); + onFinish?.(); + + return; + } + + devLog(`Streaming ${uncachedIds.length} book covers`); + + let loaded = bookIds.length - uncachedIds.length; + const total = bookIds.length; + + try { + await streamBookCovers(uncachedIds, (event: CoverStreamEvent) => { + switch (event.event) { + case "started": + if ( + event.data && + typeof event.data === "object" && + "total_books" in event.data + ) { + devLog(`Started streaming ${event.data.total_books} covers`); + onStart?.(); + } + break; + + case "cover": { + if ( + event.data && + typeof event.data === "object" && + "book_id" in event.data && + "data_base64" in event.data + ) { + const { book_id, data_base64 } = event.data; + + coverCache.current[book_id] = createDataUrl(data_base64); + loaded++; + + devLog(`Cached cover for book ${book_id} (${loaded}/${total})`); + + onProgress?.(loaded, total); + } + break; + } + + case "error": + if ( + event.data && + typeof event.data === "object" && + "book_id" in event.data && + "message" in event.data + ) { + devLog( + `Error loading cover for book ${event.data.book_id}: ${event.data.message}`, + ); + loaded++; + onProgress?.(loaded, total); + } + break; + + case "finished": + devLog("Cover streaming finished"); + onFinish?.(); + break; + } + }); + } catch (error) { + devLog("Cover streaming error:", error); + } + }, [bookIds, enabled, coverCache, onProgress, onFinish, onStart]); + + useEffect(() => { + startStreaming(); + }, [startStreaming]); + + return { startStreaming }; +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index fa134ff..e81416b 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -19,7 +19,7 @@ function AppContent({ Component, pageProps, router }) { return getPageWrapper({ path, pathname: router.pathname, content }); } -export default function App(appProps: AppProps) { +export default function App(appProps: AppProps): React.ReactElement { return ( diff --git a/src/test-utils/testUtils.tsx b/src/test-utils/testUtils.tsx index b270a89..43b3bb8 100644 --- a/src/test-utils/testUtils.tsx +++ b/src/test-utils/testUtils.tsx @@ -27,6 +27,7 @@ import React from "react"; import { render } from "@testing-library/react"; import { CssVarsProvider } from "@mui/joy"; import { ThemeProvider } from "../contexts/ThemeContext"; +import { BookCoverProvider } from "../contexts/BookCoverContext"; // Mock Tauri APIs jest.mock("@tauri-apps/api/core", () => ({ @@ -47,7 +48,9 @@ jest.mock("@tauri-apps/api/app", () => ({ export function renderWithProviders(ui: React.ReactElement) { return render( - {ui} + + {ui} + , ); } diff --git a/src/types/book.ts b/src/types/book.ts index 6096d0c..555bcad 100644 --- a/src/types/book.ts +++ b/src/types/book.ts @@ -24,22 +24,13 @@ export interface Identifier { export interface Book { id: number; title: string; - sort: string; - timestamp: string; pubdate: string; - series_index: number; - author_sort: string; isbn: string; - lccn: string; - path: string; - has_cover: boolean; authors: Author[]; publishers: string[]; tags: Tag[]; series: Series | null; - comments: string | null; rating: number | null; formats: string[]; - identifiers: Identifier[]; languages: string[]; } From 0f0e880d8224d36f66565e64f3e788cd3b48d0c3 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:01:24 +0100 Subject: [PATCH 4/6] fix: resolve CI failures and implement read-only database safeguards (#62) * Initial plan * fix: address Rust linting violations - Add Send/Sync implementations for CalibreDatabase - Remove unused Schema validation methods - Fix clippy::useless_vec warning in tests - Apply cargo fmt formatting * fix: address frontend linting violations - Remove unused useRef import from BooksTable - Add required blank lines before return statements - Add blank lines after if statements per eslint rules * fix: resolve race condition in useLibrary test Prevent auto-load effect from triggering when manually opening a library. The auto-load effect should only trigger when loading a saved path from localStorage on mount, not when calling openLibrary() manually. This fixes the flaky test 'can manually load books after opening library' which was sometimes seeing 2 calls to libraryGetAllBooks instead of 1. * chore: final update Co-authored-by: OGKevin <17928966+OGKevin@users.noreply.github.com> * fix: clarify safety comment and fix lifetime elision warning - Update safety comment to clarify current read-only operations - Add note about reassessing if write operations are introduced - Fix mismatched_lifetime_syntaxes warning by using explicit named lifetime in DatabaseConnection::prepare method * refactor: improve BookBuilder with database-aware pattern Refactored the BookBuilder pattern to eliminate mid-construction state exposure and improve encapsulation. The builder now holds an optional database reference and uses boolean flags to control which relations to fetch during build(). - Made BookBuilder database-aware with optional &DatabaseConnection - Added 9 boolean flags to control relation fetching (authors, publishers, tags, series, comments, rating, formats, identifiers, languages) - Moved relation-fetching logic into build() method - Exposed fetch_book_*() functions as public query API - Added with_db() and fetch_all() builder methods - Updated get_book() and all_books() to use new pattern - Removed get_id() method that broke encapsulation - Added comprehensive doc tests with lifetime design explanation - All tests pass: 11 unit tests + 17 integration tests * refactor: implement ReadOnlyDatabase trait with compile-time safeguards - Add ReadOnlyDatabase trait to mark database types as read-only - Remove execute() method from DatabaseConnection to prevent write operations - Add compile-time query validation in debug builds to ensure only SELECT queries - Add comprehensive tests for read-only constraint validation - Update safety documentation with ReadOnlyDatabase marker trait - Update CI workflow to use --workspace flag for consistency Changes ensure type-safe read-only guarantee at compile time while maintaining runtime validation in debug builds. Write operations now require explicit removal of ReadOnlyDatabase implementation. * refactor: enhance CalibreDbError handling and implement ReadOnlyDatabase trait for improved database operations * refactor: enhance build method documentation with performance trade-offs and query strategy rationale * refactor: enhance BookBuilder to require a database reference for improved relation fetching * refactor: remove redundant read-only query validation The read-only validation in DatabaseConnection was unnecessary defensive programming. Since only the ReadOnlyDatabase trait is exposed to users (with SELECT-only methods), and DatabaseConnection is an internal implementation detail, the type system already enforces read-only semantics at the public API level. - Removed is_read_only_query() function and helpers - Removed comment/string literal stripping logic - Simplified prepare() method - Removed validation-related tests - Kept essential connection and query tests * refactor: improve code readability and address review feedback - Extract long tuple type into BookRow struct with named fields - Add named constants for SQL column indices (IDX_ID, IDX_TITLE, etc.) - Remove redundant list_books() method from ReadOnlyDatabase trait - Update safety comment to suggest ReadWriteDatabase trait for future writes - Replace Into::into with explicit CalibreDbError::from for clarity - Simplify all_books() by using BookRow::from_row helper These changes improve code maintainability and make the codebase easier to understand. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: OGKevin <17928966+OGKevin@users.noreply.github.com> --- .github/copilot-instructions.md | 18 +- .../instructions/calibre_db.instructions.md | 19 +- .github/workflows/cargo.yml | 12 +- .gitignore | 1 + Cargo.lock | 6079 +++++++++++++++++ Cargo.toml | 3 + flake.nix | 2 +- src-tauri/calibre_db/src/error.rs | 26 +- src-tauri/calibre_db/src/lib.rs | 306 +- src-tauri/calibre_db/src/models.rs | 411 +- src-tauri/calibre_db/src/queries.rs | 457 -- src-tauri/calibre_db/src/schema.rs | 66 +- .../calibre_db/tests/integration_tests.rs | 2 +- src-tauri/src/library/calibre.rs | 2 +- src-tauri/src/library/commands.rs | 14 +- src-tauri/tests/lib_tests.rs | 2 +- src/components/BooksTable.tsx | 30 +- src/hooks/useLibrary.ts | 8 +- src/hooks/useStreamBookCovers.ts | 183 +- 19 files changed, 6896 insertions(+), 745 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 src-tauri/calibre_db/src/queries.rs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 654cd2a..1810800 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -20,13 +20,15 @@ When changing rust files, make sure to run `cargo fmt` to format the code and Next to this, ensure the code compiles and passes all tests by running: -- `cargo check` -- `cargo test` +- `cargo check --workspace` (or `cargo check` from workspace root) +- `cargo test --workspace` (or `cargo test` from workspace root) Or use: -- `devenv shell -- cargo check` -- `devenv shell -- cargo test` +- `devenv shell -- cargo check --workspace` +- `devenv shell -- cargo test --workspace` + +**Note:** The project uses a Rust workspace with members `src-tauri` and `src-tauri/calibre_db`. Run commands from the workspace root (`/workspaces/kikou`) to check and test all crates together, or use `--workspace` flag to ensure both crates are included. ## Frontend/Typescript Files @@ -60,9 +62,13 @@ Tests should be added first before the actual implementation. Rust tests can be executed via: -`cargo test --manifest-path=src-tauri/Cargo.toml` +`cargo test --workspace` Or use: -`devenv shell -- cargo test --manifest-path=src-tauri/Cargo.toml` +`devenv shell -- cargo test --workspace` + +To run tests for a specific crate: +- `cargo test -p calibre_db` (calibre_db crate only) +- `cargo test --manifest-path=src-tauri/Cargo.toml` (src-tauri crate only) ## Code Style diff --git a/.github/instructions/calibre_db.instructions.md b/.github/instructions/calibre_db.instructions.md index 57ed62e..61a1534 100644 --- a/.github/instructions/calibre_db.instructions.md +++ b/.github/instructions/calibre_db.instructions.md @@ -57,12 +57,19 @@ Language codes are stored as references (foreign keys) to the `languages` table, ## Build & Test ``` -cargo check # Verify compilation -cargo fmt # Format code -cargo clippy # Lint -cargo test # Run all tests -cargo test --lib # Unit tests only -cargo test --test integration_tests # Integration tests +cargo check -p calibre_db # Verify compilation +cargo fmt # Format code (or cargo fmt -p calibre_db) +cargo clippy -p calibre_db # Lint (or run from calibre_db directory) +cargo test -p calibre_db # Run all tests +cargo test -p calibre_db --lib # Unit tests only +cargo test -p calibre_db --test integration_tests # Integration tests +``` + +Alternatively, run from the workspace root or use `--workspace` to check all crates: +``` +cargo check --workspace +cargo test --workspace +cargo clippy --workspace ``` ## Troubleshooting diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index dc44b55..5bd928f 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -28,6 +28,8 @@ jobs: src-tauri/**/Cargo.toml src-tauri/**/Cargo.lock .github/workflows/cargo.yml + Cargo.toml + Cargo.lock check: needs: detect-changes @@ -62,7 +64,7 @@ jobs: Start-Process -FilePath "MicrosoftEdgeWebview2Setup.exe" -ArgumentList "/silent", "/install" -Wait shell: powershell - name: Run cargo check - run: cargo check --manifest-path=src-tauri/Cargo.toml + run: cargo check --workspace test: needs: detect-changes @@ -102,9 +104,9 @@ jobs: - name: Run cargo test run: | if [ "${{ matrix.platform }}" = "windows-latest" ]; then - cargo test --manifest-path=src-tauri/Cargo.toml --lib + cargo test --lib else - cargo test --manifest-path=src-tauri/Cargo.toml + cargo test fi shell: bash env: @@ -144,7 +146,7 @@ jobs: Start-Process -FilePath "MicrosoftEdgeWebview2Setup.exe" -ArgumentList "/silent", "/install" -Wait shell: powershell - name: Run cargo build - run: cargo build --release --manifest-path=src-tauri/Cargo.toml + run: cargo build --release clippy: needs: detect-changes @@ -169,4 +171,4 @@ jobs: sudo apt-get update sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsoup-3.0-dev libjavascriptcoregtk-4.1-dev - name: Run cargo clippy - run: cargo clippy --manifest-path=src-tauri/Cargo.toml --all-targets --all-features -- -D warnings + run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/.gitignore b/.gitignore index 143f446..0f3fe85 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ out tmp coverage result +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6400891 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6079 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Kikou" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "calibre_db", + "log", + "notify", + "once_cell", + "quick-xml 0.31.0", + "rayon", + "rusqlite", + "serde", + "serde-xml-rs", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-log", + "tempfile", + "tokio", + "zip", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.3", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byte-unit" +version = "5.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "calibre_db" +version = "0.1.0" +dependencies = [ + "chrono", + "futures", + "rusqlite", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.8", +] + +[[package]] +name = "cc" +version = "1.2.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.109", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.109", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.9", +] + +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.8", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.10.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.10.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.12.0", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +dependencies = [ + "value-bag", +] + +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.10.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.12.0", + "quick-xml 0.38.3", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.10.0", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.109", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde-xml-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53630160a98edebde0123eb4dfd0fce6adff091b2305db3154a9e920206eb510" +dependencies = [ + "log", + "serde", + "thiserror 1.0.69", + "xml-rs", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.0", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases", + "core-graphics", + "foreign-types", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.17", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a924b6c50fe83193f0f8b14072afa7c25b7a72752a2a73d9549b463f5fe91a38" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.109", + "tauri-utils", + "thiserror 2.0.17", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "260c5d2eb036b76206b9fca20b7be3614cfd21046c5396f7959e0e64a4b07f2f" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.109", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "toml 0.9.8", + "url", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5709c792b8630290b5d9811a1f8fe983dd925fc87c7fc7f4923616458cd00b6" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "tauri-runtime" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2 0.6.3", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.17", + "toml 0.9.8", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +dependencies = [ + "embed-resource", + "toml 0.9.8", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.12.0", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow 0.7.13", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.12.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.109", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.10.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +dependencies = [ + "thiserror 2.0.17", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +dependencies = [ + "base64 0.22.1", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.109", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap 2.12.0", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.109", + "winnow 0.7.13", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4ec4b5b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["src-tauri", "src-tauri/calibre_db"] diff --git a/flake.nix b/flake.nix index d7b6f04..05ffec4 100644 --- a/flake.nix +++ b/flake.nix @@ -24,7 +24,7 @@ src = ./.; - cargoHash = "sha256-m7RauKqcxHWonfhTiEEfA8J0rdZ7TO8IRmcHPWgdMRA="; + cargoHash = "sha256-cp7EFD1RI/qTkmpqritTvjsUCQq8MVWCVYXQwoNNj6g="; pnpmDeps = pkgs.pnpm.fetchDeps { inherit (finalAttrs) pname version src; diff --git a/src-tauri/calibre_db/src/error.rs b/src-tauri/calibre_db/src/error.rs index 908f602..4574b75 100644 --- a/src-tauri/calibre_db/src/error.rs +++ b/src-tauri/calibre_db/src/error.rs @@ -1,9 +1,8 @@ use std::fmt; -use rusqlite; #[derive(Debug)] pub enum CalibreDbError { - DatabaseError(String), + DatabaseError(rusqlite::Error), NotFound(String), InvalidData(String), IoError(String), @@ -13,7 +12,7 @@ pub enum CalibreDbError { impl fmt::Display for CalibreDbError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - CalibreDbError::DatabaseError(msg) => write!(f, "Database error: {}", msg), + CalibreDbError::DatabaseError(err) => write!(f, "Database error: {}", err), CalibreDbError::NotFound(msg) => write!(f, "Not found: {}", msg), CalibreDbError::InvalidData(msg) => write!(f, "Invalid data: {}", msg), CalibreDbError::IoError(msg) => write!(f, "IO error: {}", msg), @@ -26,7 +25,7 @@ impl std::error::Error for CalibreDbError {} impl From for CalibreDbError { fn from(err: rusqlite::Error) -> Self { - CalibreDbError::DatabaseError(err.to_string()) + CalibreDbError::DatabaseError(err) } } @@ -42,4 +41,21 @@ impl From for CalibreDbError { } } -pub type Result = std::result::Result; \ No newline at end of file +pub type Result = std::result::Result; + +/// Extension trait to convert `QueryReturnedNoRows` errors into `Ok(None)`. +/// This is useful for queries that may or may not return a row. +pub trait OptionalResult { + /// Converts a database query result to an `Option`, treating `QueryReturnedNoRows` as `None`. + fn optional(self) -> Result>; +} + +impl OptionalResult for Result { + fn optional(self) -> Result> { + match self { + Ok(value) => Ok(Some(value)), + Err(CalibreDbError::DatabaseError(rusqlite::Error::QueryReturnedNoRows)) => Ok(None), + Err(e) => Err(e), + } + } +} diff --git a/src-tauri/calibre_db/src/lib.rs b/src-tauri/calibre_db/src/lib.rs index 2b9a496..0bbaf9a 100644 --- a/src-tauri/calibre_db/src/lib.rs +++ b/src-tauri/calibre_db/src/lib.rs @@ -1,38 +1,310 @@ mod error; mod models; mod schema; -mod queries; -pub use error::{CalibreDbError, Result}; -pub use models::{Book, Author, Series, Tag, Identifier, BookMetadata}; +pub use error::{CalibreDbError, OptionalResult, Result}; +pub use models::{Author, Book, BookMetadata, Identifier, Series, Tag}; pub use schema::DatabaseConnection; +use chrono::{DateTime, Utc}; +use rusqlite::params; use std::path::Path; +/// Struct representing a row from the books table. +/// Used to extract data from SQL queries before building Book objects. +struct BookRow { + id: u32, + title: String, + sort: String, + timestamp_str: String, + pubdate_str: String, + series_index: f32, + author_sort: String, + isbn: String, + lccn: String, + path: String, + has_cover: i32, +} + +impl BookRow { + // Field indices for books table SELECT query + const IDX_ID: usize = 0; + const IDX_TITLE: usize = 1; + const IDX_SORT: usize = 2; + const IDX_TIMESTAMP: usize = 3; + const IDX_PUBDATE: usize = 4; + const IDX_SERIES_INDEX: usize = 5; + const IDX_AUTHOR_SORT: usize = 6; + const IDX_ISBN: usize = 7; + const IDX_LCCN: usize = 8; + const IDX_PATH: usize = 9; + const IDX_HAS_COVER: usize = 10; + + /// Extracts a BookRow from a rusqlite Row. + fn from_row(row: &rusqlite::Row) -> rusqlite::Result { + Ok(BookRow { + id: row.get(Self::IDX_ID)?, + title: row.get(Self::IDX_TITLE)?, + sort: row.get(Self::IDX_SORT)?, + timestamp_str: row.get(Self::IDX_TIMESTAMP)?, + pubdate_str: row.get(Self::IDX_PUBDATE)?, + series_index: row.get(Self::IDX_SERIES_INDEX)?, + author_sort: row.get(Self::IDX_AUTHOR_SORT)?, + isbn: row.get(Self::IDX_ISBN)?, + lccn: row.get(Self::IDX_LCCN)?, + path: row.get(Self::IDX_PATH)?, + has_cover: row.get(Self::IDX_HAS_COVER)?, + }) + } +} + +/// Trait defining all read-only database operations for Calibre libraries. +/// +/// This trait provides a compile-time guarantee that implementing types +/// only perform SELECT queries. +/// +/// # Safety +/// Types implementing this trait must ensure that: +/// - All database operations are SELECT statements only +/// - No INSERT, UPDATE, DELETE, or other write operations are performed +/// - The underlying connection is thread-safe (SQLite with proper locking) +pub trait ReadOnlyDatabase: Send + Sync { + fn get_book(&self, book_id: u32) -> Result; + fn all_books(&self) -> Result>; + fn fetch_book_authors(&self, book_id: u32) -> Result>; + fn fetch_book_publishers(&self, book_id: u32) -> Result>; + fn fetch_book_tags(&self, book_id: u32) -> Result>; + fn fetch_book_series(&self, book_id: u32) -> Result>; + fn fetch_book_comments(&self, book_id: u32) -> Result>; + fn fetch_book_rating(&self, book_id: u32) -> Result>; + fn fetch_book_formats(&self, book_id: u32) -> Result>; + fn fetch_book_identifiers(&self, book_id: u32) -> Result>; + fn fetch_book_languages(&self, book_id: u32) -> Result>; +} + pub struct CalibreDatabase { conn: DatabaseConnection, } -impl CalibreDatabase { - pub fn open>(path: P) -> Result { - let conn = DatabaseConnection::new(&path)?; - Ok(CalibreDatabase { conn }) +impl ReadOnlyDatabase for CalibreDatabase { + fn get_book(&self, book_id: u32) -> Result { + let book_row = self.conn.query_row( + "SELECT id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path, has_cover + FROM books WHERE id = ?1", + params![book_id], + BookRow::from_row, + )?; + + Book::builder(book_row.id, book_row.title, book_row.path, self) + .sort(book_row.sort) + .timestamp(parse_timestamp(&book_row.timestamp_str)) + .pubdate(parse_timestamp(&book_row.pubdate_str)) + .series_index(book_row.series_index) + .author_sort(book_row.author_sort) + .isbn(book_row.isbn) + .lccn(book_row.lccn) + .has_cover(book_row.has_cover != 0) + .fetch_all() + .build() } - pub fn connection(&self) -> &DatabaseConnection { - &self.conn + fn all_books(&self) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path, has_cover + FROM books ORDER BY sort ASC")?; + + let books = stmt + .query_map([], BookRow::from_row) + .map_err(CalibreDbError::from)? + .collect::, _>>() + .map_err(CalibreDbError::from)?; + + let enriched_books = books + .into_iter() + .map(|book_row| { + Book::builder(book_row.id, book_row.title, book_row.path, self) + .sort(book_row.sort) + .timestamp(parse_timestamp(&book_row.timestamp_str)) + .pubdate(parse_timestamp(&book_row.pubdate_str)) + .series_index(book_row.series_index) + .author_sort(book_row.author_sort) + .isbn(book_row.isbn) + .lccn(book_row.lccn) + .has_cover(book_row.has_cover != 0) + .fetch_all() + .build() + }) + .collect::>>()?; + + Ok(enriched_books) + } + + fn fetch_book_authors(&self, book_id: u32) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT a.id, a.name, a.sort + FROM authors a + JOIN books_authors_link bal ON a.id = bal.author + WHERE bal.book = ?1 + ORDER BY bal.id ASC", + )?; + + let authors = stmt + .query_map(params![book_id], |row| { + Ok(Author::new(row.get(0)?, row.get(1)?, row.get(2)?)) + }) + .map_err(CalibreDbError::from)? + .collect::, _>>() + .map_err(CalibreDbError::from)?; + + Ok(authors) } - pub fn get_book(&self, book_id: u32) -> Result { - queries::get_book(&self.conn, book_id) + fn fetch_book_publishers(&self, book_id: u32) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT p.name + FROM publishers p + JOIN books_publishers_link bpl ON p.id = bpl.publisher + WHERE bpl.book = ?1", + )?; + + let publishers = stmt + .query_map(params![book_id], |row| row.get(0)) + .map_err(CalibreDbError::from)? + .collect::, _>>() + .map_err(CalibreDbError::from)?; + + Ok(publishers) } - pub fn all_books(&self) -> Result> { - queries::all_books(&self.conn) + fn fetch_book_tags(&self, book_id: u32) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT t.id, t.name + FROM tags t + JOIN books_tags_link btl ON t.id = btl.tag + WHERE btl.book = ?1", + )?; + + let tags = stmt + .query_map(params![book_id], |row| { + Ok(Tag::new(row.get(0)?, row.get(1)?)) + }) + .map_err(CalibreDbError::from)? + .collect::, _>>() + .map_err(CalibreDbError::from)?; + + Ok(tags) } - pub fn books_iter(&self) -> Result> { - queries::all_books(&self.conn) + fn fetch_book_series(&self, book_id: u32) -> Result> { + self.conn + .query_row( + "SELECT s.id, s.name + FROM series s + JOIN books_series_link bsl ON s.id = bsl.series + WHERE bsl.book = ?1", + params![book_id], + |row| Ok(Series::new(row.get(0)?, row.get(1)?)), + ) + .optional() + } + + fn fetch_book_comments(&self, book_id: u32) -> Result> { + self.conn + .query_row( + "SELECT text FROM comments WHERE book = ?1", + params![book_id], + |row| row.get(0), + ) + .optional() + } + + fn fetch_book_rating(&self, book_id: u32) -> Result> { + self.conn + .query_row( + "SELECT r.rating + FROM ratings r + JOIN books_ratings_link brl ON r.id = brl.rating + WHERE brl.book = ?1", + params![book_id], + |row| { + let rating: u32 = row.get(0)?; + Ok((rating / 2) as u8) + }, + ) + .optional() + } + + fn fetch_book_formats(&self, book_id: u32) -> Result> { + let mut stmt = self + .conn + .prepare("SELECT format FROM data WHERE book = ?1 ORDER BY id ASC")?; + + let formats = stmt + .query_map(params![book_id], |row| row.get(0)) + .map_err(CalibreDbError::from)? + .collect::, _>>() + .map_err(CalibreDbError::from)?; + + Ok(formats) + } + + fn fetch_book_identifiers(&self, book_id: u32) -> Result> { + let mut stmt = self + .conn + .prepare("SELECT book, type, val FROM identifiers WHERE book = ?1")?; + + let identifiers = stmt + .query_map(params![book_id], |row| { + Ok(Identifier::new(row.get(0)?, row.get(1)?, row.get(2)?)) + }) + .map_err(CalibreDbError::from)? + .collect::, _>>() + .map_err(CalibreDbError::from)?; + + Ok(identifiers) + } + + fn fetch_book_languages(&self, book_id: u32) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT l.lang_code + FROM languages l + JOIN books_languages_link bll ON l.id = bll.lang_code + WHERE bll.book = ?1 + ORDER BY bll.item_order ASC", + )?; + + let languages = stmt + .query_map(params![book_id], |row| row.get(0)) + .map_err(CalibreDbError::from)? + .collect::, _>>() + .map_err(CalibreDbError::from)?; + + Ok(languages) + } +} + +// SAFETY: CalibreDatabase implements ReadOnlyDatabase trait, which by design enforces +// read-only operations through its API. All methods perform only SELECT queries. +// rusqlite::Connection is thread-safe and uses SQLite's built-in locking mechanisms. +// If write operations are needed, introduce a separate ReadWriteDatabase trait. +unsafe impl Send for CalibreDatabase {} +unsafe impl Sync for CalibreDatabase {} + +fn parse_timestamp(timestamp_str: &str) -> DateTime { + DateTime::parse_from_rfc3339(timestamp_str) + .ok() + .map(|dt| dt.with_timezone(&Utc)) + .unwrap_or(DateTime::::UNIX_EPOCH) +} + +impl CalibreDatabase { + pub fn open>(path: P) -> Result { + let conn = DatabaseConnection::new(&path)?; + Ok(CalibreDatabase { conn }) + } + + pub fn connection(&self) -> &DatabaseConnection { + &self.conn } } @@ -42,7 +314,7 @@ mod tests { #[test] fn test_error_creation() { - let err = CalibreDbError::DatabaseError("test".to_string()); + let err = CalibreDbError::DatabaseError(rusqlite::Error::QueryReturnedNoRows); assert!(matches!(err, CalibreDbError::DatabaseError(_))); } -} \ No newline at end of file +} diff --git a/src-tauri/calibre_db/src/models.rs b/src-tauri/calibre_db/src/models.rs index 483cc75..027cbd6 100644 --- a/src-tauri/calibre_db/src/models.rs +++ b/src-tauri/calibre_db/src/models.rs @@ -1,5 +1,6 @@ -use serde::{Deserialize, Serialize}; +use crate::ReadOnlyDatabase; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Book { @@ -26,31 +27,35 @@ pub struct Book { } impl Book { - pub fn new( - id: u32, - title: String, - sort: String, - timestamp: DateTime, - pubdate: DateTime, - series_index: f32, - author_sort: String, - isbn: String, - lccn: String, - path: String, - has_cover: bool, - ) -> Self { - Book { + /// Creates a new book builder with the given ID, title, path, and database reference. + /// + /// The database is required to enable automatic relation fetching during `build()`. + /// The builder's lifetime is tied to the database reference, ensuring type-safe access. + /// + /// # Examples + /// + /// ```ignore + /// use calibre_db::Book; + /// let book = Book::builder(1, "Title".into(), "/path".into(), &db) + /// .sort("book, title".into()) + /// .fetch_all() + /// .build()?; + /// assert_eq!(book.id, 1); + /// assert_eq!(book.title, "Title"); + /// ``` + pub fn builder(id: u32, title: String, path: String, db: &dyn ReadOnlyDatabase) -> BookBuilder { + BookBuilder { id, title, - sort, - timestamp, - pubdate, - series_index, - author_sort, - isbn, - lccn, path, - has_cover, + sort: String::new(), + timestamp: Utc::now(), + pubdate: Utc::now(), + series_index: 0.0, + author_sort: String::new(), + isbn: String::new(), + lccn: String::new(), + has_cover: false, authors: Vec::new(), publishers: Vec::new(), tags: Vec::new(), @@ -60,53 +65,268 @@ impl Book { formats: Vec::new(), identifiers: Vec::new(), languages: Vec::new(), + fetch_authors: false, + fetch_publishers: false, + fetch_tags: false, + fetch_series: false, + fetch_comments: false, + fetch_rating: false, + fetch_formats: false, + fetch_identifiers: false, + fetch_languages: false, + db, } } +} - pub fn with_authors(mut self, authors: Vec) -> Self { +pub struct BookBuilder<'a> { + id: u32, + title: String, + path: String, + sort: String, + timestamp: DateTime, + pubdate: DateTime, + series_index: f32, + author_sort: String, + isbn: String, + lccn: String, + has_cover: bool, + authors: Vec, + publishers: Vec, + tags: Vec, + series: Option, + comments: Option, + rating: Option, + formats: Vec, + identifiers: Vec, + languages: Vec, + fetch_authors: bool, + fetch_publishers: bool, + fetch_tags: bool, + fetch_series: bool, + fetch_comments: bool, + fetch_rating: bool, + fetch_formats: bool, + fetch_identifiers: bool, + fetch_languages: bool, + db: &'a dyn ReadOnlyDatabase, +} + +impl BookBuilder<'_> { + pub fn sort(mut self, sort: String) -> Self { + self.sort = sort; + self + } + + pub fn timestamp(mut self, timestamp: DateTime) -> Self { + self.timestamp = timestamp; + self + } + + pub fn pubdate(mut self, pubdate: DateTime) -> Self { + self.pubdate = pubdate; + self + } + + pub fn series_index(mut self, series_index: f32) -> Self { + self.series_index = series_index; + self + } + + pub fn author_sort(mut self, author_sort: String) -> Self { + self.author_sort = author_sort; + self + } + + pub fn isbn(mut self, isbn: String) -> Self { + self.isbn = isbn; + self + } + + pub fn lccn(mut self, lccn: String) -> Self { + self.lccn = lccn; + self + } + + pub fn has_cover(mut self, has_cover: bool) -> Self { + self.has_cover = has_cover; + self + } + + pub fn authors(mut self, authors: Vec) -> Self { self.authors = authors; self } - pub fn with_publishers(mut self, publishers: Vec) -> Self { + pub fn publishers(mut self, publishers: Vec) -> Self { self.publishers = publishers; self } - pub fn with_tags(mut self, tags: Vec) -> Self { + pub fn tags(mut self, tags: Vec) -> Self { self.tags = tags; self } - pub fn with_series(mut self, series: Option) -> Self { + pub fn series(mut self, series: Option) -> Self { self.series = series; self } - pub fn with_comments(mut self, comments: Option) -> Self { + pub fn comments(mut self, comments: Option) -> Self { self.comments = comments; self } - pub fn with_rating(mut self, rating: Option) -> Self { + pub fn rating(mut self, rating: Option) -> Self { self.rating = rating; self } - pub fn with_formats(mut self, formats: Vec) -> Self { + pub fn formats(mut self, formats: Vec) -> Self { self.formats = formats; self } - pub fn with_identifiers(mut self, identifiers: Vec) -> Self { + pub fn identifiers(mut self, identifiers: Vec) -> Self { self.identifiers = identifiers; self } - pub fn with_languages(mut self, languages: Vec) -> Self { + pub fn languages(mut self, languages: Vec) -> Self { self.languages = languages; self } + + pub fn fetch_authors(mut self, fetch: bool) -> Self { + self.fetch_authors = fetch; + self + } + + pub fn fetch_publishers(mut self, fetch: bool) -> Self { + self.fetch_publishers = fetch; + self + } + + pub fn fetch_tags(mut self, fetch: bool) -> Self { + self.fetch_tags = fetch; + self + } + + pub fn fetch_series(mut self, fetch: bool) -> Self { + self.fetch_series = fetch; + self + } + + pub fn fetch_comments(mut self, fetch: bool) -> Self { + self.fetch_comments = fetch; + self + } + + pub fn fetch_rating(mut self, fetch: bool) -> Self { + self.fetch_rating = fetch; + self + } + + pub fn fetch_formats(mut self, fetch: bool) -> Self { + self.fetch_formats = fetch; + self + } + + pub fn fetch_identifiers(mut self, fetch: bool) -> Self { + self.fetch_identifiers = fetch; + self + } + + pub fn fetch_languages(mut self, fetch: bool) -> Self { + self.fetch_languages = fetch; + self + } + + pub fn fetch_all(mut self) -> Self { + self.fetch_authors = true; + self.fetch_publishers = true; + self.fetch_tags = true; + self.fetch_series = true; + self.fetch_comments = true; + self.fetch_rating = true; + self.fetch_formats = true; + self.fetch_identifiers = true; + self.fetch_languages = true; + self + } + + pub fn build(mut self) -> crate::error::Result { + let db = self.db; + + // Sequential queries are used here instead of a single complex JOIN. + // This implements a pragmatic trade-off between performance and maintainability. + // + // Why sequential queries instead of a single complex JOIN? + // + // 1. **Cartesian Product Complexity**: Multiple many-to-many JOINs create a cartesian + // product that requires complex aggregation logic to deduplicate results. + // 2. **Maintainability**: Sequential targeted queries are simpler to understand and debug. + // 3. **Performance**: In practice, sequential queries perform well due to SQLite's + // query optimization and caching. The number of queries is fixed (at most 9), + // not dependent on result set size. + // + // When to Consider a Single Query Approach: + // If performance profiling shows N+1 query overhead is significant, consider: + // - Using UNION queries to avoid cartesian products + // - Building a smarter aggregation layer + // - Caching frequently accessed relations + if self.fetch_authors { + self.authors = db.fetch_book_authors(self.id)?; + } + if self.fetch_publishers { + self.publishers = db.fetch_book_publishers(self.id)?; + } + if self.fetch_tags { + self.tags = db.fetch_book_tags(self.id)?; + } + if self.fetch_series { + self.series = db.fetch_book_series(self.id)?; + } + if self.fetch_comments { + self.comments = db.fetch_book_comments(self.id)?; + } + if self.fetch_rating { + self.rating = db.fetch_book_rating(self.id)?; + } + if self.fetch_formats { + self.formats = db.fetch_book_formats(self.id)?; + } + if self.fetch_identifiers { + self.identifiers = db.fetch_book_identifiers(self.id)?; + } + if self.fetch_languages { + self.languages = db.fetch_book_languages(self.id)?; + } + + Ok(Book { + id: self.id, + title: self.title, + path: self.path, + sort: self.sort, + timestamp: self.timestamp, + pubdate: self.pubdate, + series_index: self.series_index, + author_sort: self.author_sort, + isbn: self.isbn, + lccn: self.lccn, + has_cover: self.has_cover, + authors: self.authors, + publishers: self.publishers, + tags: self.tags, + series: self.series, + comments: self.comments, + rating: self.rating, + formats: self.formats, + identifiers: self.identifiers, + languages: self.languages, + }) + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -215,49 +435,76 @@ impl BookMetadata { mod tests { use super::*; + struct MockDatabase; + + impl ReadOnlyDatabase for MockDatabase { + fn get_book(&self, _book_id: u32) -> crate::Result { + unimplemented!() + } + fn all_books(&self) -> crate::Result> { + unimplemented!() + } + fn fetch_book_authors(&self, _book_id: u32) -> crate::Result> { + Ok(Vec::new()) + } + fn fetch_book_publishers(&self, _book_id: u32) -> crate::Result> { + Ok(Vec::new()) + } + fn fetch_book_tags(&self, _book_id: u32) -> crate::Result> { + Ok(Vec::new()) + } + fn fetch_book_series(&self, _book_id: u32) -> crate::Result> { + Ok(None) + } + fn fetch_book_comments(&self, _book_id: u32) -> crate::Result> { + Ok(None) + } + fn fetch_book_rating(&self, _book_id: u32) -> crate::Result> { + Ok(None) + } + fn fetch_book_formats(&self, _book_id: u32) -> crate::Result> { + Ok(Vec::new()) + } + fn fetch_book_identifiers(&self, _book_id: u32) -> crate::Result> { + Ok(Vec::new()) + } + fn fetch_book_languages(&self, _book_id: u32) -> crate::Result> { + Ok(Vec::new()) + } + } + #[test] fn test_book_creation() { - let now = Utc::now(); - let book = Book::new( - 1, - "Test Book".to_string(), - "book, test".to_string(), - now, - now, - 1.0, - "Author, Test".to_string(), - "123-456-789".to_string(), - "".to_string(), - "/path/to/book".to_string(), - true, - ); + let db = MockDatabase; + let book = Book::builder(1, "Test Book".to_string(), "/path/to/book".to_string(), &db) + .sort("book, test".to_string()) + .has_cover(true) + .build() + .unwrap(); assert_eq!(book.id, 1); assert_eq!(book.title, "Test Book"); + assert_eq!(book.path, "/path/to/book"); assert!(book.authors.is_empty()); } #[test] fn test_book_builder_chain() { - let now = Utc::now(); - let authors = vec![Author::new(1, "Test Author".to_string(), "Author, Test".to_string())]; - let tags = vec![Tag::new(1, "Fiction".to_string())]; - - let book = Book::new( + let db = MockDatabase; + let authors = vec![Author::new( 1, - "Test Book".to_string(), - "book, test".to_string(), - now, - now, - 1.0, + "Test Author".to_string(), "Author, Test".to_string(), - "123-456-789".to_string(), - "".to_string(), - "/path/to/book".to_string(), - true, - ) - .with_authors(authors) - .with_tags(tags); + )]; + let tags = vec![Tag::new(1, "Fiction".to_string())]; + + let book = Book::builder(1, "Test Book".to_string(), "/path/to/book".to_string(), &db) + .sort("book, test".to_string()) + .authors(authors) + .tags(tags) + .has_cover(true) + .build() + .unwrap(); assert_eq!(book.authors.len(), 1); assert_eq!(book.tags.len(), 1); @@ -292,28 +539,26 @@ mod tests { #[test] fn test_book_metadata_from_book() { - let now = Utc::now(); - let authors = vec![Author::new(1, "Test Author".to_string(), "Author, Test".to_string())]; + let db = MockDatabase; + let authors = vec![Author::new( + 1, + "Test Author".to_string(), + "Author, Test".to_string(), + )]; let tags = vec![Tag::new(1, "Fiction".to_string())]; let series = Some(Series::new(1, "Test Series".to_string())); - let book = Book::new( - 1, - "Test Book".to_string(), - "book, test".to_string(), - now, - now, - 1.5, - "Author, Test".to_string(), - "123-456-789".to_string(), - "".to_string(), - "/path/to/book".to_string(), - true, - ) - .with_authors(authors) - .with_tags(tags) - .with_series(series) - .with_rating(Some(4)); + let book = Book::builder(1, "Test Book".to_string(), "/path/to/book".to_string(), &db) + .sort("book, test".to_string()) + .series_index(1.5) + .isbn("123-456-789".to_string()) + .authors(authors) + .tags(tags) + .series(series) + .rating(Some(4)) + .has_cover(true) + .build() + .unwrap(); let metadata = BookMetadata::from_book(&book); assert_eq!(metadata.title, "Test Book"); @@ -321,4 +566,4 @@ mod tests { assert_eq!(metadata.rating, Some(4)); assert_eq!(metadata.series, Some("Test Series".to_string())); } -} \ No newline at end of file +} diff --git a/src-tauri/calibre_db/src/queries.rs b/src-tauri/calibre_db/src/queries.rs deleted file mode 100644 index 74431ca..0000000 --- a/src-tauri/calibre_db/src/queries.rs +++ /dev/null @@ -1,457 +0,0 @@ -use crate::error::{CalibreDbError, Result}; -use crate::models::{Author, Book, Identifier, Series, Tag}; -use crate::schema::DatabaseConnection; -use chrono::{DateTime, Utc}; -use rusqlite::params; - -pub fn get_book(conn: &DatabaseConnection, book_id: u32) -> Result { - let mut book = conn.query_row( - "SELECT id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path, has_cover - FROM books WHERE id = ?1", - params![book_id], - |row| { - let timestamp_str: String = row.get(3)?; - let pubdate_str: String = row.get(4)?; - - Ok(Book::new( - row.get(0)?, - row.get(1)?, - row.get(2)?, - parse_timestamp(×tamp_str), - parse_timestamp(&pubdate_str), - row.get(5)?, - row.get(6)?, - row.get(7)?, - row.get(8)?, - row.get(9)?, - row.get::<_, i32>(10)? != 0, - )) - }, - )?; - - let bid = book.id; - book = book.with_authors(get_book_authors(conn, bid)?); - book = book.with_publishers(get_book_publishers(conn, bid)?); - book = book.with_tags(get_book_tags(conn, bid)?); - book = book.with_series(get_book_series(conn, bid)?); - book = book.with_comments(get_book_comments(conn, bid)?); - book = book.with_rating(get_book_rating(conn, bid)?); - book = book.with_formats(get_book_formats(conn, bid)?); - book = book.with_identifiers(get_book_identifiers(conn, bid)?); - book = book.with_languages(get_book_languages(conn, bid)?); - - Ok(book) -} - -pub fn all_books(conn: &DatabaseConnection) -> Result> { - let mut stmt = conn.prepare( - "SELECT id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path, has_cover - FROM books ORDER BY sort ASC")?; - - let books = stmt - .query_map([], |row| { - let timestamp_str: String = row.get(3)?; - let pubdate_str: String = row.get(4)?; - - Ok(Book::new( - row.get(0)?, - row.get(1)?, - row.get(2)?, - parse_timestamp(×tamp_str), - parse_timestamp(&pubdate_str), - row.get(5)?, - row.get(6)?, - row.get(7)?, - row.get(8)?, - row.get(9)?, - row.get::<_, i32>(10)? != 0, - )) - }) - .map_err(|e| CalibreDbError::from(e))? - .collect::, _>>() - .map_err(|e| CalibreDbError::from(e))?; - - let mut enriched_books = Vec::new(); - - for book in books { - let book_id = book.id; - let mut enriched = book; - enriched = enriched.with_authors(get_book_authors(conn, book_id)?); - enriched = enriched.with_publishers(get_book_publishers(conn, book_id)?); - enriched = enriched.with_tags(get_book_tags(conn, book_id)?); - enriched = enriched.with_series(get_book_series(conn, book_id)?); - enriched = enriched.with_comments(get_book_comments(conn, book_id)?); - enriched = enriched.with_rating(get_book_rating(conn, book_id)?); - enriched = enriched.with_formats(get_book_formats(conn, book_id)?); - enriched = enriched.with_identifiers(get_book_identifiers(conn, book_id)?); - enriched = enriched.with_languages(get_book_languages(conn, book_id)?); - - enriched_books.push(enriched); - } - - Ok(enriched_books) -} - -fn get_book_authors(conn: &DatabaseConnection, book_id: u32) -> Result> { - let mut stmt = conn.prepare( - "SELECT a.id, a.name, a.sort - FROM authors a - JOIN books_authors_link bal ON a.id = bal.author - WHERE bal.book = ?1 - ORDER BY bal.id ASC", - )?; - - let authors = stmt - .query_map(params![book_id], |row| { - Ok(Author::new(row.get(0)?, row.get(1)?, row.get(2)?)) - }) - .map_err(|e| CalibreDbError::from(e))? - .collect::, _>>() - .map_err(|e| CalibreDbError::from(e))?; - - Ok(authors) -} - -fn get_book_publishers(conn: &DatabaseConnection, book_id: u32) -> Result> { - let mut stmt = conn.prepare( - "SELECT p.name - FROM publishers p - JOIN books_publishers_link bpl ON p.id = bpl.publisher - WHERE bpl.book = ?1", - )?; - - let publishers = stmt - .query_map(params![book_id], |row| row.get(0)) - .map_err(|e| CalibreDbError::from(e))? - .collect::, _>>() - .map_err(|e| CalibreDbError::from(e))?; - - Ok(publishers) -} - -fn get_book_tags(conn: &DatabaseConnection, book_id: u32) -> Result> { - let mut stmt = conn.prepare( - "SELECT t.id, t.name - FROM tags t - JOIN books_tags_link btl ON t.id = btl.tag - WHERE btl.book = ?1", - )?; - - let tags = stmt - .query_map(params![book_id], |row| { - Ok(Tag::new(row.get(0)?, row.get(1)?)) - }) - .map_err(|e| CalibreDbError::from(e))? - .collect::, _>>() - .map_err(|e| CalibreDbError::from(e))?; - - Ok(tags) -} - -fn get_book_series(conn: &DatabaseConnection, book_id: u32) -> Result> { - let result = conn.query_row( - "SELECT s.id, s.name - FROM series s - JOIN books_series_link bsl ON s.id = bsl.series - WHERE bsl.book = ?1", - params![book_id], - |row| Ok(Series::new(row.get(0)?, row.get(1)?)), - ); - - match result { - Ok(series) => Ok(Some(series)), - Err(CalibreDbError::DatabaseError(ref msg)) if msg.contains("no rows") => Ok(None), - Err(e) => Err(e), - } -} - -fn get_book_comments(conn: &DatabaseConnection, book_id: u32) -> Result> { - let result = conn.query_row( - "SELECT text FROM comments WHERE book = ?1", - params![book_id], - |row| row.get(0), - ); - - match result { - Ok(text) => Ok(Some(text)), - Err(CalibreDbError::DatabaseError(ref msg)) if msg.contains("no rows") => Ok(None), - Err(e) => Err(e), - } -} - -fn get_book_rating(conn: &DatabaseConnection, book_id: u32) -> Result> { - let result = conn.query_row( - "SELECT r.rating - FROM ratings r - JOIN books_ratings_link brl ON r.id = brl.rating - WHERE brl.book = ?1", - params![book_id], - |row| { - let rating: u32 = row.get(0)?; - Ok((rating / 2) as u8) - }, - ); - - match result { - Ok(rating) => Ok(Some(rating)), - Err(CalibreDbError::DatabaseError(ref msg)) if msg.contains("no rows") => Ok(None), - Err(e) => Err(e), - } -} - -fn get_book_formats(conn: &DatabaseConnection, book_id: u32) -> Result> { - let mut stmt = conn.prepare("SELECT format FROM data WHERE book = ?1 ORDER BY id ASC")?; - - let formats = stmt - .query_map(params![book_id], |row| row.get(0)) - .map_err(|e| CalibreDbError::from(e))? - .collect::, _>>() - .map_err(|e| CalibreDbError::from(e))?; - - Ok(formats) -} - -fn get_book_identifiers(conn: &DatabaseConnection, book_id: u32) -> Result> { - let mut stmt = conn.prepare("SELECT book, type, val FROM identifiers WHERE book = ?1")?; - - let identifiers = stmt - .query_map(params![book_id], |row| { - Ok(Identifier::new(row.get(0)?, row.get(1)?, row.get(2)?)) - }) - .map_err(|e| CalibreDbError::from(e))? - .collect::, _>>() - .map_err(|e| CalibreDbError::from(e))?; - - Ok(identifiers) -} - -fn get_book_languages(conn: &DatabaseConnection, book_id: u32) -> Result> { - let mut stmt = conn.prepare( - "SELECT l.lang_code - FROM languages l - JOIN books_languages_link bll ON l.id = bll.lang_code - WHERE bll.book = ?1 - ORDER BY bll.item_order ASC", - )?; - - let languages = stmt - .query_map(params![book_id], |row| row.get(0)) - .map_err(|e| CalibreDbError::from(e))? - .collect::, _>>() - .map_err(|e| CalibreDbError::from(e))?; - - Ok(languages) -} - -fn parse_timestamp(timestamp_str: &str) -> DateTime { - DateTime::parse_from_rfc3339(timestamp_str) - .ok() - .and_then(|dt| Some(dt.with_timezone(&Utc))) - .unwrap_or_else(|| Utc::now()) -} - -#[cfg(test)] -mod tests { - use super::*; - use rusqlite::Connection; - use tempfile::NamedTempFile; - - fn create_test_db_with_books() -> Result<(DatabaseConnection, NamedTempFile)> { - let temp_file = NamedTempFile::new().map_err(|e| CalibreDbError::from(e))?; - let path = temp_file.path().to_path_buf(); - - let conn = Connection::open(&path)?; - - conn.execute( - "CREATE TABLE books ( - id INTEGER PRIMARY KEY, - title TEXT NOT NULL, - sort TEXT, - timestamp TIMESTAMP, - pubdate TIMESTAMP, - series_index REAL, - author_sort TEXT, - isbn TEXT, - lccn TEXT, - path TEXT, - has_cover BOOLEAN DEFAULT 0 - )", - [], - )?; - - conn.execute( - "INSERT INTO books (id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path, has_cover) - VALUES (1, 'Test Book', 'book, test', '2024-01-01T00:00:00Z', '2024-01-01T00:00:00Z', 1.0, 'Author, Test', '123-456', '', '/test', 0)", - [], - )?; - - conn.execute( - "CREATE TABLE authors ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - sort TEXT - )", - [], - )?; - - conn.execute( - "CREATE TABLE books_authors_link ( - id INTEGER PRIMARY KEY, - book INTEGER, - author INTEGER - )", - [], - )?; - - conn.execute( - "CREATE TABLE publishers ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL - )", - [], - )?; - - conn.execute( - "CREATE TABLE books_publishers_link ( - id INTEGER PRIMARY KEY, - book INTEGER, - publisher INTEGER - )", - [], - )?; - - conn.execute( - "CREATE TABLE tags ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL - )", - [], - )?; - - conn.execute( - "CREATE TABLE books_tags_link ( - id INTEGER PRIMARY KEY, - book INTEGER, - tag INTEGER - )", - [], - )?; - - conn.execute( - "CREATE TABLE series ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL - )", - [], - )?; - - conn.execute( - "CREATE TABLE books_series_link ( - id INTEGER PRIMARY KEY, - book INTEGER, - series INTEGER - )", - [], - )?; - - conn.execute( - "CREATE TABLE comments ( - id INTEGER PRIMARY KEY, - book INTEGER, - text TEXT - )", - [], - )?; - - conn.execute( - "CREATE TABLE ratings ( - id INTEGER PRIMARY KEY, - rating INTEGER - )", - [], - )?; - - conn.execute( - "CREATE TABLE books_ratings_link ( - id INTEGER PRIMARY KEY, - book INTEGER, - rating INTEGER - )", - [], - )?; - - conn.execute( - "CREATE TABLE data ( - id INTEGER PRIMARY KEY, - book INTEGER, - format TEXT - )", - [], - )?; - - conn.execute( - "CREATE TABLE identifiers ( - id INTEGER PRIMARY KEY, - book INTEGER, - type TEXT, - val TEXT - )", - [], - )?; - - conn.execute( - "CREATE TABLE languages ( - id INTEGER PRIMARY KEY, - lang_code TEXT NOT NULL - )", - [], - )?; - - conn.execute( - "CREATE TABLE books_languages_link ( - id INTEGER PRIMARY KEY, - book INTEGER, - lang_code INTEGER, - item_order INTEGER DEFAULT 0 - )", - [], - )?; - - drop(conn); - - let db_conn = DatabaseConnection::new(&path)?; - Ok((db_conn, temp_file)) - } - - #[test] - fn test_get_book_basic() { - let result = create_test_db_with_books(); - assert!(result.is_ok()); - - if let Ok((db, _temp)) = result { - let book = get_book(&db, 1); - assert!(book.is_ok()); - - if let Ok(book) = book { - assert_eq!(book.id, 1); - assert_eq!(book.title, "Test Book"); - } - } - } - - #[test] - fn test_all_books() { - let result = create_test_db_with_books(); - assert!(result.is_ok()); - - if let Ok((db, _temp)) = result { - let books = all_books(&db); - assert!(books.is_ok()); - - if let Ok(books) = books { - assert_eq!(books.len(), 1); - assert_eq!(books[0].id, 1); - } - } - } -} diff --git a/src-tauri/calibre_db/src/schema.rs b/src-tauri/calibre_db/src/schema.rs index 04501ef..e44e7d0 100644 --- a/src-tauri/calibre_db/src/schema.rs +++ b/src-tauri/calibre_db/src/schema.rs @@ -19,65 +19,23 @@ impl DatabaseConnection { { self.conn .query_row(query, params, f) - .map_err(|e| CalibreDbError::DatabaseError(e.to_string())) + .map_err(CalibreDbError::from) } - pub fn prepare(&self, query: &str) -> Result { - self.conn - .prepare(query) - .map_err(|e| CalibreDbError::DatabaseError(e.to_string())) - } - - pub fn execute(&self, query: &str, params: &[&dyn rusqlite::ToSql]) -> Result { - self.conn - .execute(query, params) - .map_err(|e| CalibreDbError::DatabaseError(e.to_string())) - } -} - -pub struct Schema; - -impl Schema { - pub fn validate_books_table(conn: &DatabaseConnection) -> Result<()> { - conn.query_row( - "SELECT id, title, sort, timestamp, pubdate, series_index, author_sort, isbn, lccn, path FROM books LIMIT 1", - &[], - |_| Ok(()), - ) - .or_else(|_| { - Err(CalibreDbError::InvalidData( - "books table does not have expected schema".to_string(), - )) - }) - } - - pub fn validate_authors_table(conn: &DatabaseConnection) -> Result<()> { - conn.query_row( - "SELECT id, name, sort FROM authors LIMIT 1", - &[], - |_| Ok(()), - ) - .or_else(|_| { - Err(CalibreDbError::InvalidData( - "authors table does not have expected schema".to_string(), - )) - }) - } - - pub fn validate_database(conn: &DatabaseConnection) -> Result<()> { - Self::validate_books_table(conn)?; - Self::validate_authors_table(conn)?; - Ok(()) + pub fn prepare<'a>(&'a self, query: &str) -> Result> { + self.conn.prepare(query).map_err(CalibreDbError::from) } } #[cfg(test)] mod tests { + use crate::CalibreDbError; + use super::*; use tempfile::NamedTempFile; fn create_test_db() -> Result<(DatabaseConnection, NamedTempFile)> { - let temp_file = NamedTempFile::new().map_err(|e| CalibreDbError::from(e))?; + let temp_file = NamedTempFile::new().map_err(CalibreDbError::from)?; let path = temp_file.path().to_path_buf(); let conn = Connection::open(&path)?; @@ -119,4 +77,14 @@ mod tests { let result = create_test_db(); assert!(result.is_ok()); } -} \ No newline at end of file + + #[test] + fn test_query_row_with_select() { + let (db_conn, _temp_file) = create_test_db().unwrap(); + + let result: Result = + db_conn.query_row("SELECT COUNT(*) FROM books", &[], |row| row.get(0)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0); + } +} diff --git a/src-tauri/calibre_db/tests/integration_tests.rs b/src-tauri/calibre_db/tests/integration_tests.rs index 5cb41a5..fec0310 100644 --- a/src-tauri/calibre_db/tests/integration_tests.rs +++ b/src-tauri/calibre_db/tests/integration_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod integration_tests { - use calibre_db::CalibreDatabase; + use calibre_db::{CalibreDatabase, ReadOnlyDatabase}; use rusqlite::Connection; use tempfile::NamedTempFile; diff --git a/src-tauri/src/library/calibre.rs b/src-tauri/src/library/calibre.rs index e971436..d41f37a 100644 --- a/src-tauri/src/library/calibre.rs +++ b/src-tauri/src/library/calibre.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use calibre_db::CalibreDatabase; +use calibre_db::{CalibreDatabase, ReadOnlyDatabase}; use std::path::PathBuf; use std::sync::Arc; diff --git a/src-tauri/src/library/commands.rs b/src-tauri/src/library/commands.rs index 9de8f4d..6e36add 100644 --- a/src-tauri/src/library/commands.rs +++ b/src-tauri/src/library/commands.rs @@ -125,17 +125,9 @@ pub async fn library_get_book_count(state: State<'_, LibraryState>) -> Result bStr) return sortDirection === "asc" ? 1 : -1; + return 0; }); }, [filteredBooks, sortColumn, sortDirection]); - const handleColumnMenuOpen = (event: React.MouseEvent): void => { + const handleColumnMenuOpen = ( + event: React.MouseEvent, + ): void => { setColumnMenuAnchor(event.currentTarget); }; @@ -299,6 +303,7 @@ export function BooksTable({ if (columnId === "cover") { return 100; } + return columnWidths[columnId] ?? 300; }; @@ -329,7 +334,11 @@ export function BooksTable({ if (isLoading) { return ( - + Loading books... ); @@ -399,14 +408,21 @@ export function BooksTable({ - onResetColumns()}> + onResetColumns()} + > Reset to Default {/* Table */} {filteredBooks.length === 0 ? ( - + No books found ) : ( @@ -479,9 +495,7 @@ export function BooksTable({ {col.label} {col.id !== "cover" && sortColumn === col.id && ( - - {sortDirection === "asc" ? "↑" : "↓"} - + {sortDirection === "asc" ? "↑" : "↓"} )} {col.id !== "cover" && ( diff --git a/src/hooks/useLibrary.ts b/src/hooks/useLibrary.ts index 9d266ae..9ef9290 100644 --- a/src/hooks/useLibrary.ts +++ b/src/hooks/useLibrary.ts @@ -1,9 +1,6 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { Book } from "@/types/book"; -import { - libraryOpen, - libraryGetAllBooks, -} from "@/api/library"; +import { libraryOpen, libraryGetAllBooks } from "@/api/library"; import { useStreamBookCovers } from "./useStreamBookCovers"; const LIBRARY_PATH_KEY = "kikou_library_path"; @@ -50,6 +47,9 @@ export function useLibrary(): UseLibraryReturn { // Open library when path is set const openLibrary = useCallback(async (path: string) => { + // Mark that auto-load should not trigger for this manual open + autoLoadAttemptedRef.current = true; + try { setIsLoading(true); setError(null); diff --git a/src/hooks/useStreamBookCovers.ts b/src/hooks/useStreamBookCovers.ts index 6adc1dc..db67377 100644 --- a/src/hooks/useStreamBookCovers.ts +++ b/src/hooks/useStreamBookCovers.ts @@ -4,105 +4,108 @@ import { streamBookCovers, CoverStreamEvent } from "@/api/library"; import { devLog } from "@/utils/devLog"; export interface UseStreamBookCoversOptions { - bookIds: number[]; - enabled?: boolean; - onProgress?: (loaded: number, total: number) => void; - onFinish?: () => void; - onStart?: () => void; + bookIds: number[]; + enabled?: boolean; + onProgress?: (loaded: number, total: number) => void; + onFinish?: () => void; + onStart?: () => void; } function createDataUrl(base64Data: string): string { - return `data:image/jpeg;base64,${base64Data}`; + return `data:image/jpeg;base64,${base64Data}`; } export function useStreamBookCovers({ - bookIds, - enabled = true, - onProgress, - onFinish, - onStart, + bookIds, + enabled = true, + onProgress, + onFinish, + onStart, }: UseStreamBookCoversOptions): { startStreaming: () => Promise } { - const { coverCache } = useBookCoverContext(); - - const startStreaming = useCallback(async (): Promise => { - if (!enabled || bookIds.length === 0) return; const uncachedIds = bookIds.filter( - (bookId) => !coverCache.current[bookId], - ); - - if (uncachedIds.length === 0) { - devLog("All book covers already cached"); + const { coverCache } = useBookCoverContext(); + + const startStreaming = useCallback(async (): Promise => { + if (!enabled || bookIds.length === 0) return; + + const uncachedIds = bookIds.filter((bookId) => !coverCache.current[bookId]); + + if (uncachedIds.length === 0) { + devLog("All book covers already cached"); + onFinish?.(); + + return; + } + + devLog(`Streaming ${uncachedIds.length} book covers`); + + let loaded = bookIds.length - uncachedIds.length; + const total = bookIds.length; + + try { + await streamBookCovers(uncachedIds, (event: CoverStreamEvent) => { + switch (event.event) { + case "started": + if ( + event.data && + typeof event.data === "object" && + "total_books" in event.data + ) { + devLog(`Started streaming ${event.data.total_books} covers`); + onStart?.(); + } + + break; + + case "cover": { + if ( + event.data && + typeof event.data === "object" && + "book_id" in event.data && + "data_base64" in event.data + ) { + const { book_id, data_base64 } = event.data; + + coverCache.current[book_id] = createDataUrl(data_base64); + loaded++; + + devLog(`Cached cover for book ${book_id} (${loaded}/${total})`); + + onProgress?.(loaded, total); + } + + break; + } + + case "error": + if ( + event.data && + typeof event.data === "object" && + "book_id" in event.data && + "message" in event.data + ) { + devLog( + `Error loading cover for book ${event.data.book_id}: ${event.data.message}`, + ); + loaded++; + onProgress?.(loaded, total); + } + + break; + + case "finished": + devLog("Cover streaming finished"); onFinish?.(); - - return; - } - - devLog(`Streaming ${uncachedIds.length} book covers`); - - let loaded = bookIds.length - uncachedIds.length; - const total = bookIds.length; - - try { - await streamBookCovers(uncachedIds, (event: CoverStreamEvent) => { - switch (event.event) { - case "started": - if ( - event.data && - typeof event.data === "object" && - "total_books" in event.data - ) { - devLog(`Started streaming ${event.data.total_books} covers`); - onStart?.(); - } - break; - - case "cover": { - if ( - event.data && - typeof event.data === "object" && - "book_id" in event.data && - "data_base64" in event.data - ) { - const { book_id, data_base64 } = event.data; - - coverCache.current[book_id] = createDataUrl(data_base64); - loaded++; - - devLog(`Cached cover for book ${book_id} (${loaded}/${total})`); - - onProgress?.(loaded, total); - } - break; - } - - case "error": - if ( - event.data && - typeof event.data === "object" && - "book_id" in event.data && - "message" in event.data - ) { - devLog( - `Error loading cover for book ${event.data.book_id}: ${event.data.message}`, - ); - loaded++; - onProgress?.(loaded, total); - } - break; - - case "finished": - devLog("Cover streaming finished"); - onFinish?.(); - break; - } - }); - } catch (error) { - devLog("Cover streaming error:", error); + break; } - }, [bookIds, enabled, coverCache, onProgress, onFinish, onStart]); + }); + } catch (error) { + devLog("Cover streaming error:", error); + } + }, [bookIds, enabled, coverCache, onProgress, onFinish, onStart]); - useEffect(() => { - startStreaming(); - }, [startStreaming]); + useEffect(() => { + startStreaming(); + }, [startStreaming]); - return { startStreaming }; + return { startStreaming }; } From 684fd1b318953fcb510a98db4727f8be3425b4b9 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:16:57 +0100 Subject: [PATCH 5/6] style: format code and fix lifetime syntax warnings Apply formatting and style improvements across the codebase, including fixing lifetime syntax warnings in Rust code and improving code consistency in TypeScript and Markdown files. - Fix `BookBuilder` return type to explicitly show lifetime `<'_>` - Reformat Rust function signatures for better readability - Apply consistent line breaks in TypeScript test files - Simplify destructured imports in frontend components - Normalize Markdown formatting with consistent spacing - Simplify cargo commands in devenv by using workspace root - Fix various line length and indentation issues Change-Id: 85e497a7abb33f4c592cd26899437264 Change-Id-Short: rulvqspspoow --- .github/copilot-instructions.md | 1 + .../instructions/calibre_db.instructions.md | 9 ++ .github/instructions/error.instructions.md | 10 +- .../library.calibre.instructions.md | 11 ++- .github/instructions/library.instructions.md | 3 + .github/instructions/ts.tests.instructions.md | 26 +++-- .../typescript.typing.instructions.md | 21 ++-- devenv.nix | 7 +- src-tauri/calibre_db/README.md | 12 +-- src-tauri/calibre_db/src/models.rs | 7 +- src/__tests__/components/BooksTable.test.tsx | 16 ++- .../components/OpenLibraryDialog.test.tsx | 24 +++-- src/__tests__/hooks/useLibrary.test.tsx | 24 ++--- src/components/OpenLibraryDialog.tsx | 18 ++-- src/components/README.md | 56 ++++++----- src/components/__tests__/BookCover.test.tsx | 87 ++++++++-------- src/contexts/BookCoverContext.tsx | 32 +++--- src/hooks/useBookCover.ts | 98 +++++++++---------- src/pages/index.tsx | 42 ++++---- 19 files changed, 289 insertions(+), 215 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1810800..7081e8a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -67,6 +67,7 @@ Or use: `devenv shell -- cargo test --workspace` To run tests for a specific crate: + - `cargo test -p calibre_db` (calibre_db crate only) - `cargo test --manifest-path=src-tauri/Cargo.toml` (src-tauri crate only) diff --git a/.github/instructions/calibre_db.instructions.md b/.github/instructions/calibre_db.instructions.md index 61a1534..5f92bc3 100644 --- a/.github/instructions/calibre_db.instructions.md +++ b/.github/instructions/calibre_db.instructions.md @@ -7,9 +7,11 @@ applyTo: "src-tauri/calibre_db/**/*.rs" This is a standalone, publishable Rust crate providing type-safe access to Calibre SQLite databases based on Calibre's database schema (from `third_party/calibre/src/calibre/db`). ## Purpose + Read and deserialize book metadata from Calibre's SQLite `metadata.db` format, supporting 10 core tables (books, authors, publishers, tags, series, data, comments, ratings, identifiers, languages) and their many-to-many relationships. ## Key Modules + - `lib.rs`: Public API (`CalibreDatabase::open()`, `get_book()`, `all_books()`) - `models.rs`: Book, Author, Series, Tag, Identifier data structures with builder patterns - `schema.rs`: Database connection management and schema validation @@ -17,6 +19,7 @@ Read and deserialize book metadata from Calibre's SQLite `metadata.db` format, s - `error.rs`: Error types (DatabaseError, NotFound, InvalidData, IoError, SerializationError) ## Development Guidelines + - Query functions return `Result` or `Result>` for missing relations - Use builder pattern (`with_authors()`, `with_tags()`, etc.) for complex object construction - Handle missing relations gracefully (some books lack series, publishers, ratings) @@ -27,6 +30,7 @@ Read and deserialize book metadata from Calibre's SQLite `metadata.db` format, s ## Database Schema Notes ### Languages Table + The `books_languages_link` table uses a foreign key relationship to the `languages` table: ```sql @@ -45,17 +49,20 @@ CREATE TABLE books_languages_link ( ``` Language codes are stored as references (foreign keys) to the `languages` table, not as direct text columns. When querying languages: + - Join `books_languages_link` with `languages` to fetch language codes - Use `ORDER BY item_order ASC` to maintain language order - Handle cases where books have no associated languages ## Testing + - Unit tests in each module (models, error, schema, queries) - Integration tests in `tests/integration_tests.rs` with real SQLite databases - All tests must create temporary databases; no file I/O to real libraries - Verify both presence and absence of optional relations ## Build & Test + ``` cargo check -p calibre_db # Verify compilation cargo fmt # Format code (or cargo fmt -p calibre_db) @@ -66,6 +73,7 @@ cargo test -p calibre_db --test integration_tests # Integration tests ``` Alternatively, run from the workspace root or use `--workspace` to check all crates: + ``` cargo check --workspace cargo test --workspace @@ -75,6 +83,7 @@ cargo clippy --workspace ## Troubleshooting ### Schema Verification + If you encounter unexpected column errors or schema-related issues, ask the user for their Calibre database path, then verify the actual database schema with: ```bash diff --git a/.github/instructions/error.instructions.md b/.github/instructions/error.instructions.md index 9fd8260..72f609b 100644 --- a/.github/instructions/error.instructions.md +++ b/.github/instructions/error.instructions.md @@ -7,7 +7,9 @@ applyTo: "src-tauri/**/*.rs" Consistent error handling across all Rust modules using enums for type safety and clarity. ## Purpose + Provide a unified error handling strategy that: + - Prioritizes enum error types for domain-specific errors - Ensures Tauri commands return the top-level `AppError` type for IPC communication - Uses specific error enums for internal library functions @@ -16,6 +18,7 @@ Provide a unified error handling strategy that: ## Error Hierarchy ### Tauri Commands Layer + Tauri commands should always return `Result` to ensure proper serialization for IPC: ```rust @@ -26,6 +29,7 @@ async fn fetch_book(lib: State>, id: u32) -> Result Result { ``` ### AppError - Top-Level Error Type + The unified error enum for converting all domain-specific errors to IPC-compatible format. **Variants:** + - `LibraryError(String)`: General library operation failures (path validation, initialization) - `BookNotFound(String)`: Specific book not found by ID or query - `DatabaseError(String)`: Underlying SQLite or database failures @@ -52,6 +58,7 @@ The unified error enum for converting all domain-specific errors to IPC-compatib - `ArchiveError(String)`: Archive/compression operation failures ## Error Conversion + Implement `From` traits to convert domain-specific errors to `AppError`: ```rust @@ -79,9 +86,10 @@ impl From for AppError { ``` ## Guidelines + - **Prioritize enums**: Use specific error enums for internal operations (e.g., `LibraryError`, `ArchiveError`) - **Tauri boundary**: Always return `AppError` from Tauri commands - **Error conversion**: Implement `From` traits to bridge domain-specific errors to `AppError` - **Descriptive messages**: Include context in error messages for debugging - **No panics**: Return errors instead of panicking in library code -- **Result types**: Use `Result` consistently throughout the codebase \ No newline at end of file +- **Result types**: Use `Result` consistently throughout the codebase diff --git a/.github/instructions/library.calibre.instructions.md b/.github/instructions/library.calibre.instructions.md index 866f001..930b788 100644 --- a/.github/instructions/library.calibre.instructions.md +++ b/.github/instructions/library.calibre.instructions.md @@ -24,25 +24,30 @@ unsafe impl Sync for CalibreLibrary {} ## Why This Is Safe ### 1. rusqlite::Connection is Thread-Safe + - rusqlite wraps SQLite's C library, which provides thread-safe database access - SQLite uses internal mutexes for serialization - The Rust wrapper correctly exposes these guarantees - See: https://www.sqlite.org/threadsafe.html ### 2. All Operations Are Read-Only + - The calibre_db crate only queries the database - No mutations occur - No shared mutable state across threads - Data races are impossible with read-only access ### 3. Arc Provides Safe Sharing + - `Arc` is always `Send + Sync` if `T` is `Send + Sync` - Atomic reference counting handles concurrent access safely - The Arc itself is allocated on the heap with stable address - No use-after-free or double-free can occur ### 4. Compiler Verification Still Applies + While marked `unsafe impl`, the actual usage is verified as safe: + - Closures capturing `self` cannot violate Send/Sync requirements - The async runtime cannot move tasks between threads unsafely - Tauri's event loop enforces single-threaded execution where needed @@ -50,16 +55,19 @@ While marked `unsafe impl`, the actual usage is verified as safe: ## Why Not Other Approaches? ### Approach: tokio::task::spawn_blocking + - Would require copying all data into blocking tasks - Unnecessary performance overhead for read-only operations - Adds complexity without benefit ### Approach: Don't make it async + - Violates the trait definition which requires async - Cannot integrate properly with Tauri's async runtime - Blocks on database I/O instead of yielding ### Approach: RwLock or Mutex wrapper + - rusqlite::Connection already handles internal locking - Adding another lock layer creates unnecessary contention - Defeats the purpose of SQLite's built-in concurrency @@ -67,6 +75,7 @@ While marked `unsafe impl`, the actual usage is verified as safe: ## When This Pattern Is Appropriate Use `unsafe impl Send + Sync` when: + 1. Wrapping a known thread-safe C/FFI library (like rusqlite) 2. All operations are read-only or the library handles synchronization 3. The safety invariants are clearly documented @@ -78,4 +87,4 @@ This is a common and accepted pattern in Rust for FFI bindings. - Rust nomicon on FFI and thread safety: https://doc.rust-lang.org/nomicon/ffi.html - rusqlite documentation: https://docs.rs/rusqlite/ -- Tokio task blocking: https://tokio.rs/tokio/tutorial/select#cancellation \ No newline at end of file +- Tokio task blocking: https://tokio.rs/tokio/tutorial/select#cancellation diff --git a/.github/instructions/library.instructions.md b/.github/instructions/library.instructions.md index 6a789de..73aeb50 100644 --- a/.github/instructions/library.instructions.md +++ b/.github/instructions/library.instructions.md @@ -62,6 +62,7 @@ All library commands are prefixed with `library_` for clarity and follow a consi Opens a library from the specified URI and stores it in application state. **Example frontend usage:** + ```typescript const libraryPath = "/home/user/.local/share/calibre/"; await invoke("library_open", { uri: `calibre://${libraryPath}` }); @@ -90,6 +91,7 @@ Returns the total count of books in the currently open library. ## Send + Sync Implementation When implementing `BookLibrary` for thread-safe resources, implementations may need to declare `unsafe impl Send + Sync`. This is required when: + 1. The underlying resource provides thread-safe guarantees (SQLite, etc.) 2. Operations are read-only or properly synchronized 3. Reference counting (Arc) is used for safe sharing @@ -177,6 +179,7 @@ For detailed error handling patterns, see `error.instructions.md`. ## Code Organization Keep the module organized: + - Each library implementation in its own file - Shared types and trait in `base.rs` - Commands layer in `commands.rs` diff --git a/.github/instructions/ts.tests.instructions.md b/.github/instructions/ts.tests.instructions.md index cb76e48..efd53ad 100644 --- a/.github/instructions/ts.tests.instructions.md +++ b/.github/instructions/ts.tests.instructions.md @@ -13,70 +13,80 @@ When testing React components, any code that causes state updates must be wrappe ### Common Scenarios Requiring act() 1. **Timer Advances (jest.advanceTimersByTime, jest.runAllTimers, etc.)** + ```typescript import { act } from "@testing-library/react"; - + act(() => { - jest.advanceTimersByTime(1000); + jest.advanceTimersByTime(1000); }); ``` 2. **Manual State Updates in Tests** + ```typescript act(() => { - // Code that triggers state updates - someFunction(); + // Code that triggers state updates + someFunction(); }); ``` 3. **Cleanup with Pending Timers** ```typescript afterEach(() => { - act(() => { - jest.runOnlyPendingTimers(); - }); - jest.useRealTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); + jest.useRealTimers(); }); ``` ### Why This Matters + - Ensures tests accurately simulate browser behavior - Prevents flaky tests and race conditions - Makes test assertions reliable and deterministic - React requires this for proper testing of concurrent features **If you see "An update to [Component] inside a test was not wrapped in act(...)" warnings:** + 1. Identify what's causing the state update (timers, async operations, etc.) 2. Wrap that code in `act()` 3. Re-run tests to verify warnings are gone ## Test ID Selection + - Prefer using `getByTestId` (or `queryByTestId`, `findByTestId`, etc.) over `getByText` for selecting elements in frontend tests. This ensures selectors are robust against UI text changes and localization. - Only use `getByText` when there is no reasonable alternative (e.g. for verifying visible text content). - Always add a `data-testid` attribute to important elements/components that need to be targeted in tests. ## Content Assertions + - When asserting text content within an element retrieved via `getByTestId`, check the element's content against **test data variables** rather than hardcoded strings. - Use test data (like `mockBook.title`) to verify that rendered content matches the expected data. **Good:** + ```typescript const element = screen.getByTestId("book-title"); expect(element).toHaveTextContent(mockBook.title); ``` **Avoid:** + ```typescript const element = screen.getByTestId("book-title"); expect(element).toHaveTextContent("Test Book"); ``` **Also Good (when text is meant to be static UI):** + ```typescript expect(screen.getByTestId("error-message")).toBeInTheDocument(); ``` ## Guidelines + - Define test data (mocks, fixtures) as constants at the top of test files - Reference these constants in assertions instead of hardcoding strings - This makes tests maintainable: when test data changes, assertions automatically reflect those changes diff --git a/.github/instructions/typescript.typing.instructions.md b/.github/instructions/typescript.typing.instructions.md index 443c63e..a997752 100644 --- a/.github/instructions/typescript.typing.instructions.md +++ b/.github/instructions/typescript.typing.instructions.md @@ -17,6 +17,7 @@ Ensure strict typing across all TypeScript files to catch errors at compile time ## Guidelines ### Function Declarations + ```typescript // Good - Explicit parameter and return types function getBookTitle(book: Book): string { @@ -35,6 +36,7 @@ function getBookTitle(book: Book) { ``` ### Object/Interface Definitions + ```typescript // Good - Explicit typed object interface SearchQuery { @@ -51,6 +53,7 @@ const query = { ``` ### useState and State Management + ```typescript // Good - Explicit type parameter const [books, setBooks] = useState([]); @@ -61,6 +64,7 @@ const [books, setBooks] = useState([]); ``` ### Event Handlers + ```typescript // Good - Explicit event type const handleClick = (event: React.MouseEvent): void => { @@ -74,6 +78,7 @@ const handleClick = (event) => { ``` ### Callback Functions + ```typescript // Good - Explicit parameter and return types const handleToggle = (columnId: string): void => { @@ -87,6 +92,7 @@ const handleToggle = (columnId) => { ``` ### useMemo and useCallback + ```typescript // Good - Explicit return type const filteredBooks = useMemo( @@ -94,12 +100,9 @@ const filteredBooks = useMemo( [books, query], ); -const handleSort = useCallback( - (columnId: string): void => { - // Implementation - }, - [], -); +const handleSort = useCallback((columnId: string): void => { + // Implementation +}, []); // Bad - No return type const filteredBooks = useMemo( @@ -109,6 +112,7 @@ const filteredBooks = useMemo( ``` ### Union and Optional Types + ```typescript // Good - Explicit union type type SortDirection = "asc" | "desc" | null; @@ -123,6 +127,7 @@ const handleError = (message?) => {}; ``` ### Generic Types + ```typescript // Good - Explicit generic parameter function parseQuery(data: string): Record { @@ -136,6 +141,7 @@ function parseQuery(data: string) { ``` ### Array Types + ```typescript // Good - Explicit array type const books: Book[] = []; @@ -149,6 +155,7 @@ const books = []; ``` ### Ref Types + ```typescript // Good - Explicit ref type const resizingColumn = useRef(null); @@ -161,12 +168,14 @@ const resizingColumn = useRef(null); ## When `any` or `unknown` is Acceptable Only use `any` or `unknown` when: + 1. Working with truly dynamic external data (e.g., JSON from API without schema) 2. Integrating with untyped third-party libraries 3. There is an explicit code comment explaining why 4. The situation is temporary and there is a task to fix it **Always document with a comment:** + ```typescript // TODO: Replace with proper type once API schema is defined const data: any = response.data; diff --git a/devenv.nix b/devenv.nix index eb08084..9353589 100644 --- a/devenv.nix +++ b/devenv.nix @@ -81,9 +81,10 @@ eslint src pnpm test pnpm run build - cargo test --manifest-path ./src-tauri/Cargo.toml - cargo check --manifest-path ./src-tauri/Cargo.toml - cargo build --manifest-path ./src-tauri/Cargo.toml + cargo test + cargo check + cargo clippy + cargo build nix flake check nix eval ''; diff --git a/src-tauri/calibre_db/README.md b/src-tauri/calibre_db/README.md index fe75b1f..a9c79c2 100644 --- a/src-tauri/calibre_db/README.md +++ b/src-tauri/calibre_db/README.md @@ -33,16 +33,16 @@ use calibre_db::CalibreDatabase; fn main() -> Result<(), Box> { let db = CalibreDatabase::open("/path/to/library/metadata.db")?; - + // Get a specific book let book = db.get_book(1)?; println!("Title: {}", book.title); println!("Authors: {:?}", book.authors); - + // Get all books let books = db.all_books()?; println!("Total books: {}", books.len()); - + Ok(()) } ``` @@ -118,8 +118,8 @@ let db = CalibreDatabase::open("metadata.db")?; let books = db.all_books()?; for book in books { - println!("{}: {} by {}", - book.id, + println!("{}: {} by {}", + book.id, book.title, book.author_sort); } @@ -140,7 +140,7 @@ println!("Published: {}", book.pubdate); println!("Added: {}", book.timestamp); // Relations -println!("Authors: {} ({})", +println!("Authors: {} ({})", book.authors.len(), book.author_sort); println!("Publishers: {}", book.publishers.join(", ")); diff --git a/src-tauri/calibre_db/src/models.rs b/src-tauri/calibre_db/src/models.rs index 027cbd6..cadbcb0 100644 --- a/src-tauri/calibre_db/src/models.rs +++ b/src-tauri/calibre_db/src/models.rs @@ -43,7 +43,12 @@ impl Book { /// assert_eq!(book.id, 1); /// assert_eq!(book.title, "Title"); /// ``` - pub fn builder(id: u32, title: String, path: String, db: &dyn ReadOnlyDatabase) -> BookBuilder { + pub fn builder( + id: u32, + title: String, + path: String, + db: &dyn ReadOnlyDatabase, + ) -> BookBuilder<'_> { BookBuilder { id, title, diff --git a/src/__tests__/components/BooksTable.test.tsx b/src/__tests__/components/BooksTable.test.tsx index a4f1ccd..16175c6 100644 --- a/src/__tests__/components/BooksTable.test.tsx +++ b/src/__tests__/components/BooksTable.test.tsx @@ -86,7 +86,9 @@ describe("BooksTable", () => { />, ); - const searchInput = screen.getByTestId("books-search-input") as HTMLInputElement; + const searchInput = screen.getByTestId( + "books-search-input", + ) as HTMLInputElement; await userEvent.type(searchInput, "Another"); expect(screen.getByText(mockBook2.title)).toBeInTheDocument(); @@ -119,7 +121,9 @@ describe("BooksTable", () => { />, ); - const searchInput = screen.getByTestId("books-search-input") as HTMLInputElement; + const searchInput = screen.getByTestId( + "books-search-input", + ) as HTMLInputElement; await userEvent.type(searchInput, `title:"${mockBook.title}"`); expect(screen.getByText(mockBook.title)).toBeInTheDocument(); @@ -195,7 +199,9 @@ describe("BooksTable", () => { />, ); - const searchInput = screen.getByTestId("books-search-input") as HTMLInputElement; + const searchInput = screen.getByTestId( + "books-search-input", + ) as HTMLInputElement; await userEvent.type(searchInput, "NonexistentBook"); expect(screen.getByTestId("empty-state")).toBeInTheDocument(); @@ -245,7 +251,9 @@ describe("BooksTable", () => { ); const bookCountElement = screen.getByTestId("book-count"); - expect(bookCountElement).toHaveTextContent(`Showing ${books.length} of ${books.length} books`); + expect(bookCountElement).toHaveTextContent( + `Showing ${books.length} of ${books.length} books`, + ); }); it("formats array values with comma separation", () => { diff --git a/src/__tests__/components/OpenLibraryDialog.test.tsx b/src/__tests__/components/OpenLibraryDialog.test.tsx index 59b4e36..82c7d89 100644 --- a/src/__tests__/components/OpenLibraryDialog.test.tsx +++ b/src/__tests__/components/OpenLibraryDialog.test.tsx @@ -26,7 +26,9 @@ describe("OpenLibraryDialog", () => { ); expect(screen.getByText("Open Calibre Library")).toBeInTheDocument(); - expect(screen.getByText(/Select the folder containing/)).toBeInTheDocument(); + expect( + screen.getByText(/Select the folder containing/), + ).toBeInTheDocument(); }); it("does not render dialog when open is false", () => { @@ -143,7 +145,9 @@ describe("OpenLibraryDialog", () => { await userEvent.click(browseButton); await waitFor(() => { - expect(screen.getByDisplayValue("/test/library/path")).toBeInTheDocument(); + expect( + screen.getByDisplayValue("/test/library/path"), + ).toBeInTheDocument(); }); const openButton = screen.getByRole("button", { name: /Open Library/i }); @@ -155,7 +159,9 @@ describe("OpenLibraryDialog", () => { }); it("displays error when file dialog fails", async () => { - (dialogPlugin.open as jest.Mock).mockRejectedValue(new Error("Dialog failed")); + (dialogPlugin.open as jest.Mock).mockRejectedValue( + new Error("Dialog failed"), + ); renderWithProviders( { await userEvent.click(browseButton); await waitFor(() => { - expect(screen.getByDisplayValue("/test/library/path")).toBeInTheDocument(); + expect( + screen.getByDisplayValue("/test/library/path"), + ).toBeInTheDocument(); }); const openButton = screen.getByRole("button", { name: /Open Library/i }); @@ -230,7 +238,9 @@ describe("OpenLibraryDialog", () => { await userEvent.click(browseButton); await waitFor(() => { - expect(screen.getByDisplayValue("/test/library/path")).toBeInTheDocument(); + expect( + screen.getByDisplayValue("/test/library/path"), + ).toBeInTheDocument(); }); rerender( @@ -282,7 +292,9 @@ describe("OpenLibraryDialog", () => { await userEvent.click(browseButton); await waitFor(() => { - expect(screen.getByDisplayValue("/test/library/path")).toBeInTheDocument(); + expect( + screen.getByDisplayValue("/test/library/path"), + ).toBeInTheDocument(); }); const openButton = screen.getByRole("button", { name: /Open Library/i }); diff --git a/src/__tests__/hooks/useLibrary.test.tsx b/src/__tests__/hooks/useLibrary.test.tsx index 3a00262..138cdc2 100644 --- a/src/__tests__/hooks/useLibrary.test.tsx +++ b/src/__tests__/hooks/useLibrary.test.tsx @@ -94,7 +94,7 @@ describe("useLibrary", () => { it("handles error when opening library fails", async () => { const errorMessage = "Failed to open library"; (libraryApi.libraryOpen as jest.Mock).mockRejectedValueOnce( - new Error(errorMessage) + new Error(errorMessage), ); (libraryApi.libraryGetAllBooks as jest.Mock).mockResolvedValue([]); @@ -128,8 +128,7 @@ describe("useLibrary", () => { it("sets loading state while fetching books", async () => { (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockImplementation( - () => - new Promise((resolve) => setTimeout(() => resolve(mockBooks), 50)) + () => new Promise((resolve) => setTimeout(() => resolve(mockBooks), 50)), ); const { result } = renderHook(() => useLibrary(), { wrapper }); @@ -150,7 +149,7 @@ describe("useLibrary", () => { const errorMessage = "Failed to load books"; (libraryApi.libraryOpen as jest.Mock).mockResolvedValue(undefined); (libraryApi.libraryGetAllBooks as jest.Mock).mockRejectedValue( - new Error(errorMessage) + new Error(errorMessage), ); const { result } = renderHook(() => useLibrary(), { wrapper }); @@ -224,7 +223,7 @@ describe("useLibrary", () => { it("handles string error objects", async () => { (libraryApi.libraryOpen as jest.Mock).mockRejectedValueOnce( - "String error message" + "String error message", ); const { result } = renderHook(() => useLibrary(), { wrapper }); @@ -272,7 +271,9 @@ describe("useLibrary", () => { }); // Both API calls should be made - expect(libraryApi.libraryOpen).toHaveBeenCalledWith("calibre:///test/path"); + expect(libraryApi.libraryOpen).toHaveBeenCalledWith( + "calibre:///test/path", + ); // Manually load books to verify the call happens await act(async () => { @@ -295,7 +296,7 @@ describe("useLibrary", () => { // Verify the backend was called expect(libraryApi.libraryOpen).toHaveBeenCalledWith( - "calibre:///saved/path" + "calibre:///saved/path", ); // Load books @@ -324,7 +325,7 @@ describe("useLibrary", () => { // Wait for auto-load to happen await waitFor(() => { expect(libraryApi.libraryOpen).toHaveBeenCalledWith( - "calibre:///saved/path" + "calibre:///saved/path", ); }); @@ -348,9 +349,8 @@ describe("useLibrary", () => { await result.current.openLibrary("/test/path"); }); - const openCallCount = ( - libraryApi.libraryOpen as jest.Mock - ).mock.calls.length; + const openCallCount = (libraryApi.libraryOpen as jest.Mock).mock.calls + .length; // Try opening the same path again await act(async () => { @@ -359,7 +359,7 @@ describe("useLibrary", () => { // Should have called open twice (once for each call) expect((libraryApi.libraryOpen as jest.Mock).mock.calls.length).toBe( - openCallCount + 1 + openCallCount + 1, ); }); diff --git a/src/components/OpenLibraryDialog.tsx b/src/components/OpenLibraryDialog.tsx index d6de212..46eef83 100644 --- a/src/components/OpenLibraryDialog.tsx +++ b/src/components/OpenLibraryDialog.tsx @@ -128,20 +128,16 @@ export function OpenLibraryDialog({ /> - - - - + + setDialogOpen(false)} @@ -292,7 +297,7 @@ export function LibraryPage() { return ( <>

Library: {libraryPath}

- + ) => { - return ( - - - - ); +const MockedBookCover = ({ + bookId, + ...props +}: React.ComponentProps) => { + return ( + + + + ); }; describe("BookCover", () => { - beforeEach(() => { - jest.useFakeTimers(); - }); + beforeEach(() => { + jest.useFakeTimers(); + }); - afterEach(() => { - act(() => { - jest.runOnlyPendingTimers(); - }); - jest.useRealTimers(); + afterEach(() => { + act(() => { + jest.runOnlyPendingTimers(); }); + jest.useRealTimers(); + }); - it("should render skeleton when loading", () => { - render(); + it("should render skeleton when loading", () => { + render(); - const skeleton = document.querySelector(".MuiSkeleton-root"); + const skeleton = document.querySelector(".MuiSkeleton-root"); - expect(skeleton).toBeInTheDocument(); - }); + expect(skeleton).toBeInTheDocument(); + }); - it("should render 'No cover' when bookId is null", async () => { - render(); + it("should render 'No cover' when bookId is null", async () => { + render(); - await screen.findByText("No cover"); + await screen.findByText("No cover"); - expect(screen.getByText("No cover")).toBeInTheDocument(); - }); + expect(screen.getByText("No cover")).toBeInTheDocument(); + }); - it("should render image alt text correctly", () => { - const ref = React.createRef>(); + it("should render image alt text correctly", () => { + const ref = React.createRef>(); - ref.current = { 1: "data:image/jpeg;base64,test123" }; + ref.current = { 1: "data:image/jpeg;base64,test123" }; - render(); + render(); - act(() => { - jest.advanceTimersByTime(200); - }); + act(() => { + jest.advanceTimersByTime(200); + }); - const img = screen.queryByAltText("Test Book Cover"); + const img = screen.queryByAltText("Test Book Cover"); - if (img) { - expect(img).toBeInTheDocument(); - } - }); + if (img) { + expect(img).toBeInTheDocument(); + } + }); - it("should handle width and height props", async () => { - render(); + it("should handle width and height props", async () => { + render(); - await screen.findByText("No cover"); + await screen.findByText("No cover"); - const container = screen.getByText("No cover").parentElement; + const container = screen.getByText("No cover").parentElement; - expect(container).toHaveStyle({ width: "100px", height: "150px" }); - }); + expect(container).toHaveStyle({ width: "100px", height: "150px" }); + }); }); diff --git a/src/contexts/BookCoverContext.tsx b/src/contexts/BookCoverContext.tsx index 4d300fb..aec09fd 100644 --- a/src/contexts/BookCoverContext.tsx +++ b/src/contexts/BookCoverContext.tsx @@ -1,35 +1,35 @@ import React, { createContext, useContext, useRef, ReactNode } from "react"; interface BookCoverContextType { - coverCache: React.RefObject>; + coverCache: React.RefObject>; } const BookCoverContext = createContext(null); interface BookCoverProviderProps { - children: ReactNode; + children: ReactNode; } export function BookCoverProvider({ - children, + children, }: BookCoverProviderProps): React.ReactElement { - const coverCache = useRef>({}); + const coverCache = useRef>({}); - return ( - - {children} - - ); + return ( + + {children} + + ); } export function useBookCoverContext(): BookCoverContextType { - const context = useContext(BookCoverContext); + const context = useContext(BookCoverContext); - if (!context) { - throw new Error( - "useBookCoverContext must be used within BookCoverProvider", - ); - } + if (!context) { + throw new Error( + "useBookCoverContext must be used within BookCoverProvider", + ); + } - return context; + return context; } diff --git a/src/hooks/useBookCover.ts b/src/hooks/useBookCover.ts index fa5aa67..f7edeba 100644 --- a/src/hooks/useBookCover.ts +++ b/src/hooks/useBookCover.ts @@ -2,56 +2,56 @@ import { useState, useEffect } from "react"; import { useBookCoverContext } from "@/contexts/BookCoverContext"; export interface UseBookCoverResult { - coverUrl: string | null; - loading: boolean; - error: string | null; + coverUrl: string | null; + loading: boolean; + error: string | null; } export function useBookCover(bookId: number | null): UseBookCoverResult { - const { coverCache } = useBookCoverContext(); - const [coverUrl, setCoverUrl] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - if (bookId === null) { - setCoverUrl(null); - setLoading(false); - setError(null); - return; - } - - if (coverCache.current[bookId]) { - setCoverUrl(coverCache.current[bookId]); - setLoading(false); - setError(null); - return; - } - - setLoading(true); - setError(null); - - const checkInterval = setInterval(() => { - if (coverCache.current[bookId]) { - setCoverUrl(coverCache.current[bookId]); - setLoading(false); - clearInterval(checkInterval); - } - }, 100); - - const timeout = setTimeout(() => { - if (!coverCache.current[bookId]) { - setError("Cover not available"); - setLoading(false); - clearInterval(checkInterval); - } - }, 10000); - - return () => { - clearInterval(checkInterval); - clearTimeout(timeout); - }; - }, [bookId, coverCache]); - - return { coverUrl, loading, error }; + const { coverCache } = useBookCoverContext(); + const [coverUrl, setCoverUrl] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (bookId === null) { + setCoverUrl(null); + setLoading(false); + setError(null); + return; + } + + if (coverCache.current[bookId]) { + setCoverUrl(coverCache.current[bookId]); + setLoading(false); + setError(null); + return; + } + + setLoading(true); + setError(null); + + const checkInterval = setInterval(() => { + if (coverCache.current[bookId]) { + setCoverUrl(coverCache.current[bookId]); + setLoading(false); + clearInterval(checkInterval); + } + }, 100); + + const timeout = setTimeout(() => { + if (!coverCache.current[bookId]) { + setError("Cover not available"); + setLoading(false); + clearInterval(checkInterval); + } + }, 10000); + + return () => { + clearInterval(checkInterval); + clearTimeout(timeout); + }; + }, [bookId, coverCache]); + + return { coverUrl, loading, error }; } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index dacc37d..483c175 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,14 +1,7 @@ "use client"; import { useState } from "react"; -import { - Box, - Button, - Typography, - Card, - Alert, - Stack, -} from "@mui/joy"; +import { Box, Button, Typography, Card, Alert, Stack } from "@mui/joy"; import { useResetNavigation } from "@/hooks/useResetNavigation"; import { useLibrary } from "@/hooks/useLibrary"; import { useTableColumns } from "@/hooks/useTableColumns"; @@ -19,21 +12,11 @@ import LoadingOverlay from "@/components/ui/LoadingOverlay"; export default function HomePage() { const [openLibraryDialogOpen, setOpenLibraryDialogOpen] = useState(false); - const { - books, - isLoading, - error, - libraryPath, - openLibrary, - isLibraryOpen, - } = useLibrary(); + const { books, isLoading, error, libraryPath, openLibrary, isLibraryOpen } = + useLibrary(); - const { - columns, - toggleColumnVisibility, - reorderColumns, - resetColumns, - } = useTableColumns(); + const { columns, toggleColumnVisibility, reorderColumns, resetColumns } = + useTableColumns(); useResetNavigation(); @@ -138,12 +121,23 @@ export default function HomePage() {
{error && ( - + {error} )} - + Date: Thu, 6 Nov 2025 21:16:57 +0100 Subject: [PATCH 6/6] build: remove lock files and update dependencies Update Cargo.lock files and flake dependencies to latest versions. Remove cargoRoot configuration from flake.nix as it's no longer needed with the workspace structure. - Delete src-tauri/Cargo.lock and calibre_db/Cargo.lock from VCS - Update quote crate to 1.0.42 in Cargo.lock - Update nixpkgs to latest version (1762233356) - Update cargoHash in flake.nix due to dependency changes - Remove unused cargoRoot and buildAndTestSubdir from flake.nix Change-Id: 680869581f6438bb27258817c61b7bf1 Change-Id-Short: trzrtquryktv --- Cargo.lock | 4 +- flake.lock | 6 +- flake.nix | 5 +- src-tauri/Cargo.lock | 6112 ------------------------------- src-tauri/calibre_db/Cargo.lock | 893 ----- 5 files changed, 6 insertions(+), 7014 deletions(-) delete mode 100644 src-tauri/Cargo.lock delete mode 100644 src-tauri/calibre_db/Cargo.lock diff --git a/Cargo.lock b/Cargo.lock index 6400891..0a8d034 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3169,9 +3169,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] diff --git a/flake.lock b/flake.lock index d134e37..8cd8389 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1759281824, - "narHash": "sha256-FIBE1qXv9TKvSNwst6FumyHwCRH3BlWDpfsnqRDCll0=", + "lastModified": 1762233356, + "narHash": "sha256-cGS3lLTYusbEP/IJIWGgnkzIl+FA5xDvtiHyjalGr4k=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5b5be50345d4113d04ba58c444348849f5585b4a", + "rev": "ca534a76c4afb2bdc07b681dbc11b453bab21af8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 05ffec4..2649a42 100644 --- a/flake.nix +++ b/flake.nix @@ -24,7 +24,7 @@ src = ./.; - cargoHash = "sha256-cp7EFD1RI/qTkmpqritTvjsUCQq8MVWCVYXQwoNNj6g="; + cargoHash = "sha256-c5ck7uzDC1d5EMMSr9caYZFoJ/GjQ4aIngoTjaFkbhw="; pnpmDeps = pkgs.pnpm.fetchDeps { inherit (finalAttrs) pname version src; @@ -48,9 +48,6 @@ pkgs.webkitgtk_4_1 ]; - cargoRoot = "src-tauri"; - buildAndTestSubdir = cargoRoot; - # installPhase = '' # mkdir -p $out/bin # diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock deleted file mode 100644 index 04d9f6b..0000000 --- a/src-tauri/Cargo.lock +++ /dev/null @@ -1,6112 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "Kikou" -version = "0.1.0" -dependencies = [ - "async-trait", - "base64 0.22.1", - "calibre_db", - "log", - "notify", - "once_cell", - "quick-xml 0.31.0", - "rayon", - "rusqlite", - "serde", - "serde-xml-rs", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-log", - "tempfile", - "tokio", - "zip", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android_log-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" - -[[package]] -name = "android_logger" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" -dependencies = [ - "android_log-sys", - "env_filter", - "log", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "ashpd" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" -dependencies = [ - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.2", - "raw-window-handle", - "serde", - "serde_repr", - "tokio", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus", -] - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.0", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" -dependencies = [ - "serde", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] - -[[package]] -name = "block2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" -dependencies = [ - "objc2 0.6.2", -] - -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "borsh-derive", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" -dependencies = [ - "once_cell", - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "byte-unit" -version = "5.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" -dependencies = [ - "rust_decimal", - "serde", - "utf8-width", -] - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bytemuck" -version = "1.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -dependencies = [ - "serde", -] - -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.9.4", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "calibre_db" -version = "0.1.0" -dependencies = [ - "chrono", - "rusqlite", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "camino" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "cargo_toml" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" -dependencies = [ - "serde", - "toml 0.9.7", -] - -[[package]] -name = "cc" -version = "1.2.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.0", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.9.4", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.9.4", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.29.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.106", -] - -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" -dependencies = [ - "quote", - "syn 2.0.106", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.106", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "deflate64" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" - -[[package]] -name = "deranged" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.106", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.1", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" -dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", - "libc", - "objc2 0.6.2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading", -] - -[[package]] -name = "dlopen2" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -dependencies = [ - "serde", -] - -[[package]] -name = "dtoa" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "embed-resource" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 0.9.7", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" - -[[package]] -name = "endi" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.1", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "fern" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" -dependencies = [ - "log", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" - -[[package]] -name = "flate2" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" -dependencies = [ - "crc32fast", - "libz-rs-sys", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.9.4", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.12", -] - -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever", - "match_token", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-util" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.1", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ico" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" -dependencies = [ - "byteorder", - "png", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" -dependencies = [ - "equivalent", - "hashbrown 0.16.0", - "serde", - "serde_core", -] - -[[package]] -name = "infer" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" -dependencies = [ - "cfb", -] - -[[package]] -name = "inotify" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" -dependencies = [ - "bitflags 2.9.4", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.9.4", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser", - "html5ever", - "indexmap 2.11.4", - "selectors", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading", - "once_cell", -] - -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - -[[package]] -name = "libc" -version = "0.2.176" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libredox" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = [ - "bitflags 2.9.4", - "libc", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libz-rs-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" -dependencies = [ - "zlib-rs", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" -dependencies = [ - "value-bag", -] - -[[package]] -name = "lzma-rust2" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" -dependencies = [ - "crc", - "sha2", -] - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "markup5ever" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" -dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", -] - -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "muda" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2 0.6.2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation 0.3.1", - "once_cell", - "png", - "serde", - "thiserror 2.0.17", - "windows-sys 0.60.2", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.9.4", - "jni-sys", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags 2.9.4", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-types" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" -dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" -dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", - "libc", - "objc2 0.6.2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-foundation 0.3.1", - "objc2-quartz-core 0.3.1", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" -dependencies = [ - "bitflags 2.9.4", - "dispatch2", - "objc2 0.6.2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" -dependencies = [ - "bitflags 2.9.4", - "dispatch2", - "objc2 0.6.2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" -dependencies = [ - "objc2 0.6.2", - "objc2-foundation 0.3.1", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "libc", - "objc2 0.5.2", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" -dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", - "libc", - "objc2 0.6.2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-javascript-core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a" -dependencies = [ - "objc2 0.6.2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", -] - -[[package]] -name = "objc2-security" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-core-foundation", - "objc2-foundation 0.3.1", -] - -[[package]] -name = "objc2-web-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" -dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", - "objc2 0.6.2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation 0.3.1", - "objc2-javascript-core", - "objc2-security", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher 1.0.1", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64 0.22.1", - "indexmap 2.11.4", - "quick-xml 0.38.3", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppmd-rust" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.6", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[package]] -name = "proc-macro2" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "quick-xml" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = [ - "bitflags 2.9.4", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 2.0.17", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "regex" -version = "1.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "reqwest" -version = "0.12.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "rfd" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" -dependencies = [ - "ashpd", - "block2 0.6.1", - "dispatch2", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc2 0.6.2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation 0.3.1", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rkyv" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rusqlite" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" -dependencies = [ - "bitflags 2.9.4", - "chrono", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - -[[package]] -name = "rust_decimal" -version = "1.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.5", - "rkyv", - "serde", - "serde_json", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags 2.9.4", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.1", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "indexmap 1.9.3", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.106", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "selectors" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" -dependencies = [ - "bitflags 1.3.2", - "cssparser", - "derive_more", - "fxhash", - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "serde-xml-rs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53630160a98edebde0123eb4dfd0fce6adff091b2305db3154a9e920206eb510" -dependencies = [ - "log", - "serde", - "thiserror 1.0.69", - "xml-rs", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.11.4", - "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "servo_arc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" -dependencies = [ - "nodrop", - "stable_deref_trait", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = [ - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "softbuffer" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" -dependencies = [ - "bytemuck", - "cfg_aliases", - "core-graphics", - "foreign-types", - "js-sys", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", - "raw-window-handle", - "redox_syscall", - "wasm-bindgen", - "web-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "swift-rs" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.34.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" -dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", - "core-foundation", - "core-graphics", - "crossbeam-channel", - "dispatch", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni", - "lazy_static", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "objc2 0.6.2", - "objc2-app-kit", - "objc2-foundation 0.3.1", - "once_cell", - "parking_lot", - "raw-window-handle", - "scopeguard", - "tao-macros", - "unicode-segmentation", - "url", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f07c6590706b2fc0ab287b041cf5ce9c435b3850bdae5571e19d9d27584e89d" -dependencies = [ - "anyhow", - "bytes", - "cookie", - "dirs", - "dunce", - "embed_plist", - "getrandom 0.3.3", - "glob", - "gtk", - "heck 0.5.0", - "http", - "jni", - "libc", - "log", - "mime", - "muda", - "objc2 0.6.2", - "objc2-app-kit", - "objc2-foundation 0.3.1", - "objc2-ui-kit", - "objc2-web-kit", - "percent-encoding", - "plist", - "raw-window-handle", - "reqwest", - "serde", - "serde_json", - "serde_repr", - "serialize-to-javascript", - "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", - "thiserror 2.0.17", - "tokio", - "tray-icon", - "url", - "urlpattern", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows", -] - -[[package]] -name = "tauri-build" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71be1f494b683ac439e6d61c16ab5c472c6f9c6ee78995b29556d9067c021a1" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs", - "glob", - "heck 0.5.0", - "json-patch", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "tauri-utils", - "tauri-winres", - "toml 0.9.7", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190" -dependencies = [ - "base64 0.22.1", - "brotli", - "ico", - "json-patch", - "plist", - "png", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.106", - "tauri-utils", - "thiserror 2.0.17", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "260c5d2eb036b76206b9fca20b7be3614cfd21046c5396f7959e0e64a4b07f2f" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-plugin" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "toml 0.9.7", - "walkdir", -] - -[[package]] -name = "tauri-plugin-dialog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" -dependencies = [ - "log", - "raw-window-handle", - "rfd", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.17", - "url", -] - -[[package]] -name = "tauri-plugin-fs" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" -dependencies = [ - "anyhow", - "dunce", - "glob", - "percent-encoding", - "schemars 0.8.22", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "tauri-utils", - "thiserror 2.0.17", - "toml 0.9.7", - "url", -] - -[[package]] -name = "tauri-plugin-log" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c1438bc7662acd16d508c919b3c087efd63669a4c75625dff829b1c75975ec" -dependencies = [ - "android_logger", - "byte-unit", - "fern", - "log", - "objc2 0.6.2", - "objc2-foundation 0.3.1", - "serde", - "serde_json", - "serde_repr", - "swift-rs", - "tauri", - "tauri-plugin", - "thiserror 2.0.17", - "time", -] - -[[package]] -name = "tauri-runtime" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3367f0b47df90e9195cd9f04a56b0055a2cba45aa11923c6c253d748778176fc" -dependencies = [ - "cookie", - "dpi", - "gtk", - "http", - "jni", - "objc2 0.6.2", - "objc2-ui-kit", - "objc2-web-kit", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.17", - "url", - "webkit2gtk", - "webview2-com", - "windows", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d91d29ca680c545364cf75ba2f2e3c7ea2ab6376bfa3be26b56fa2463a5b5e" -dependencies = [ - "gtk", - "http", - "jni", - "log", - "objc2 0.6.2", - "objc2-app-kit", - "objc2-foundation 0.3.1", - "once_cell", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows", - "wry", -] - -[[package]] -name = "tauri-utils" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor", - "dunce", - "glob", - "html5ever", - "http", - "infer", - "json-patch", - "kuchikiki", - "log", - "memchr", - "phf 0.11.3", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.17", - "toml 0.9.7", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" -dependencies = [ - "embed-resource", - "toml 0.9.7", -] - -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix", - "windows-sys 0.61.1", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2", - "tokio-macros", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tokio-util" -version = "0.7.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" -dependencies = [ - "indexmap 2.11.4", - "serde_core", - "serde_spanned 1.0.2", - "toml_datetime 0.7.2", - "toml_parser", - "toml_writer", - "winnow 0.7.13", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap 2.11.4", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.23.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" -dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.7.2", - "toml_parser", - "winnow 0.7.13", -] - -[[package]] -name = "toml_parser" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" -dependencies = [ - "winnow 0.7.13", -] - -[[package]] -name = "toml_writer" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.4", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tray-icon" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" -dependencies = [ - "crossbeam-channel", - "dirs", - "libappindicator", - "muda", - "objc2 0.6.2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.1", - "once_cell", - "png", - "serde", - "thiserror 2.0.17", - "windows-sys 0.59.0", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset", - "tempfile", - "winapi", -] - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-ident" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlpattern" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" -dependencies = [ - "regex", - "serde", - "unic-ucd-ident", - "url", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8-width" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "getrandom 0.3.3", - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "value-bag" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wayland-backend" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" -dependencies = [ - "cc", - "downcast-rs", - "rustix", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" -dependencies = [ - "bitflags 2.9.4", - "rustix", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" -dependencies = [ - "bitflags 2.9.4", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" -dependencies = [ - "proc-macro2", - "quick-xml 0.37.5", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" -dependencies = [ - "dlib", - "log", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webview2-com" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows", - "windows-core 0.61.2", - "windows-implement", - "windows-interface", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "webview2-com-sys" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" -dependencies = [ - "thiserror 2.0.17", - "windows", - "windows-core 0.61.2", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.1", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window-vibrancy" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" -dependencies = [ - "objc2 0.6.2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation 0.3.1", - "raw-window-handle", - "windows-sys 0.59.0", - "windows-version", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "windows-interface" -version = "0.59.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" -dependencies = [ - "windows-link 0.2.0", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" -dependencies = [ - "windows-link 0.2.0", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.4", -] - -[[package]] -name = "windows-sys" -version = "0.61.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" -dependencies = [ - "windows-link 0.2.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" -dependencies = [ - "windows-link 0.2.0", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-version" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "700dad7c058606087f6fdc1f88da5841e06da40334413c6cd4367b25ef26d24e" -dependencies = [ - "windows-link 0.2.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "wry" -version = "0.53.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6" -dependencies = [ - "base64 0.22.1", - "block2 0.6.1", - "cookie", - "crossbeam-channel", - "dirs", - "dpi", - "dunce", - "gdkx11", - "gtk", - "html5ever", - "http", - "javascriptcore-rs", - "jni", - "kuchikiki", - "libc", - "ndk", - "objc2 0.6.2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation 0.3.1", - "objc2-ui-kit", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.17", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "xml-rs" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - -[[package]] -name = "zbus" -version = "5.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" -dependencies = [ - "async-broadcast", - "async-recursion", - "async-trait", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "nix", - "ordered-stream", - "serde", - "serde_repr", - "tokio", - "tracing", - "uds_windows", - "windows-sys 0.60.2", - "winnow 0.7.13", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.106", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" -dependencies = [ - "serde", - "static_assertions", - "winnow 0.7.13", - "zvariant", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "zip" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" -dependencies = [ - "aes", - "arbitrary", - "bzip2", - "constant_time_eq", - "crc32fast", - "deflate64", - "flate2", - "getrandom 0.3.3", - "hmac", - "indexmap 2.11.4", - "lzma-rust2", - "memchr", - "pbkdf2", - "ppmd-rust", - "sha1", - "time", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zlib-rs" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" - -[[package]] -name = "zopfli" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "zvariant" -version = "5.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" -dependencies = [ - "endi", - "enumflags2", - "serde", - "url", - "winnow 0.7.13", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.106", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.106", - "winnow 0.7.13", -] diff --git a/src-tauri/calibre_db/Cargo.lock b/src-tauri/calibre_db/Cargo.lock deleted file mode 100644 index 5cc8f1c..0000000 --- a/src-tauri/calibre_db/Cargo.lock +++ /dev/null @@ -1,893 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "calibre_db" -version = "0.1.0" -dependencies = [ - "chrono", - "futures", - "rusqlite", - "serde", - "serde_json", - "tempfile", - "thiserror", - "tokio", -] - -[[package]] -name = "cc" -version = "1.2.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bbf3b3619004ad9bd139f62a9ab5cfe467f307455a0d307b0cf58bf070feaa" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.177" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" - -[[package]] -name = "libsqlite3-sys" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "mio" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "rusqlite" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" -dependencies = [ - "bitflags", - "chrono", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "syn" -version = "2.0.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio" -version = "1.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "unicode-ident" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn", -]