From 8c494958652859edb41e256daa2e113df992a26d Mon Sep 17 00:00:00 2001 From: rizary Date: Tue, 8 Jul 2025 14:57:51 +0700 Subject: [PATCH] initial hunting-ground --- CONTRIBUTING.md | 453 ++++++++ Cargo.lock | 596 +++++------ Cargo.toml | 173 +++- README.md | 193 ++-- build.rs | 8 +- justfile | 59 ++ src/config.rs | 37 + src/error/app.rs | 22 + src/error/mod.rs | 13 + src/handler/mod.rs | 19 + src/handler/token.rs | 284 +++++ src/handler/ui.rs | 99 ++ src/lib.rs | 363 ++++++- src/main.rs | 143 +-- src/model/cex.rs | 612 +++++++++++ src/model/graph.rs | 121 +++ src/model/mod.rs | 3 + src/model/token.rs | 48 + src/storage/mod.rs | 26 + src/storage/redis.rs | 73 ++ src/task/mod.rs | 3 + src/task/shutdown.rs | 33 + src/task/subscriber.rs | 186 ++++ src/task/ui.rs | 147 +++ src/tracing/filter.rs | 73 ++ src/tracing/format.rs | 60 ++ src/tracing/mod.rs | 187 ++++ ui/app/index.slint | 485 ++++----- ui/index.slint | 139 ++- ui/pages/auth/ui/login-page.slint | 252 +++-- ui/pages/dashboard/ui/dashboard-page.slint | 491 ++------- .../ui/hunting-ground-page.slint | 86 +- ui/pages/markets/ui/markets-page.slint | 107 +- ui/pages/portfolio/ui/portfolio-page.slint | 106 +- ui/pages/trading/ui/trading-page.slint | 146 ++- ui/shared/config/index.slint | 2 +- .../components/design-system.slint | 971 ------------------ ui/shared/design-system/index.slint | 8 - ui/shared/design-system/tokens/theme.slint | 29 - ui/shared/index.slint | 2 +- ui/shared/lib/index.slint | 2 +- ui/shared/types/token.slint | 43 + ui/shared/ui/button/button.slint | 50 - ui/shared/ui/button/index.slint | 2 - ui/shared/ui/card/card.slint | 27 - ui/shared/ui/card/index.slint | 2 - ui/shared/ui/index.slint | 8 +- ui/shared/ui/input/index.slint | 2 - ui/shared/ui/input/input.slint | 20 - ui/shared/ui/layout/index.slint | 2 - ui/shared/ui/layout/layout.slint | 32 - ui/shared/ui/loading/index.slint | 2 - ui/shared/ui/loading/loading.slint | 190 +++- .../navigation/ui/navigation-widget.slint | 478 ++++++--- ui/widgets/navigation/ui/sidebar.slint | 178 ---- .../ui/analysis-complete-column.slint | 193 ++++ .../token-column/ui/cex-tokens-column.slint | 193 ++++ ui/widgets/token-column/ui/mod.slint | 6 + .../token-column/ui/new-tokens-column.slint | 193 ++++ ui/widgets/window-controls/index.slint | 2 +- ui/widgets/window-controls/ui/title-bar.slint | 232 +++-- .../ui/window-controls-widget.slint | 18 - 62 files changed, 5624 insertions(+), 3109 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 justfile create mode 100644 src/config.rs create mode 100644 src/error/app.rs create mode 100644 src/error/mod.rs create mode 100644 src/handler/mod.rs create mode 100644 src/handler/token.rs create mode 100644 src/handler/ui.rs create mode 100644 src/model/cex.rs create mode 100644 src/model/graph.rs create mode 100644 src/model/mod.rs create mode 100644 src/model/token.rs create mode 100644 src/storage/mod.rs create mode 100644 src/storage/redis.rs create mode 100644 src/task/mod.rs create mode 100644 src/task/shutdown.rs create mode 100644 src/task/subscriber.rs create mode 100644 src/task/ui.rs create mode 100644 src/tracing/filter.rs create mode 100644 src/tracing/format.rs create mode 100644 src/tracing/mod.rs delete mode 100644 ui/shared/design-system/components/design-system.slint delete mode 100644 ui/shared/design-system/index.slint delete mode 100644 ui/shared/design-system/tokens/theme.slint create mode 100644 ui/shared/types/token.slint delete mode 100644 ui/shared/ui/button/button.slint delete mode 100644 ui/shared/ui/button/index.slint delete mode 100644 ui/shared/ui/card/card.slint delete mode 100644 ui/shared/ui/card/index.slint delete mode 100644 ui/shared/ui/input/index.slint delete mode 100644 ui/shared/ui/input/input.slint delete mode 100644 ui/shared/ui/layout/index.slint delete mode 100644 ui/shared/ui/layout/layout.slint delete mode 100644 ui/shared/ui/loading/index.slint delete mode 100644 ui/widgets/navigation/ui/sidebar.slint create mode 100644 ui/widgets/token-column/ui/analysis-complete-column.slint create mode 100644 ui/widgets/token-column/ui/cex-tokens-column.slint create mode 100644 ui/widgets/token-column/ui/mod.slint create mode 100644 ui/widgets/token-column/ui/new-tokens-column.slint delete mode 100644 ui/widgets/window-controls/ui/window-controls-widget.slint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0a8b5fb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,453 @@ +# Contributing to Ziya-Slint + +Thank you for your interest in contributing to Ziya-Slint! This guide outlines our development approach, coding standards, and best practices for the desktop trading application. + +## Project Overview + +Ziya-Slint is a desktop trading application built with Rust and Slint UI framework, designed for cryptocurrency trading with real-time market data and portfolio management. + +## Development Philosophy + +We follow these core principles: + +1. **KISS Principle** - Keep code simple and straightforward +2. **Explicit Error Handling** - Never use dummy types or unfinished code +3. **Real-world Ready** - All code should be production-ready +4. **Async-first** - Prefer tokio over std for async operations +5. **Type Safety** - Leverage Rust's type system for correctness +6. **Desktop UX** - Native desktop app behavior and performance + +## Getting Started + +### Prerequisites + +- Rust 2024 Edition (see `rust-toolchain.toml`) +- Just command runner (for development tasks) +- Redis (for data caching) +- Git and Git-cliff (for development workflow) + +### Initial Setup + +1. Clone the repository +2. Copy configuration files: + ```bash + cp Config.example.toml Config.toml + ``` +3. Install development dependencies: + ```bash + just install-deps + ``` +4. Verify setup: + ```bash + just test + just clippy + ``` + +### Development Workflow + +We use `just` for common development tasks: + +```bash +# List all available commands +just + +# Development with hot reloading +just dev + +# Building +just build # Build with dev features +just build-prod # Build with prod features + +# Quality assurance +just test # Run tests +just clippy # Run linter +just fmt # Format code +just check # Check code for errors +just clean # Clean build artifacts +``` + +## Coding Standards + +### Error Handling + +We use strict error handling patterns. **Never** use: +- Dummy types or results +- Placeholder implementations +- TODO comments in production code + +**Always** use our error handling pattern: + +```rust +use crate::err_with_loc; +use crate::error::Result; + +// Import the Result type +use crate::error::Result; + +// Use map_err before ? operator +let result = some_operation() + .map_err(|e| { + error!("operation_failed: {}", e); + err_with_loc!(AppError::OperationFailed(format!("operation_failed: {}", e))) + })?; +``` + +### Async Programming + +- **Use tokio** instead of std for async operations +- Follow the actors pattern from [this guide](https://ryhl.io/blog/actors-with-tokio/) +- **Never** use `tokio::spawn` with UI components directly +- **Always** use `slint::invoke_from_event_loop` for UI updates from background tasks + +### Code Formatting + +We use `rustfmt` with specific configuration (see `rustfmt.toml`): + +- Max width: 120 characters +- Use field init shorthand +- Reorder imports and impl items +- Group imports by StdExternalCrate +- Prefer same line braces + +Run formatting: +```bash +just fmt +``` + +## Architecture: Feature-Sliced Design (FSD) + +We follow **Feature-Sliced Design (FSD)** methodology for frontend architecture: + +### FSD Layer Structure +``` +ui/ +├── app/ # Application layer - root, global setup +├── pages/ # Page layer - complete screens +├── widgets/ # Widget layer - composite UI blocks +├── entities/ # Entity layer - business entities +├── features/ # Feature layer - user interactions +└── shared/ # Shared layer - reusable resources + ├── ui/ # UI components + ├── types/ # Type definitions + └── design-system/ # Theme, tokens, etc. +``` + +### FSD Rules & Guidelines + +1. **Import Rule**: Higher layers can only import from lower layers + - ✅ `app/` → `pages/` → `widgets/` → `entities/` → `features/` → `shared/` + - ❌ Never import from higher layers + +2. **Layer Responsibilities**: + - **App**: Global providers, routing, application setup + - **Pages**: Complete screens, page-level logic + - **Widgets**: Composite UI blocks (navigation, forms, cards) + - **Entities**: Business entities (token, user, market data) + - **Features**: User interactions (login, trading, portfolio management) + - **Shared**: Reusable resources (UI kit, utilities, constants) + +3. **Slicing by Features**: Each feature should be self-contained + ``` + features/ + ├── authentication/ + │ ├── ui/ # Login components + │ ├── model/ # Auth state + │ └── api/ # Auth API calls + └── trading/ + ├── ui/ # Trading components + ├── model/ # Trading state + └── api/ # Trading API calls + ``` + +4. **Component Composition**: Build from bottom-up + ```slint + // shared/ui/button/ + export component Button { /* base button */ } + + // widgets/navigation/ + import { Button } from "../../shared/ui/button/"; + export component NavigationWidget { + Button { /* composed navigation */ } + } + + // pages/dashboard/ + import { NavigationWidget } from "../../widgets/navigation/"; + export component Dashboard { + NavigationWidget { /* page composition */ } + } + ``` + +5. **State Management**: Follow unidirectional data flow + - Global state in `app/` layer + - Feature state in respective `features/` slices + - Pass data down, emit events up + +6. **Design System**: Centralized in `shared/design-system/` + - Theme tokens and variables + - Consistent spacing, colors, typography + - Reusable UI components + +## Slint UI Development + +### Threading & State Management + +**Critical Pattern for Slint + Tokio:** +```rust +// Background work in Rust +tokio::spawn(async move { + let result = do_background_work().await; + + // Update UI from main thread following FSD data flow + let _ = slint::invoke_from_event_loop(move || { + if let Some(ui) = ui_weak.upgrade() { + // Update flows from app → pages → widgets → shared + ui.update_global_state(result); + } + }); +}); + ``` + +### Component Development Guidelines + +1. **Start with Shared Layer**: Build reusable components first + ```slint + // shared/ui/loading/loading.slint + export component LoadingSpinner { + // Base loading component + } + ``` + +2. **Compose in Widgets**: Create business-specific blocks + ```slint + // widgets/token-card/index.slint + import { LoadingSpinner } from "../../shared/ui/loading/"; + export component TokenCard { + LoadingSpinner { /* token-specific loading */ } + } + ``` + +3. **Integrate in Pages**: Assemble complete screens + ```slint + // pages/dashboard/index.slint + import { TokenCard } from "../../widgets/token-card/"; + export component Dashboard { + TokenCard { /* dashboard context */ } + } + ``` + +4. **Wire in App**: Handle global state and routing + ```slint + // app/index.slint + import { Dashboard } from "../pages/dashboard/"; + export component App { + if current-page == "dashboard": Dashboard { } + } + ``` + +### File Naming Conventions + +- Use `index.slint` for main component exports +- Use kebab-case for file names: `token-card.slint` +- Use PascalCase for component names: `TokenCard` + +### Property & Callback Flow + +- Properties flow down: `app → pages → widgets → shared` +- Callbacks flow up: `shared → widgets → pages → app` +- Keep callback interfaces simple and focused + +## FSD Best Practices + +### ✅ Do: +- Keep components focused on single responsibility +- Use semantic component names that reflect business domain +- Extract common patterns to `shared/` layer +- Implement loading and error states consistently +- Use TypeScript-like typing through Slint properties +- Follow consistent import paths relative to FSD structure + +### ❌ Don't: +- Import from higher layers (breaks FSD hierarchy) +- Put business logic directly in UI components +- Create circular dependencies between features +- Hardcode values that should be in design system +- Mix concerns (UI logic with business logic) +- Create deep nesting beyond FSD layers + +### Common Anti-patterns to Avoid: +```slint +// ❌ BAD: Widget importing from pages +// widgets/header/index.slint +import { Dashboard } from "../../pages/dashboard/"; // Wrong! + +// ❌ BAD: Shared component with business logic +// shared/ui/button/index.slint +export component Button { + // Don't put trading logic here! + clicked => { execute_trade(); } +} + +// ✅ GOOD: Proper callback delegation +// shared/ui/button/index.slint +export component Button { + callback clicked(); +} + +// ✅ GOOD: Business logic in appropriate layer +// features/trading/ui/trade-button.slint +import { Button } from "../../../shared/ui/button/"; +export component TradeButton { + Button { + clicked => { handle_trade_click(); } + } + } + ``` + +## Testing + +### Unit Tests + +```bash +just test +``` + +### Integration Tests + +- Test UI components with mock data +- Test service layer integration +- Use feature flags for test environments + +### Feature Testing + +```bash +# Run with specific features +cargo test --features dev +cargo test --features prod +``` + +## UI/UX Principles + +We follow comprehensive usability guidelines: + +1. **10 Usability Heuristics** - Nielsen's principles +2. **Gestalt Principles** - Visual hierarchy and grouping +3. **Accessibility** - Support for different user needs +4. **Simplicity** - Minimize cognitive load +5. **Desktop Patterns** - Native desktop app behavior + +### Key Requirements: +- Responsive design within window constraints (1080x800 - 1920x1080) +- Proper error messaging in plain language +- Loading states with progress indication +- Keyboard navigation support +- Native desktop interactions + +## Submission Guidelines + +### Branch Naming + +Use descriptive names: +- `feat/shared/button-component` - New shared component +- `feat/pages/dashboard-redesign` - Page-level changes +- `feat/features/trading-flow` - Feature implementation +- `fix/widgets/token-card-loading` - Bug fixes +- `refactor/shared/design-system` - Code improvements + +### Commit Messages + +Be descriptive: +``` +feat(shared): add loading spinner component + +- Implement reusable loading spinner in shared/ui +- Add animation and theme support +- Export from shared layer for use in widgets + +fix(pages): resolve dashboard layout overflow + +- Fix token card grid overflow in dashboard +- Improve responsive behavior for small windows +- Add proper scrolling for token list +``` + +### Code Review Checklist + +**General:** +- [ ] Follows error handling patterns +- [ ] Uses tokio for async operations +- [ ] Includes proper logging +- [ ] Formatted with rustfmt +- [ ] Passes all tests +- [ ] Updates documentation if needed + +**FSD-Specific:** +- [ ] Components placed in correct FSD layer +- [ ] Import dependencies only from lower layers +- [ ] Properties flow down, callbacks flow up +- [ ] No direct business logic in UI components +- [ ] Reusable components in `shared/` layer +- [ ] Feature-specific logic in `features/` layer + +**UI/UX:** +- [ ] Follows design system patterns +- [ ] Implements proper loading states +- [ ] Handles error states gracefully +- [ ] Supports keyboard navigation +- [ ] Responsive within window constraints + +## Development Environment + +### Recommended Tools + +- **IDE**: VS Code with rust-analyzer and Slint extension +- **Debugging**: Use tracing for structured logging +- **File Watching**: `watchexec` for auto-reload during development +- **Hot Reloading**: `just dev` for development workflow + +### Common Issues + +#### Slint Compilation Errors + +If you encounter field offset errors with watchexec: +```bash +cargo clean +# Then rebuild +``` + +#### Threading Issues + +Remember: Slint UI components are not `Send`. Always use `slint::invoke_from_event_loop` for UI updates from background tasks. + +#### Hot Reloading + +For the best development experience: + ```bash +just dev +``` + +This watches both `src/` and `ui/` directories and automatically rebuilds. + +## Documentation + +- **Component APIs**: Document with clear examples +- **Architecture**: Update this guide when patterns change +- **Configuration**: Document all config options +- **UI Components**: Maintain design system documentation + +## Getting Help + +1. Check existing documentation +2. Search closed issues on GitHub +3. Ask in project discussions +4. Check Slint documentation for UI issues + +## Code of Conduct + +- Be respectful and inclusive +- Focus on constructive feedback +- Help others learn and grow +- Maintain professional communication + +--- + +By following these guidelines, you help maintain code quality and ensure smooth collaboration on the Ziya-Slint desktop application. Thank you for contributing! 🚀 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index b51b57f..0a5afe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -434,6 +434,19 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bab94bde396a3f7b4962e396fdad640e241ed797d4d8d77fc8c237d14c58fc0" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-executor" version = "1.13.2" @@ -678,6 +691,27 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bb8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d8b8e1a22743d9241575c6ba822cf9c8fef34771c86ab7e477a4fbfd254e5" +dependencies = [ + "futures-util", + "parking_lot", + "tokio", +] + +[[package]] +name = "bb8-redis" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5143936af5e1eea1a881e3e3d21b6777da6315e5e307bc3d0c2301c44fa37da9" +dependencies = [ + "bb8", + "redis", +] + [[package]] name = "bincode" version = "1.3.3" @@ -1154,7 +1188,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", + "futures-core", "memchr", + "pin-project-lite", + "tokio", + "tokio-util", ] [[package]] @@ -1556,6 +1594,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "derivation-path" version = "0.2.0" @@ -1768,15 +1815,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "endi" version = "1.1.0" @@ -2023,6 +2061,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.2" @@ -2429,25 +2473,6 @@ dependencies = [ "gl_generator", ] -[[package]] -name = "h2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "half" version = "2.6.0" @@ -2548,109 +2573,12 @@ dependencies = [ "hmac 0.8.1", ] -[[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 = "humantime" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "libc", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - [[package]] name = "i-slint-backend-linuxkms" version = "1.12.1" @@ -3152,12 +3080,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -3547,6 +3469,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -3590,12 +3521,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3642,23 +3567,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "ndk" version = "0.9.0" @@ -3724,6 +3632,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3734,6 +3652,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.2" @@ -4187,12 +4111,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-sys" version = "0.9.109" @@ -4224,6 +4142,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owned_ttf_parser" version = "0.25.0" @@ -4292,6 +4216,19 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.4", + "indexmap", + "serde", + "serde_derive", +] + [[package]] name = "phf" version = "0.11.3" @@ -4455,6 +4392,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4748,6 +4691,28 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redis" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fd3fd5bb3a9a48819ae5cbad501968f208c1abd139649e7e2cf5d7f4b40b" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -4774,8 +4739,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -4786,57 +4760,21 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "reqwest" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-tls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - [[package]] name = "resvg" version = "0.45.1" @@ -4934,24 +4872,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "zeroize", -] - [[package]] name = "rustversion" version = "1.0.21" @@ -4991,15 +4911,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "scoped-tls" version = "1.0.1" @@ -5031,29 +4942,6 @@ dependencies = [ "tiny-skia", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.26" @@ -5130,18 +5018,6 @@ dependencies = [ "serde", ] -[[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.13.0" @@ -5165,6 +5041,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.9.9" @@ -5199,6 +5081,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -6969,12 +6860,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "synstructure" version = "0.13.2" @@ -6995,27 +6880,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "system-deps" version = "6.2.2" @@ -7114,6 +6978,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiff" version = "0.9.1" @@ -7125,6 +6998,37 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -7219,12 +7123,13 @@ dependencies = [ ] [[package]] -name = "tokio-native-tls" -version = "0.3.1" +name = "tokio-stream" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ - "native-tls", + "futures-core", + "pin-project-lite", "tokio", ] @@ -7291,12 +7196,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[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" @@ -7309,6 +7208,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.30" @@ -7327,13 +7238,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", ] [[package]] -name = "try-lock" -version = "0.2.5" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] [[package]] name = "ttf-parser" @@ -7526,6 +7461,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -7577,15 +7518,6 @@ dependencies = [ "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" @@ -8303,16 +8235,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wio" version = "0.2.2" @@ -8653,23 +8575,37 @@ dependencies = [ ] [[package]] -name = "ziya-slint" -version = "0.2.0" +name = "ziya" +version = "0.1.0" dependencies = [ "anyhow", + "async-compat", + "bb8", + "bb8-redis", + "bs58", "chrono", "chrono-tz", "env_logger 0.11.8", + "futures-util", "i-slint-backend-winit", - "image", "log", - "reqwest", + "petgraph", + "redis", "serde", "serde_json", "slint", "slint-build", + "solana-pubkey", "solana-sdk", + "thiserror 2.0.12", "tokio", + "tokio-stream", + "tokio-util", + "toml 0.8.23", + "tracing", + "tracing-appender", + "tracing-subscriber", + "winit", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 799f0f4..9c2aa6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,182 @@ [package] -name = "ziya-slint" -version = "0.2.0" +name = "ziya" +version = "0.1.0" edition = "2021" description = "One stop shop for your trading habit - Slint version" authors = ["rizary"] license = "MIT" +[lib] +name = "ziya" + [dependencies] -slint = "1.12.0" +slint = "1.8.0" i-slint-backend-winit = "1.12.0" +winit = "0.30" tokio = { version = "1.0", features = ["full"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -reqwest = { version = "0.12", features = ["json"] } +tokio-util = "0.7.13" +async-compat = "0.2.4" anyhow = "1.0" log = "0.4" env_logger = "0.11" -solana-sdk = "2.3.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2" chrono = { version = "0.4.39", features = ["serde"] } chrono-tz = "0.10.3" -image = { version = "0.25.6", default-features = false, features = [ "ico", "jpeg", "gif", "webp", "tiff" ] } +redis = { version = "0.32.2", features = ["aio", "tokio-comp"] } +bb8 = "0.9" +bb8-redis = "0.24.0" +solana-sdk = "2.3.0" +solana-pubkey = "2.3.0" +thiserror = "2.0.12" +futures-util = "0.3" +tokio-stream = "0.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +toml = "0.8" +bs58 = "0.5" +petgraph = { version = "0.8.1", features = ["serde-1"] } [build-dependencies] slint-build = "1.12.0" + +[features] +default = ["prod"] +dev = [] +prod = [] +deep-trace = [] + +# Config for 'git cliff' +# Run with `GITHUB_TOKEN=$(gh auth token) git cliff --bump -up CHANGELOG.md` +# https://git-cliff.org/docs/configuration +[workspace.metadata.git-cliff.bump] +features_always_bump_minor = false +breaking_always_bump_major = false + +[workspace.metadata.git-cliff.remote.github] +owner = "rizilab" +repo = "ziya" + +[workspace.metadata.git-cliff.changelog] +# changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% if version -%} + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else -%} + ## [Unreleased] +{% endif -%} + +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {%- for commit in commits %} + - {% if commit.breaking %}**BREAKING** {% endif -%} + {% if commit.scope %}*({{ commit.scope }})* {% endif -%} + {{ commit.message | trim | upper_first }}\ + {% if commit.github.username and commit.github.username != "rizary" %} by \ + [@{{ commit.github.username }}](https://github.com/{{ commit.github.username }})\ + {%- endif -%} + {% if commit.github.pr_number %} in \ + [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }})\ + {%- endif -%}. + {%- set fixes = commit.footers | filter(attribute="token", value="Fixes") -%} + {%- set closes = commit.footers | filter(attribute="token", value="Closes") -%} + {% for footer in fixes | concat(with=closes) -%} + {%- set issue_number = footer.value | trim_start_matches(pat="#") %} \ + ([{{ footer.value }}]({{ self::remote_url() }}/issues/{{ issue_number }}))\ + {%- endfor -%} + {% if commit.body %} + {%- for section in commit.body | trim | split(pat="\n\n") %} + {% raw %} {% endraw %}- {{ section | replace(from="\n", to=" ") }} + {%- endfor -%} + {%- endif -%} + {% endfor %} +{% endfor %} + +{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} + ### New Contributors +{%- endif -%} + +{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} + - @{{ contributor.username }} made their first contribution + {%- if contributor.pr_number %} in \ + [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ + {%- endif %} +{%- endfor %}\n +""" +# template for the changelog footer +footer = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% for release in releases -%} + {% if release.version -%} + {% if release.previous.version -%} + [{{ release.version | trim_start_matches(pat="v") }}]: \ + {{ self::remote_url() }}/compare/{{ release.previous.version }}...{{ release.version }} + {% else -%} + {#- compare against the initial commit for the first version -#} + [{{ release.version | trim_start_matches(pat="v") }}]: \ + {{ self::remote_url() }}/compare/{{ release.commit_id }}...{{ release.version }} + {% endif -%} + {% else -%} + [Unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}...HEAD + {% endif -%} +{%- endfor -%} +""" +# remove the leading and trailing whitespace from the templates +trim = true +# postprocessors +postprocessors = [] + +[workspace.metadata.git-cliff.git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "Features" }, + { body = ".*security", group = "Security" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^doc", group = "Documentation" }, + { message = "^test", group = "Tests" }, + { message = "^refactor", group = "Refactor" }, + { message = "^style", group = "Style" }, + { message = "^chore", group = "Miscellaneous" }, + { message = "^ci", default_scope = "ci", group = "Miscellaneous" }, + { message = "^release", skip = true }, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out the commits that are not matched by commit parsers +filter_commits = false +# regex for matching git tags +tag_pattern = "v[0-9].*" +# regex for skipping tags +skip_tags = "" +# regex for ignoring tags +ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" \ No newline at end of file diff --git a/README.md b/README.md index 8731c94..c08cf0c 100644 --- a/README.md +++ b/README.md @@ -7,149 +7,118 @@ ## Overview -This is a modern rewrite of the Ziya trading platform using [Slint](https://slint.dev/) for the UI and Rust for the backend logic. Slint provides a declarative UI language that makes it easy to create beautiful, native desktop applications. +Ziya is a modern, high-performance trading platform built with [Slint](https://slint.dev/) and Rust. Designed for cryptocurrency traders who demand speed, reliability, and a beautiful user interface. ## Features -- **Modern Native UI**: Built with Slint's declarative UI language -- **Fast Performance**: Native Rust performance with efficient rendering -- **Clean Architecture**: Separated UI and business logic -- **Async Operations**: Non-blocking token fetching and user management -- **Modular Design**: Well-organized codebase with clear separation of concerns +- **⚡ Lightning Fast**: Native Rust performance with instant startup +- **🎨 Beautiful UI**: Modern interface built with Slint's declarative language +- **📊 Real-time Data**: Live token prices and market analysis +- **🔒 Secure**: Type-safe Rust backend with robust error handling +- **📱 Native Feel**: True desktop application, not a web wrapper +- **🚀 Lightweight**: ~5MB standalone binary vs ~100MB Electron apps -### Application States +## Installation -- **Loading**: Initial app startup and initialization -- **Login**: User authentication interface -- **Dashboard**: Main trading dashboard with token search -- **Hunting Ground**: Advanced trading tools (placeholder) -- **Profile**: User profile and wallet information +### Download Pre-built Binaries -## Project Structure +Visit our [Releases](https://github.com/rizilab/ziya/releases) page to download the latest version for your platform: -``` -ziya-slint/ -├── src/ -│ ├── main.rs # Application entry point -│ ├── lib.rs # Module organization -│ ├── app_state.rs # Application state management -│ └── services/ # Business logic services -│ ├── mod.rs -│ ├── token_service.rs # Token data and search -│ └── user_service.rs # User authentication and profiles -├── ui/ -│ └── app-window.slint # Main UI definition -├── Cargo.toml # Project dependencies -└── build.rs # Build configuration -``` +- **Windows**: `ziya-x86_64-pc-windows-msvc.zip` +- **macOS**: `ziya-x86_64-apple-darwin.tar.xz` (Intel) or `ziya-aarch64-apple-darwin.tar.xz` (Apple Silicon) +- **Linux**: `ziya-x86_64-unknown-linux-gnu.tar.xz` -## Getting Started +### Build from Source -### Prerequisites - -- Rust 1.70+ (recommended via [rustup](https://rustup.rs/)) -- Git - -### Installation - -1. Clone the repository: - ```bash - git clone - cd ziya-slint - ``` - -2. Build and run: - ```bash - cargo run - ``` - -## Development - -### Running in Development Mode +If you have Rust installed: ```bash -# Run with debug logging -RUST_LOG=debug cargo run +git clone https://github.com/rizilab/ziya.git +cd ziya-slint +cargo install --path . ``` -### Building for Release +For detailed build instructions, see [CONTRIBUTING.md](CONTRIBUTING.md). -```bash -cargo build --release -``` +## Quick Start -## Architecture +1. **Launch Ziya**: Run the application +2. **Login**: Enter your credentials or create a new account +3. **Dashboard**: Browse and search tokens +4. **Trade**: Access advanced tools in the Hunting Ground -### UI Layer (Slint) +## Application Features -The UI is defined in `ui/app-window.slint` using Slint's declarative language. Key features: +### Dashboard +- **Token Search**: Find tokens by name, symbol, or address +- **Market Overview**: Real-time price data and trends +- **Portfolio**: Track your holdings and performance -- **State-based rendering**: Different UI states for loading, login, dashboard, etc. -- **Reactive properties**: Automatic UI updates when data changes -- **Component composition**: Reusable UI elements -- **Modern styling**: Clean, professional design +### Hunting Ground +- **Advanced Analysis**: Deep market insights and analytics +- **Trading Tools**: Professional-grade trading interface +- **Risk Management**: Built-in tools to manage trading risks -### Business Logic (Rust) +### Profile Management +- **Wallet Integration**: Connect and manage multiple wallets +- **Settings**: Customize your trading experience +- **Security**: Secure credential management -- **Services Pattern**: Separate services for different concerns -- **Async/Await**: Non-blocking operations for better UX -- **Error Handling**: Proper error propagation with `anyhow` -- **Type Safety**: Strong typing throughout the application +## Why Slint + Rust? -### Key Components +| Aspect | Traditional (Electron) | Ziya (Slint + Rust) | +|--------|----------------------|-------------------| +| **Memory Usage** | ~50MB+ | ~10MB | +| **Startup Time** | 2-5 seconds | Instant | +| **Bundle Size** | ~100MB | ~5MB | +| **Performance** | JavaScript V8 | Native machine code | +| **Security** | Web vulnerabilities | Memory-safe Rust | +| **Updates** | Large downloads | Efficient delta updates | -1. **TokenService**: Handles token data fetching and search -2. **UserService**: Manages user authentication and profiles -3. **AppState**: Central application state management -4. **Main**: Coordinates UI and business logic +## Support -## Comparison with Vue/Electron Version +- **Documentation**: [Full documentation](https://docs.ziya.trading) +- **Issues**: [GitHub Issues](https://github.com/rizilab/ziya/issues) +- **Discussions**: [GitHub Discussions](https://github.com/rizilab/ziya/discussions) +- **Community**: [Discord Server](https://discord.gg/bismillahdao) -| Aspect | Vue/Electron | Slint/Rust | -|--------|--------------|------------| -| **Performance** | ~50MB memory, slower startup | ~10MB memory, instant startup | -| **Bundle Size** | ~100MB with Node.js | ~5MB standalone binary | -| **Development** | Hot reload, web tools | Fast compilation, native debugging | -| **Deployment** | Requires Node.js runtime | Single self-contained binary | -| **UI Development** | HTML/CSS/JS knowledge required | Slint DSL (QML-like, simpler) | -| **Type Safety** | TypeScript (optional) | Full Rust type safety | +## Roadmap + +### Current Version (v0.2.0) +- ✅ Core trading dashboard +- ✅ Token search and filtering +- ✅ User authentication +- ✅ Basic portfolio tracking + +### Next Release (v0.3.0) +- 🚧 Real-time WebSocket data feeds +- 🚧 Advanced charting with TradingView +- 🚧 Multi-exchange support +- 🚧 Trading automation tools + +### Future Releases +- 📋 Mobile companion app +- 📋 DeFi protocol integration +- 📋 Social trading features +- 📋 Advanced risk analytics ## Contributing -This is part of the bismillahDAO ecosystem. When contributing: +We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for: -1. Follow Rust conventions and best practices -2. Keep UI logic in Slint files, business logic in Rust -3. Write tests for business logic -4. Update documentation for new features - -## Migration Status - -### ✅ Completed Features -- [x] Basic application structure -- [x] Loading, login, and dashboard states -- [x] Token list display and search -- [x] User profile management -- [x] Navigation between views -- [x] Mock data services - -### 🚧 In Progress -- [ ] Real API integration -- [ ] Advanced token analysis -- [ ] Wallet integration -- [ ] Trading features - -### 📋 Planned Features -- [ ] Real-time token price updates -- [ ] Advanced charting -- [ ] Portfolio tracking -- [ ] Trading automation -- [ ] Export/import functionality +- Development setup +- Code style guidelines +- Testing procedures +- Release process ## License -MIT License - see LICENSE file for details. +MIT License - see [LICENSE](LICENSE) file for details. ## About bismillahDAO -This project is created by bismillahDAO, focused on building ethical and innovative trading tools for the cryptocurrency community. +Ziya is proudly developed by [bismillahDAO](https://bismillahdao.org), building ethical and innovative tools for the cryptocurrency community. + +--- + +**⭐ Star this repository if you find Ziya useful!** diff --git a/build.rs b/build.rs index baece34..9a8c914 100644 --- a/build.rs +++ b/build.rs @@ -1,8 +1,6 @@ fn main() { // Use fluent-light as default, but we'll support dynamic switching - let config = slint_build::CompilerConfiguration::new() - .with_style("fluent-light".to_string()); - - slint_build::compile_with_config("ui/index.slint", config) - .expect("Slint build failed"); + let config = slint_build::CompilerConfiguration::new().with_style("fluent".to_string()); + + slint_build::compile_with_config("ui/index.slint", config).expect("Slint build failed"); } diff --git a/justfile b/justfile new file mode 100644 index 0000000..5378f7c --- /dev/null +++ b/justfile @@ -0,0 +1,59 @@ +# Default recipe to display available commands +default: + @just --list + +# Run the application with dev features +run: + cargo run --features dev + +# Run the application with prod features +run-prod: + cargo run --features prod + +# Build the application with dev features +build: + cargo build --features dev + +# Build the application with prod features +build-prod: + cargo build --features prod + +# Run tests +test: + cargo test + +# Run clippy linter +clippy: + cargo clippy --all-targets --all-features -- -D warnings + +# Format code +fmt: + cargo fmt + +# Check formatting +fmt-check: + cargo fmt --check + +# Run development server with hot reloading using watchexec +dev: + watchexec --clear --stop-timeout=3s -i "./**/target" -w "src" -w "ui" -e "rs,toml,slint" --project-origin "." -r "just run" + +# Generate changelog using git-cliff +changelog: + git-cliff -o CHANGELOG.md + +# Generate unreleased changelog +changelog-unreleased: + git-cliff --unreleased --tag unreleased -o CHANGELOG.md + +# Clean build artifacts +clean: + cargo clean + +# Check for security vulnerabilities +audit: + cargo audit + +# Install development dependencies +install-deps: + cargo install watchexec-cli git-cliff cargo-audit diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..fb8a662 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,37 @@ +use crate::err_with_loc; +use crate::error::app::AppError; +use crate::error::Result; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StorageRedisConfig { + pub host: String, + pub port: u16, + pub pool_size: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoggingConfig { + pub directory: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub storage_redis: StorageRedisConfig, + pub logging: LoggingConfig, +} + +pub async fn load_config(path: impl AsRef) -> Result { + let config_str = std::fs::read_to_string(path).map_err(|e| { + err_with_loc!(AppError::Config(format!( + "failed_to_read_config_file: {}", + e + ))) + })?; + + let config: Config = toml::from_str(&config_str) + .map_err(|e| err_with_loc!(AppError::Config(format!("failed_to_parse_config: {}", e))))?; + + Ok(config) +} diff --git a/src/error/app.rs b/src/error/app.rs new file mode 100644 index 0000000..4c7c97f --- /dev/null +++ b/src/error/app.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error("Redis error: {0}")] + Redis(String), + + #[error("Handler error: {0}")] + Handler(String), + + #[error("Slint error: {0}")] + Slint(String), + + #[error("Serialization error: {0}")] + Serialization(String), + + #[error("Configuration error: {0}")] + Config(String), + + #[error("UI error: {0}")] + UiError(String), +} diff --git a/src/error/mod.rs b/src/error/mod.rs new file mode 100644 index 0000000..94f7830 --- /dev/null +++ b/src/error/mod.rs @@ -0,0 +1,13 @@ +pub mod app; + +pub use anyhow::anyhow; +pub use anyhow::Context; +pub use anyhow::Error; +pub use anyhow::Result; + +#[macro_export] +macro_rules! err_with_loc { + ($err:expr) => { + anyhow::anyhow!($err).context(format!("at {}:{}", file!(), line!())) + }; +} diff --git a/src/handler/mod.rs b/src/handler/mod.rs new file mode 100644 index 0000000..a6b4b21 --- /dev/null +++ b/src/handler/mod.rs @@ -0,0 +1,19 @@ +pub mod token; +pub mod ui; + +use crate::model::token::{MaxDepthReachedData, NewTokenCreatedData, TokenCexUpdatedData}; + +// Messages that can be sent to the hunting ground handler +#[derive(Debug)] +pub enum TokenHandler { + NewToken { data: NewTokenCreatedData }, + CexUpdated { data: TokenCexUpdatedData }, + MaxDepthReached { data: MaxDepthReachedData }, +} + +#[derive(Debug)] +pub enum SlintHandler { + ClearNewTokens {}, + ClearCexTokens {}, + ClearAnalysisTokens {}, +} diff --git a/src/handler/token.rs b/src/handler/token.rs new file mode 100644 index 0000000..c9a4639 --- /dev/null +++ b/src/handler/token.rs @@ -0,0 +1,284 @@ +use chrono::TimeZone; +use chrono::Utc; +use chrono_tz::Asia::Jakarta; +use std::{collections::VecDeque, sync::Arc}; +use tokio::sync::{mpsc, oneshot}; +use tracing::{debug, error, info, warn}; + +use super::SlintHandler; +use super::TokenHandler; +use crate::error::Result; +use crate::model::token::{MaxDepthReachedData, NewTokenCreatedData, TokenCexUpdatedData}; +use crate::slint_ui::{CexUpdatedUiData, MainWindow, MaxDepthReachedUiData, NewTokenUiData}; +use crate::storage::StorageEngine; +use crate::task::shutdown::ShutdownSignal; +use slint::{Model, SharedString, Weak}; + +// Internal state for the hunting ground handler +pub struct TokenMetadataHandler { + pub receiver: mpsc::Receiver, + pub slint_tx: mpsc::Sender, + pub ui_weak: Weak, + pub shutdown: ShutdownSignal, +} + +impl TokenMetadataHandler { + pub fn new( + receiver: mpsc::Receiver, + slint_tx: mpsc::Sender, + ui_weak: Weak, + shutdown: ShutdownSignal, + ) -> Self { + Self { + receiver, + slint_tx, + ui_weak, + shutdown, + } + } + + pub async fn process_new_token_to_slint(&self, token: NewTokenCreatedData) { + info!("processing_new_token_to_slint: mint={}", token.mint.clone()); + let utc_timestamp = Utc.timestamp_millis_opt(token.created_at as i64).unwrap(); + let jakarta_timestamp = utc_timestamp.with_timezone(&chrono_tz::Asia::Jakarta); + let created_at = jakarta_timestamp.format("%Y-%m-%d %H:%M:%S"); + + let new_token_ui_data = NewTokenUiData { + mint: SharedString::from(token.mint.to_string()), + bonding_curve: token + .bonding_curve + .map(|bc| SharedString::from(bc.to_string())) + .unwrap_or(SharedString::from("")), + name: SharedString::from(token.name), + symbol: SharedString::from(token.symbol), + uri: SharedString::from(token.uri), + creator: SharedString::from(token.creator.to_string()), + created_at: SharedString::from(created_at.to_string()), + }; + + // ✅ Update UI using proper Slint threading model + let ui_weak_clone = self.ui_weak.clone(); + slint::invoke_from_event_loop(move || { + if let Some(ui) = ui_weak_clone.upgrade() { + // Convert ModelRc to Vec, add new item, and set back + let current_tokens = ui.get_new_tokens(); + let mut tokens_vec: Vec = current_tokens.iter().collect(); + tokens_vec.insert(0, new_token_ui_data); + ui.set_new_tokens(tokens_vec.as_slice().into()); + info!( + "process_new_token_to_slint::ui_updated::total_tokens={}", + tokens_vec.len() + ); + } else { + warn!("process_new_token_to_slint::ui_weak_reference_expired"); + } + }) + .unwrap_or_else(|e| { + error!( + "process_new_token_to_slint::failed_to_invoke_from_event_loop: {}", + e + ); + }); + } + + pub async fn process_cex_updated_to_slint(&self, token: TokenCexUpdatedData) { + info!("processing_cex_updated_to_slint: mint={}", token.mint); + + let utc_created_at = Utc.timestamp_millis_opt(token.created_at as i64).unwrap(); + let jakarta_created_at = utc_created_at.with_timezone(&chrono_tz::Asia::Jakarta); + let created_at = jakarta_created_at.format("%Y-%m-%d %H:%M:%S"); + + let utc_updated_at = Utc.timestamp_millis_opt(token.updated_at as i64).unwrap(); + let jakarta_updated_at = utc_updated_at.with_timezone(&chrono_tz::Asia::Jakarta); + let updated_at = jakarta_updated_at.format("%Y-%m-%d %H:%M:%S"); + + let cex_updated_ui_data = CexUpdatedUiData { + mint: SharedString::from(token.mint), + name: SharedString::from(token.name), + uri: SharedString::from(token.uri), + dev_name: SharedString::from(token.dev_name), + creator: SharedString::from(token.creator), + cex_name: SharedString::from(token.cex_name), + cex_address: SharedString::from(token.cex_address), + bonding_curve: SharedString::from(token.bonding_curve), + created_at: SharedString::from(created_at.to_string()), + updated_at: SharedString::from(updated_at.to_string()), + node_count: token.node_count as i32, + edge_count: token.edge_count as i32, + }; + + // ✅ Update UI using proper Slint threading model + let ui_weak_clone = self.ui_weak.clone(); + slint::invoke_from_event_loop(move || { + if let Some(ui) = ui_weak_clone.upgrade() { + // Convert ModelRc to Vec, add new item, and set back + let current_tokens = ui.get_cex_tokens(); + let mut tokens_vec: Vec = current_tokens.iter().collect(); + tokens_vec.insert(0, cex_updated_ui_data); + ui.set_cex_tokens(tokens_vec.as_slice().into()); + info!( + "process_cex_updated_to_slint::ui_updated::total_tokens={}", + tokens_vec.len() + ); + } else { + warn!("process_cex_updated_to_slint::ui_weak_reference_expired"); + } + }) + .unwrap_or_else(|e| { + error!( + "process_cex_updated_to_slint::failed_to_invoke_from_event_loop: {}", + e + ); + }); + } + + pub async fn process_max_depth_reached_to_slint(&self, token: MaxDepthReachedData) { + info!("processing_max_depth_reached_to_slint: mint={}", token.mint); + + let utc_created_at = Utc.timestamp_millis_opt(token.created_at as i64).unwrap(); + let jakarta_created_at = utc_created_at.with_timezone(&chrono_tz::Asia::Jakarta); + let created_at = jakarta_created_at.format("%Y-%m-%d %H:%M:%S"); + + let utc_updated_at = Utc.timestamp_millis_opt(token.updated_at as i64).unwrap(); + let jakarta_updated_at = utc_updated_at.with_timezone(&chrono_tz::Asia::Jakarta); + let updated_at = jakarta_updated_at.format("%Y-%m-%d %H:%M:%S"); + + let max_depth_ui_data = MaxDepthReachedUiData { + mint: SharedString::from(token.mint), + name: SharedString::from(token.name), + uri: SharedString::from(token.uri), + dev_name: SharedString::from(token.dev_name), + creator: SharedString::from(token.creator), + bonding_curve: SharedString::from(token.bonding_curve), + created_at: SharedString::from(created_at.to_string()), + updated_at: SharedString::from(updated_at.to_string()), + node_count: token.node_count as i32, + edge_count: token.edge_count as i32, + }; + + // ✅ Update UI using proper Slint threading model + let ui_weak_clone = self.ui_weak.clone(); + slint::invoke_from_event_loop(move || { + if let Some(ui) = ui_weak_clone.upgrade() { + // Convert ModelRc to Vec, add new item, and set back + let current_tokens = ui.get_analysis_tokens(); + let mut tokens_vec: Vec = current_tokens.iter().collect(); + tokens_vec.insert(0, max_depth_ui_data); + ui.set_analysis_tokens(tokens_vec.as_slice().into()); + info!( + "process_max_depth_reached_to_slint::ui_updated::total_tokens={}", + tokens_vec.len() + ); + } else { + warn!("process_max_depth_reached_to_slint::ui_weak_reference_expired"); + } + }) + .unwrap_or_else(|e| { + error!( + "process_max_depth_reached_to_slint::failed_to_invoke_from_event_loop: {}", + e + ); + }); + } +} + +async fn run_token_handler(mut token_metadata_handler: TokenMetadataHandler) { + debug!("token_handler::started"); + + loop { + tokio::select! { + Some(msg) = token_metadata_handler.receiver.recv() => { + match msg { + TokenHandler::NewToken { data } => { + info!("received_new_token: mint={}", data.mint); + token_metadata_handler.process_new_token_to_slint(data).await; + }, + TokenHandler::CexUpdated { data } => { + info!("received_cex_updated_token: mint={}", data.mint); + token_metadata_handler.process_cex_updated_to_slint(data).await; + }, + TokenHandler::MaxDepthReached { data } => { + info!("received_max_depth_reached_token: mint={}", data.mint); + token_metadata_handler.process_max_depth_reached_to_slint(data).await; + }, + } + }, + else => { + // Channel closed, exit gracefully + debug!("hunting_ground_handler_token::channel_closed::exiting"); + break; + } + } + } +} + +// Main hunting ground handler following the muhafidh actor pattern +#[derive(Clone)] +pub struct TokenMetadataHandlerOperator { + pub db: Arc, + pub sender: mpsc::Sender, + pub shutdown: ShutdownSignal, + pub ui_weak: Weak, +} + +impl TokenMetadataHandlerOperator { + pub fn new( + db: Arc, + shutdown: ShutdownSignal, + receiver: mpsc::Receiver, + sender: mpsc::Sender, + slint_tx: mpsc::Sender, + ui_weak: Weak, + ) -> Self { + let token_metadata_handler = + TokenMetadataHandler::new(receiver, slint_tx, ui_weak.clone(), shutdown.clone()); + + // Spawn the actor + tokio::spawn(run_token_handler(token_metadata_handler)); + Self { + db, + sender, + shutdown, + ui_weak, + } + } + + pub async fn process_new_token(&self, token: NewTokenCreatedData) -> Result<()> { + info!("processing_new_token: mint={}", token.mint.clone()); + if let Err(e) = self.sender.try_send(TokenHandler::NewToken { + data: token.clone(), + }) { + error!( + "failed_to_send_token_to_token_handler::mint::{}::error::{}", + token.mint, e + ); + } + Ok(()) + } + + pub async fn process_cex_updated(&self, token: TokenCexUpdatedData) -> Result<()> { + info!("processing_cex_updated: mint={}", token.mint); + if let Err(e) = self.sender.try_send(TokenHandler::CexUpdated { + data: token.clone(), + }) { + error!( + "failed_to_send_cex_updated_to_token_handler::mint::{}::error::{}", + token.mint, e + ); + } + Ok(()) + } + + pub async fn process_max_depth_reached(&self, token: MaxDepthReachedData) -> Result<()> { + info!("processing_max_depth_reached: mint={}", token.mint); + if let Err(e) = self.sender.try_send(TokenHandler::MaxDepthReached { + data: token.clone(), + }) { + error!( + "failed_to_send_max_depth_reached_to_token_handler::mint::{}::error::{}", + token.mint, e + ); + } + Ok(()) + } +} diff --git a/src/handler/ui.rs b/src/handler/ui.rs new file mode 100644 index 0000000..d97a2a0 --- /dev/null +++ b/src/handler/ui.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::{debug, error, info}; + +use super::SlintHandler; +use super::TokenHandler; +use crate::storage::StorageEngine; +use crate::task::shutdown::ShutdownSignal; + +// Internal state for the hunting ground handler +pub struct SlintHandlerUi { + pub receiver: mpsc::Receiver, + pub token_tx: mpsc::Sender, + pub shutdown: ShutdownSignal, +} + +impl SlintHandlerUi { + pub fn new( + receiver: mpsc::Receiver, + token_tx: mpsc::Sender, + shutdown: ShutdownSignal, + ) -> Self { + Self { + receiver, + token_tx, + shutdown, + } + } +} + +async fn run_slint_handler_ui(mut slint_handler_ui: SlintHandlerUi) { + debug!("slint_handler_ui::started"); + + loop { + tokio::select! { + Some(msg) = slint_handler_ui.receiver.recv() => { + match msg { + SlintHandler::ClearNewTokens { } => { + info!("received_clear_new_tokens"); + }, + SlintHandler::ClearCexTokens { } => { + info!("received_clear_cex_tokens"); + }, + SlintHandler::ClearAnalysisTokens { } => { + info!("received_clear_analysis_tokens"); + }, + } + }, + else => { + // Channel closed, exit gracefully + debug!("hunting_ground_handler_token::channel_closed::exiting"); + break; + } + } + } +} +// Main hunting ground handler following the muhafidh actor pattern +#[derive(Clone)] +pub struct SlintHandlerUiOperator { + pub db: Arc, + pub sender: mpsc::Sender, + pub shutdown: ShutdownSignal, +} + +impl SlintHandlerUiOperator { + pub fn new( + db: Arc, + shutdown: ShutdownSignal, + receiver: mpsc::Receiver, + sender: mpsc::Sender, + token_tx: mpsc::Sender, + ) -> Self { + let slint_handler_ui = SlintHandlerUi::new(receiver, token_tx, shutdown.clone()); + + // Spawn the actor + tokio::spawn(run_slint_handler_ui(slint_handler_ui)); + + Self { + db, + sender, + shutdown, + } + } + pub fn clear_new_tokens(&self) { + if let Err(e) = self.sender.try_send(SlintHandler::ClearNewTokens {}) { + error!("failed_to_send_clear_new_tokens::error::{}", e); + } + } + pub fn clear_cex_tokens(&self) { + if let Err(e) = self.sender.try_send(SlintHandler::ClearCexTokens {}) { + error!("failed_to_send_clear_cex_tokens::error::{}", e); + } + } + pub fn clear_analysis_tokens(&self) { + if let Err(e) = self.sender.try_send(SlintHandler::ClearAnalysisTokens {}) { + error!("failed_to_send_clear_analysis_tokens::error::{}", e); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index cb1a3bc..5e524ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,363 @@ // This lib.rs is currently not used since we're building a binary application -// If needed in the future, add modules here \ No newline at end of file +// If needed in the future, add modules here + +pub mod config; +pub mod error; +pub mod handler; +pub mod model; +pub mod storage; +pub mod task; +pub mod tracing; + +pub mod slint_ui { + slint::include_modules!(); +} + +// Re-export commonly used types +pub use config::Config; +pub use error::app::AppError; +pub use error::Result; +pub use slint::*; +pub use slint_ui::*; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; + +use handler::token::TokenMetadataHandlerOperator; +use handler::ui::SlintHandlerUiOperator; +use handler::SlintHandler; +use handler::TokenHandler; +use storage::StorageEngine; +use task::shutdown::ShutdownSignal; +use tracing::{error, info, warn}; + +// Application states +#[derive(Debug, Clone, PartialEq)] +pub enum AppState { + Loading, + Login, + Authenticated, +} + +// User session +#[derive(Debug, Clone)] +pub struct UserSession { + pub email: String, + pub authenticated_at: SystemTime, +} + +pub fn get_current_unix_timestamp() -> i32 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i32 +} + +// Simple email validation +pub fn is_valid_email(email: &str) -> bool { + email.contains('@') && email.contains('.') && email.len() > 5 +} + +// Health check function +pub async fn check_backend_health(db: &StorageEngine) -> bool { + // Check Redis connection + match db.redis.pool.get().await { + Ok(mut conn) => { + match redis::cmd("PING").query_async::(&mut *conn).await { + Ok(_) => { + info!("Redis health check passed"); + true + } + Err(e) => { + warn!("Redis ping failed: {}", e); + false + } + } + } + Err(e) => { + warn!("Redis health check failed: {}", e); + false + } + } +} + +pub struct ZiyaApp { + pub db: Arc, + pub ui_weak: Weak, + pub shutdown: ShutdownSignal, + pub cancellation_token: CancellationToken, + pub slint_handler: Arc, + pub token_handler: Arc, + pub app_state: AppState, + pub user_session: Option, +} + +impl ZiyaApp { + pub async fn new(config: &Config, ui_weak: Weak) -> Result { + info!("Creating storage engine"); + let db = StorageEngine::new(config.clone()).await?; + let db = Arc::new(db); + + let shutdown_signal = ShutdownSignal::new(); + let cancellation_token = CancellationToken::new(); + + let (slint_tx, slint_rx) = mpsc::channel::(1000); + let (token_tx, token_rx) = mpsc::channel::(1000); + + let slint_handler = Arc::new(SlintHandlerUiOperator::new( + db.clone(), + shutdown_signal.clone(), + slint_rx, + slint_tx.clone(), + token_tx.clone(), + )); + + let token_handler = Arc::new(TokenMetadataHandlerOperator::new( + db.clone(), + shutdown_signal.clone(), + token_rx, + token_tx, + slint_tx, + ui_weak.clone(), + )); + + Ok(Self { + db, + ui_weak, + shutdown: shutdown_signal, + cancellation_token, + slint_handler, + token_handler, + app_state: AppState::Loading, + user_session: None, + }) + } + + // Initialize the UI with health check + pub fn init_ui(&self) -> Result<()> { + if let Some(ui) = self.ui_weak.upgrade() { + // Set initial state + ui.set_app_state("loading".into()); + ui.set_is_loading(true); + ui.set_has_connection_error(false); + ui.set_loading_status("Initializing your trading environment...".into()); + + // Set up health check callback + let db = self.db.clone(); + let ui_weak = self.ui_weak.clone(); + + ui.on_retry_health_check(move || { + let db = db.clone(); + let ui_weak_for_spawn = ui_weak.clone(); + let ui_weak_for_immediate = ui_weak.clone(); + + // Immediately update UI for loading state + if let Some(ui) = ui_weak_for_immediate.upgrade() { + ui.set_is_loading(true); + ui.set_has_connection_error(false); + ui.set_loading_status("Checking backend connection...".into()); + } + + // Spawn background task for health check + tokio::spawn(async move { + // Simulate loading time + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + + let health_result = check_backend_health(&db).await; + + // Update UI from event loop thread + let _ = slint::invoke_from_event_loop(move || { + if let Some(ui) = ui_weak_for_spawn.upgrade() { + if health_result { + info!("Health check passed, transitioning to login"); + ui.set_app_state("login".into()); + } else { + warn!("Health check failed"); + ui.set_has_connection_error(true); + ui.set_is_loading(false); + ui.set_loading_status("Connection failed".into()); + } + } + }); + }); + }); + + // Set up login callback + let ui_weak = self.ui_weak.clone(); + ui.on_login_attempt(move |email, _password| { + let ui_weak = ui_weak.clone(); + let email_str = email.to_string(); + + info!("Login attempt for email: {}", email_str); + + if let Some(ui) = ui_weak.upgrade() { + if is_valid_email(&email_str) { + ui.set_user_email(email.into()); + ui.set_app_state("authenticated".into()); + ui.set_is_authenticated(true); + info!("User authenticated successfully"); + } else { + warn!("Invalid email provided: {}", email_str); + // Handle login error in the UI + } + } + }); + + // Set up logout callback + let ui_weak = self.ui_weak.clone(); + ui.on_logout_requested(move || { + let ui_weak = ui_weak.clone(); + + if let Some(ui) = ui_weak.upgrade() { + info!("User logout requested"); + ui.set_user_email("".into()); + ui.set_app_state("login".into()); + ui.set_is_authenticated(false); + } + }); + + // Set up authenticate user callback (for demo mode) + let ui_weak = self.ui_weak.clone(); + ui.on_authenticate_user(move |email| { + let ui_weak = ui_weak.clone(); + let email_str = email.to_string(); + + if let Some(ui) = ui_weak.upgrade() { + info!("Demo authentication for: {}", email_str); + ui.set_user_email(email.into()); + ui.set_app_state("authenticated".into()); + ui.set_is_authenticated(true); + } + }); + + // Set up sidebar toggle + let ui_weak = self.ui_weak.clone(); + ui.on_toggle_sidebar(move || { + let ui_weak = ui_weak.clone(); + + if let Some(ui) = ui_weak.upgrade() { + let current_state = ui.get_sidebar_state(); + if current_state.as_str() == "full" { + ui.set_sidebar_state("icon-only".into()); + } else { + ui.set_sidebar_state("full".into()); + } + } + }); + + // Start initial health check + let db = self.db.clone(); + let ui_weak = self.ui_weak.clone(); + + tokio::spawn(async move { + // Initial delay to show loading screen + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + let health_result = check_backend_health(&db).await; + + // Update UI from event loop thread + let _ = slint::invoke_from_event_loop(move || { + if let Some(ui) = ui_weak.upgrade() { + ui.set_loading_status("Checking backend connection...".into()); + + if health_result { + info!("Initial health check passed"); + ui.set_app_state("login".into()); + } else { + warn!("Initial health check failed"); + ui.set_has_connection_error(true); + ui.set_is_loading(false); + ui.set_loading_status("Connection failed".into()); + } + } + }); + }); + + Ok(()) + } else { + Err(AppError::UiError("Failed to upgrade UI weak reference".into()).into()) + } + } + + pub async fn run(self) -> Result<()> { + info!("Starting Ziya Slint Application"); + + // Initialize UI callbacks + self.init_ui()?; + + let slint_handler = self.slint_handler.clone(); + let db = self.db.clone(); + + let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1); + + let ui_task = task::ui::spawn_ui_task( + slint_handler.clone(), + self.ui_weak.clone(), + self.cancellation_token.clone(), + shutdown_tx.clone(), + ); + + // ✅ Spawn all background subscriber tasks + let new_token_subscriber = task::subscriber::spawn_new_token_subscriber( + self.token_handler.clone(), + db.clone(), + self.cancellation_token.clone(), + ); + + let cex_updated_subscriber = task::subscriber::spawn_token_cex_updated_subscriber( + self.token_handler.clone(), + db.clone(), + self.cancellation_token.clone(), + ); + + let max_depth_subscriber = task::subscriber::spawn_max_depth_reached_subscriber( + self.token_handler.clone(), + db.clone(), + self.cancellation_token.clone(), + ); + + // ✅ Use the original tokio::select! pattern + tokio::select! { + result = ui_task => { + match result { + Ok(Ok(())) => info!("ui_task_completed_successfully"), + Ok(Err(e)) => error!("ui_task_error: {}", e), + Err(e) => error!("ui_task_join_error: {}", e), + } + }, + result = new_token_subscriber => { + match result { + Ok(Ok(())) => info!("new_token_subscriber_completed_successfully"), + Ok(Err(e)) => error!("new_token_subscriber_error: {}", e), + Err(e) => error!("new_token_subscriber_join_error: {}", e), + } + }, + result = cex_updated_subscriber => { + match result { + Ok(Ok(())) => info!("cex_updated_subscriber_completed_successfully"), + Ok(Err(e)) => error!("cex_updated_subscriber_error: {}", e), + Err(e) => error!("cex_updated_subscriber_join_error: {}", e), + } + }, + result = max_depth_subscriber => { + match result { + Ok(Ok(())) => info!("max_depth_subscriber_completed_successfully"), + Ok(Err(e)) => error!("max_depth_subscriber_error: {}", e), + Err(e) => error!("max_depth_subscriber_join_error: {}", e), + } + }, + _ = tokio::signal::ctrl_c() => { + info!("ctrl_c_received::initiating_shutdown"); + let _ = shutdown_tx.send(()).await; + }, + _ = shutdown_rx.recv() => { + info!("shutdown_signal_received"); + self.cancellation_token.cancel(); + }, + } + + info!("application_shutdown_complete"); + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index b025acb..2091743 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,119 +1,36 @@ // Prevent console window in addition to Slint window in Windows release builds when, e.g., starting the app via file manager. Ignored on other platforms. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use slint::*; -use i_slint_backend_winit::WinitWindowAccessor; +use ziya::config::load_config; +use ziya::error::Result; +use ziya::slint_ui::*; +use ziya::tracing::setup_tracing; +use ziya::ZiyaApp; -// Import the main window from our FSD-structured UI -slint::include_modules!(); +#[tokio::main] +async fn main() -> Result<()> { + // ✅ Create and show UI on main thread using Slint's built-in pattern + let ui = MainWindow::new() + .map_err(|e| ziya::error::app::AppError::Slint(format!("failed_to_create_ui: {}", e)))?; -fn main() -> Result<(), slint::PlatformError> { - // Create the main window - let ui = MainWindow::new()?; - - // Clone handle for callbacks - let ui_handle = ui.as_weak(); - - // Handle navigation changes - ui.on_navigation_changed({ - let ui_handle = ui_handle.clone(); - move |page| { - println!("Navigation changed to: {}", page); - if let Some(ui) = ui_handle.upgrade() { - // Update current page state - ui.set_current_page(page.into()); - } - } - }); - - // Handle theme toggle - using direct function call as per Slint discussion #5860 - ui.on_theme_toggle_clicked({ - let ui_handle = ui_handle.clone(); - move || { - println!("Theme toggle callback received from UI"); - if let Some(ui) = ui_handle.upgrade() { - // Call the public theme toggle function directly - ui.invoke_toggle_theme(); - println!("Theme toggle function invoked successfully"); - } - } - }); - - // Handle logout - ui.on_logout_clicked({ - let _ui_handle = ui_handle.clone(); - move || { - println!("Logout clicked"); - // Implement logout logic here - } - }); - - // Handle buy action - ui.on_buy_clicked({ - let _ui_handle = ui_handle.clone(); - move || { - println!("Buy clicked"); - // Implement buy logic here - } - }); - - // Handle sell action - ui.on_sell_clicked({ - let _ui_handle = ui_handle.clone(); - move || { - println!("Sell clicked"); - // Implement sell logic here - } - }); - - // Window control handlers - simplified drag handling - ui.on_start_drag_window({ - let ui_handle = ui_handle.clone(); - move || { - if let Some(ui) = ui_handle.upgrade() { - let window = ui.window(); - window.with_winit_window(|winit_window| { - if !window.is_maximized() { - let _ = winit_window.drag_window(); - } - }); - } - } - }); - - ui.on_minimize_window({ - let ui_handle = ui_handle.clone(); - move || { - if let Some(ui) = ui_handle.upgrade() { - let _ = ui.window().set_minimized(true); - } - } - }); - - ui.on_maximize_window({ - let ui_handle = ui_handle.clone(); - move || { - if let Some(ui) = ui_handle.upgrade() { - let window = ui.window(); - let _ = window.set_maximized(!window.is_maximized()); - } - } - }); - - ui.on_close_window({ - let ui_handle = ui_handle.clone(); - move || { - if let Some(ui) = ui_handle.upgrade() { - let _ = ui.window().hide(); - let _ = slint::quit_event_loop(); - } - } - }); - - // Set initial state - ui.set_current_page("Dashboard".into()); - ui.set_user_initials("JD".into()); - - // Run the application - ui.run() + // ✅ Set up async tasks using spawn_local with async-compat for Tokio futures + let ui_weak = ui.as_weak(); + // Load configuration + let config = load_config("Config.toml").await.unwrap(); + + // Initialize tracing with config + setup_tracing(config.clone(), "ziya-slint").await.unwrap(); + + // Create and run the app + let app = ZiyaApp::new(&config, ui_weak).await.unwrap(); + slint::spawn_local(async_compat::Compat::new(async { + app.run().await.unwrap(); + })) + .unwrap(); + + ui.run().map_err(|e| { + ziya::error::app::AppError::Slint(format!("failed_to_run_event_loop: {}", e)) + })?; + + Ok(()) } diff --git a/src/model/cex.rs b/src/model/cex.rs new file mode 100644 index 0000000..5631981 --- /dev/null +++ b/src/model/cex.rs @@ -0,0 +1,612 @@ +use std::str::FromStr; + +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Cex { + pub name: CexName, + pub address: solana_pubkey::Pubkey, +} + +impl Cex { + pub fn new(name: CexName, address: solana_pubkey::Pubkey) -> Self { + Self { name, address } + } + + pub fn get_exchange_name(address: solana_pubkey::Pubkey) -> Option { + match address.to_string().as_str() { + "FpwQQhQQoEaVu3WU2qZMfF1hx48YyfwsLoRgXG83E99Q" => Some(CexName::CoinbaseHW1), + "GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE" => Some(CexName::CoinbaseHW2), + "D89hHJT5Aqyx1trP6EnGY9jJUB3whgnq3aUvvCqedvzf" => Some(CexName::CoinbaseHW3), + "DPqsobysNf5iA9w7zrQM8HLzCKZEDMkZsWbiidsAt1xo" => Some(CexName::CoinbaseHW4), + "H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS" => Some(CexName::Coinbase1), + "2AQdpHJ2JpcEgPiATUXjQxA8QmafFegfQwSLWSprPicm" => Some(CexName::Coinbase2), + "59L2oxymiQQ9Hvhh92nt8Y7nDYjsauFkdb3SybdnsG6h" => Some(CexName::Coinbase4), + "9obNtb5GyUegcs3a1CbBkLuc5hEWynWfJC6gjz5uWQkE" => Some(CexName::Coinbase5), + "3vxheE5C46XzK4XftziRhwAf8QAfipD7HXXWj25mgkom" => Some(CexName::CoinbasePrime), + "CKy3KzEMSL1PQV6Wppggoqi2nGA7teE4L7JipEK89yqj" => Some(CexName::CoinbaseCW1), + "G6zmnfSdG6QJaDWYwbGQ4dpCSUC4gvjfZxYQ4ZharV7C" => Some(CexName::CoinbaseCW2), + "VTvk7sG6QQ28iK3NEKRRD9fvPzk5pKpJL2iwgVqMFcL" => Some(CexName::CoinbaseCW3), + "85cPov8nuRCkJ88VNMcHaHZ26Ux85PbSrHW4jg7izW4h" => Some(CexName::CoinbaseCW4), + "D6gCBB3CZEMNbX1PDr3GtZAMhnebEumcgJ2yv8Etv5hF" => Some(CexName::CoinbaseCW5), + "3qP77PzrHxSrW1S8dH4Ss1dmpJDHpC6ATVgwy5FmXDEf" => Some(CexName::CoinbaseCW6), + "146yGthSmnTPuCo6Zfbmr56YbAyWZ3rzAhRcT7tTF5ha" => Some(CexName::CoinbaseCW7), + "GXTrXayxMJUujsRTxYjAbkdbNvs6u2KN89UpG8f6eMAg" => Some(CexName::CoinbaseCW8), + "AzAvbCQsXurd2PbGLYcB61tyvE8kLDaZShE1S5Bp3WeS" => Some(CexName::CoinbaseCW9), + "4pHKEisSmAr5CSump4dJnTJgG6eugmtieXcUxDBcQcG5" => Some(CexName::CoinbaseCW10), + "BmGyWBMEcjJD7JQD1jRJ5vEt7XX2LyVvtxwtTGV4N1bp" => Some(CexName::CoinbaseCW11), + "py5jDEUAynTufQHM7P6Tu9M8NUd8JYux7aMcLXcC51q" => Some(CexName::CoinbaseCW12), + "is6MTRHEgyFLNTfYcuV4QBWLjrZBfmhVNYR6ccgr8KV" => Some(CexName::OKXHW1), + "C68a6RCGLiPskbPYtAcsCjhG8tfTWYcoB4JjCrXFdqyo" => Some(CexName::OKXHW2), + "5VCwKtCXgCJ6kit5FybXjvriW3xELsFDhYrPSqtJNmcD" => Some(CexName::OKX), + "9un5wqE3q4oCjyrDkwsdD48KteCJitQX5978Vh7KKxHo" => Some(CexName::OKX2), + "ASTyfSima4LLAdDgoFGkgqoKowG1LZFDr9fAQrg7iaJZ" => Some(CexName::MEXC1), + "5PAhQiYdLBd6SVdjzBQDxUAEFyDdF5ExNPQfcscnPRj5" => Some(CexName::MEXC2), + "FWznbcNXWQuHTawe9RxvQ2LdCENssh12dsznf4RiouN5" => Some(CexName::Kraken), + "9cNE6KBg2Xmf34FPMMvzDF8yUHMrgLRzBV3vD7b1JnUS" => Some(CexName::KrakenCW), + "F7RkX6Y1qTfBqoX5oHoZEgrG1Dpy55UZ3GfWwPbM58nQ" => Some(CexName::KrakenCW2), + "3yFwqXBfZY4jBVUafQ1YEXw189y2dN3V5KQq9uzBDy1E" => Some(CexName::Binance8), + "2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S" => Some(CexName::Binance1), + "5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9" => Some(CexName::Binance2), + "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM" => Some(CexName::Binance3), + "53unSgGWqEWANcPYRF35B2Bgf8BkszUtcccKiXwGGLyr" => Some(CexName::BinanceUSHW), + "3gd3dqgtJ4jWfBfLYTX67DALFetjc5iS72sCgRhCkW2u" => Some(CexName::Binance10), + "6QJzieMYfp7yr3EdrePaQoG3Ghxs2wM98xSLRu8Xh56U" => Some(CexName::Binance11), + "GBrURzmtWujJRTA3Bkvo7ZgWuZYLMMwPCwre7BejJXnK" => Some(CexName::BinanceCW), + "4S8C1yrRZmJYPzCqzEVjZYf6qCYWFoF7hWLRzssTCotX" => Some(CexName::BitgetCW), + "A77HErqtfN1hLLpvZ9pCtu66FEtM8BveoaKbbMoZ4RiR" => Some(CexName::BitgetExchange), + "u6PJ8DtQuPFnfmwHbGFULQ4u4EgjDiyYKjVEsynXq2w" => Some(CexName::Gateio1), + "HiRpdAZifEsZGdzQ5Xo5wcnaH3D2Jj9SoNsUzcYNK78J" => Some(CexName::Gateio2), + "AC5RDfQFmDS1deWZos921JfqscXdByf8BKHs5ACWjtW2" => Some(CexName::BybitHW), + "42brAgAVNzMBP7aaktPvAmBSPEkehnFQejiZc53EpJFd" => Some(CexName::BybitCW), + "FxteHmLwG9nk1eL4pjNve3Eub2goGkkz6g6TbvdmW46a" => Some(CexName::BitfinexHW), + "FyJBKcfcEBzGN74uNxZ95GxnCxeuJJujQCELpPv14ZfN" => Some(CexName::BitfinexCW), + "57vSaRTqN9iXaemgh4AoDsZ63mcaoshfMK8NP3Z5QNbs" => Some(CexName::KuCoin1), + "BmFdpraQhkiDQE6SnfG5omcA1VwzqfXrwtNYBwWTymy6" => Some(CexName::KuCoin2), + "HVh6wHNBAsG3pq1Bj5oCzRjoWKVogEDHwUHkRz3ekFgt" => Some(CexName::KuCoin3), + "DBmae92YTQKLsNzXcPscxiwPqMcz9stQr2prB5ZCAHPd" => Some(CexName::KuCoinCW), + "7Ci23i82UMa8RpfVbdMjTytiDi2VoZS8uLyHhZBV2Qy7" => Some(CexName::PoloniexHW), + "8s9j5qUtuE9PGA5s7QeAXEh5oc2UGr71pmJXgyiZMHkt" => Some(CexName::LBank), + "G9X7F4JzLzbSGMCndiBdWNi5YzZZakmtkdwq7xS3Q3FE" => Some(CexName::StakecomHotWallet), + "2snHHreXbpJ7UwZxPe37gnUNf7Wx7wv6UKDSR2JckKuS" => Some(CexName::DeBridgeVault), + "Biw4eeaiYYYq6xSqEd7GzdwsrrndxA8mqdxfAtG3PTUU" => Some(CexName::RevolutHotWallet), + "HBxZShcE86UMmF93KUM8eWJKqeEXi5cqWCLYLMMhqMYm" => Some(CexName::BitStampHotWallet), + _ => None, + } + } + + pub fn get_exchange_address(name: CexName) -> Option { + match name { + CexName::CoinbaseHW1 => Some( + solana_pubkey::Pubkey::from_str("FpwQQhQQoEaVu3WU2qZMfF1hx48YyfwsLoRgXG83E99Q") + .unwrap(), + ), + CexName::CoinbaseHW2 => Some( + solana_pubkey::Pubkey::from_str("GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE") + .unwrap(), + ), + CexName::CoinbaseHW3 => Some( + solana_pubkey::Pubkey::from_str("D89hHJT5Aqyx1trP6EnGY9jJUB3whgnq3aUvvCqedvzf") + .unwrap(), + ), + CexName::CoinbaseHW4 => Some( + solana_pubkey::Pubkey::from_str("DPqsobysNf5iA9w7zrQM8HLzCKZEDMkZsWbiidsAt1xo") + .unwrap(), + ), + CexName::Coinbase1 => Some( + solana_pubkey::Pubkey::from_str("H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS") + .unwrap(), + ), + CexName::Coinbase2 => Some( + solana_pubkey::Pubkey::from_str("2AQdpHJ2JpcEgPiATUXjQxA8QmafFegfQwSLWSprPicm") + .unwrap(), + ), + CexName::Coinbase4 => Some( + solana_pubkey::Pubkey::from_str("59L2oxymiQQ9Hvhh92nt8Y7nDYjsauFkdb3SybdnsG6h") + .unwrap(), + ), + CexName::Coinbase5 => Some( + solana_pubkey::Pubkey::from_str("9obNtb5GyUegcs3a1CbBkLuc5hEWynWfJC6gjz5uWQkE") + .unwrap(), + ), + CexName::CoinbasePrime => Some( + solana_pubkey::Pubkey::from_str("3vxheE5C46XzK4XftziRhwAf8QAfipD7HXXWj25mgkom") + .unwrap(), + ), + CexName::CoinbaseCW1 => Some( + solana_pubkey::Pubkey::from_str("CKy3KzEMSL1PQV6Wppggoqi2nGA7teE4L7JipEK89yqj") + .unwrap(), + ), + CexName::CoinbaseCW2 => Some( + solana_pubkey::Pubkey::from_str("G6zmnfSdG6QJaDWYwbGQ4dpCSUC4gvjfZxYQ4ZharV7C") + .unwrap(), + ), + CexName::CoinbaseCW3 => Some( + solana_pubkey::Pubkey::from_str("VTvk7sG6QQ28iK3NEKRRD9fvPzk5pKpJL2iwgVqMFcL") + .unwrap(), + ), + CexName::CoinbaseCW4 => Some( + solana_pubkey::Pubkey::from_str("85cPov8nuRCkJ88VNMcHaHZ26Ux85PbSrHW4jg7izW4h") + .unwrap(), + ), + CexName::CoinbaseCW5 => Some( + solana_pubkey::Pubkey::from_str("D6gCBB3CZEMNbX1PDr3GtZAMhnebEumcgJ2yv8Etv5hF") + .unwrap(), + ), + CexName::CoinbaseCW6 => Some( + solana_pubkey::Pubkey::from_str("3qP77PzrHxSrW1S8dH4Ss1dmpJDHpC6ATVgwy5FmXDEf") + .unwrap(), + ), + CexName::CoinbaseCW7 => Some( + solana_pubkey::Pubkey::from_str("146yGthSmnTPuCo6Zfbmr56YbAyWZ3rzAhRcT7tTF5ha") + .unwrap(), + ), + CexName::CoinbaseCW8 => Some( + solana_pubkey::Pubkey::from_str("GXTrXayxMJUujsRTxYjAbkdbNvs6u2KN89UpG8f6eMAg") + .unwrap(), + ), + CexName::CoinbaseCW9 => Some( + solana_pubkey::Pubkey::from_str("AzAvbCQsXurd2PbGLYcB61tyvE8kLDaZShE1S5Bp3WeS") + .unwrap(), + ), + CexName::CoinbaseCW10 => Some( + solana_pubkey::Pubkey::from_str("4pHKEisSmAr5CSump4dJnTJgG6eugmtieXcUxDBcQcG5") + .unwrap(), + ), + CexName::CoinbaseCW11 => Some( + solana_pubkey::Pubkey::from_str("BmGyWBMEcjJD7JQD1jRJ5vEt7XX2LyVvtxwtTGV4N1bp") + .unwrap(), + ), + CexName::CoinbaseCW12 => Some( + solana_pubkey::Pubkey::from_str("py5jDEUAynTufQHM7P6Tu9M8NUd8JYux7aMcLXcC51q") + .unwrap(), + ), + CexName::OKXHW1 => Some( + solana_pubkey::Pubkey::from_str("is6MTRHEgyFLNTfYcuV4QBWLjrZBfmhVNYR6ccgr8KV") + .unwrap(), + ), + CexName::OKXHW2 => Some( + solana_pubkey::Pubkey::from_str("C68a6RCGLiPskbPYtAcsCjhG8tfTWYcoB4JjCrXFdqyo") + .unwrap(), + ), + CexName::OKX => Some( + solana_pubkey::Pubkey::from_str("5VCwKtCXgCJ6kit5FybXjvriW3xELsFDhYrPSqtJNmcD") + .unwrap(), + ), + CexName::OKX2 => Some( + solana_pubkey::Pubkey::from_str("9un5wqE3q4oCjyrDkwsdD48KteCJitQX5978Vh7KKxHo") + .unwrap(), + ), + CexName::MEXC1 => Some( + solana_pubkey::Pubkey::from_str("ASTyfSima4LLAdDgoFGkgqoKowG1LZFDr9fAQrg7iaJZ") + .unwrap(), + ), + CexName::MEXC2 => Some( + solana_pubkey::Pubkey::from_str("5PAhQiYdLBd6SVdjzBQDxUAEFyDdF5ExNPQfcscnPRj5") + .unwrap(), + ), + CexName::Kraken => Some( + solana_pubkey::Pubkey::from_str("FWznbcNXWQuHTawe9RxvQ2LdCENssh12dsznf4RiouN5") + .unwrap(), + ), + CexName::KrakenCW => Some( + solana_pubkey::Pubkey::from_str("9cNE6KBg2Xmf34FPMMvzDF8yUHMrgLRzBV3vD7b1JnUS") + .unwrap(), + ), + CexName::KrakenCW2 => Some( + solana_pubkey::Pubkey::from_str("F7RkX6Y1qTfBqoX5oHoZEgrG1Dpy55UZ3GfWwPbM58nQ") + .unwrap(), + ), + CexName::Binance8 => Some( + solana_pubkey::Pubkey::from_str("3yFwqXBfZY4jBVUafQ1YEXw189y2dN3V5KQq9uzBDy1E") + .unwrap(), + ), + CexName::Binance1 => Some( + solana_pubkey::Pubkey::from_str("2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S") + .unwrap(), + ), + CexName::Binance2 => Some( + solana_pubkey::Pubkey::from_str("5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9") + .unwrap(), + ), + CexName::Binance3 => Some( + solana_pubkey::Pubkey::from_str("9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM") + .unwrap(), + ), + CexName::BinanceUSHW => Some( + solana_pubkey::Pubkey::from_str("53unSgGWqEWANcPYRF35B2Bgf8BkszUtcccKiXwGGLyr") + .unwrap(), + ), + CexName::Binance10 => Some( + solana_pubkey::Pubkey::from_str("3gd3dqgtJ4jWfBfLYTX67DALFetjc5iS72sCgRhCkW2u") + .unwrap(), + ), + CexName::Binance11 => Some( + solana_pubkey::Pubkey::from_str("6QJzieMYfp7yr3EdrePaQoG3Ghxs2wM98xSLRu8Xh56U") + .unwrap(), + ), + CexName::BinanceCW => Some( + solana_pubkey::Pubkey::from_str("GBrURzmtWujJRTA3Bkvo7ZgWuZYLMMwPCwre7BejJXnK") + .unwrap(), + ), + CexName::BitgetCW => Some( + solana_pubkey::Pubkey::from_str("4S8C1yrRZmJYPzCqzEVjZYf6qCYWFoF7hWLRzssTCotX") + .unwrap(), + ), + CexName::BitgetExchange => Some( + solana_pubkey::Pubkey::from_str("A77HErqtfN1hLLpvZ9pCtu66FEtM8BveoaKbbMoZ4RiR") + .unwrap(), + ), + CexName::Gateio1 => Some( + solana_pubkey::Pubkey::from_str("u6PJ8DtQuPFnfmwHbGFULQ4u4EgjDiyYKjVEsynXq2w") + .unwrap(), + ), + CexName::Gateio2 => Some( + solana_pubkey::Pubkey::from_str("HiRpdAZifEsZGdzQ5Xo5wcnaH3D2Jj9SoNsUzcYNK78J") + .unwrap(), + ), + CexName::BybitHW => Some( + solana_pubkey::Pubkey::from_str("AC5RDfQFmDS1deWZos921JfqscXdByf8BKHs5ACWjtW2") + .unwrap(), + ), + CexName::BybitCW => Some( + solana_pubkey::Pubkey::from_str("42brAgAVNzMBP7aaktPvAmBSPEkehnFQejiZc53EpJFd") + .unwrap(), + ), + CexName::BitfinexHW => Some( + solana_pubkey::Pubkey::from_str("FxteHmLwG9nk1eL4pjNve3Eub2goGkkz6g6TbvdmW46a") + .unwrap(), + ), + CexName::BitfinexCW => Some( + solana_pubkey::Pubkey::from_str("FyJBKcfcEBzGN74uNxZ95GxnCxeuJJujQCELpPv14ZfN") + .unwrap(), + ), + CexName::KuCoin1 => Some( + solana_pubkey::Pubkey::from_str("57vSaRTqN9iXaemgh4AoDsZ63mcaoshfMK8NP3Z5QNbs") + .unwrap(), + ), + CexName::KuCoin2 => Some( + solana_pubkey::Pubkey::from_str("BmFdpraQhkiDQE6SnfG5omcA1VwzqfXrwtNYBwWTymy6") + .unwrap(), + ), + CexName::KuCoin3 => Some( + solana_pubkey::Pubkey::from_str("HVh6wHNBAsG3pq1Bj5oCzRjoWKVogEDHwUHkRz3ekFgt") + .unwrap(), + ), + CexName::KuCoinCW => Some( + solana_pubkey::Pubkey::from_str("DBmae92YTQKLsNzXcPscxiwPqMcz9stQr2prB5ZCAHPd") + .unwrap(), + ), + CexName::PoloniexHW => Some( + solana_pubkey::Pubkey::from_str("7Ci23i82UMa8RpfVbdMjTytiDi2VoZS8uLyHhZBV2Qy7") + .unwrap(), + ), + CexName::LBank => Some( + solana_pubkey::Pubkey::from_str("8s9j5qUtuE9PGA5s7QeAXEh5oc2UGr71pmJXgyiZMHkt") + .unwrap(), + ), + CexName::StakecomHotWallet => Some( + solana_pubkey::Pubkey::from_str("G9X7F4JzLzbSGMCndiBdWNi5YzZZakmtkdwq7xS3Q3FE") + .unwrap(), + ), + CexName::DeBridgeVault => Some( + solana_pubkey::Pubkey::from_str("2snHHreXbpJ7UwZxPe37gnUNf7Wx7wv6UKDSR2JckKuS") + .unwrap(), + ), + CexName::RevolutHotWallet => Some( + solana_pubkey::Pubkey::from_str("Biw4eeaiYYYq6xSqEd7GzdwsrrndxA8mqdxfAtG3PTUU") + .unwrap(), + ), + CexName::BitStampHotWallet => Some( + solana_pubkey::Pubkey::from_str("HBxZShcE86UMmF93KUM8eWJKqeEXi5cqWCLYLMMhqMYm") + .unwrap(), + ), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum CexName { + #[serde(rename = "coinbase_hw1")] + CoinbaseHW1, + #[serde(rename = "coinbase_hw2")] + CoinbaseHW2, + #[serde(rename = "coinbase_hw3")] + CoinbaseHW3, + #[serde(rename = "coinbase_hw4")] + CoinbaseHW4, + #[serde(rename = "coinbase_1")] + Coinbase1, + #[serde(rename = "coinbase_2")] + Coinbase2, + #[serde(rename = "coinbase_4")] + Coinbase4, + #[serde(rename = "coinbase_5")] + Coinbase5, + #[serde(rename = "coinbase_prime")] + CoinbasePrime, + #[serde(rename = "coinbase_cw1")] + CoinbaseCW1, + #[serde(rename = "coinbase_cw2")] + CoinbaseCW2, + #[serde(rename = "coinbase_cw3")] + CoinbaseCW3, + #[serde(rename = "coinbase_cw4")] + CoinbaseCW4, + #[serde(rename = "coinbase_cw5")] + CoinbaseCW5, + #[serde(rename = "coinbase_cw6")] + CoinbaseCW6, + #[serde(rename = "coinbase_cw7")] + CoinbaseCW7, + #[serde(rename = "coinbase_cw8")] + CoinbaseCW8, + #[serde(rename = "coinbase_cw9")] + CoinbaseCW9, + #[serde(rename = "coinbase_cw10")] + CoinbaseCW10, + #[serde(rename = "coinbase_cw11")] + CoinbaseCW11, + #[serde(rename = "coinbase_cw12")] + CoinbaseCW12, + #[serde(rename = "okx_hw1")] + OKXHW1, + #[serde(rename = "okx_hw2")] + OKXHW2, + #[serde(rename = "okx")] + OKX, + #[serde(rename = "okx_2")] + OKX2, + #[serde(rename = "mexc_1")] + MEXC1, + #[serde(rename = "mexc_2")] + MEXC2, + #[serde(rename = "kraken")] + Kraken, + #[serde(rename = "kraken_cw")] + KrakenCW, + #[serde(rename = "kraken_cw2")] + KrakenCW2, + #[serde(rename = "binance_8")] + Binance8, + #[serde(rename = "binance_1")] + Binance1, + #[serde(rename = "binance_2")] + Binance2, + #[serde(rename = "binance_3")] + Binance3, + #[serde(rename = "binance_us_hw")] + BinanceUSHW, + #[serde(rename = "binance_10")] + Binance10, + #[serde(rename = "binance_11")] + Binance11, + #[serde(rename = "binance_cw")] + BinanceCW, + #[serde(rename = "bitget_cw")] + BitgetCW, + #[serde(rename = "bitget_exchange")] + BitgetExchange, + #[serde(rename = "gateio_1")] + Gateio1, + #[serde(rename = "gateio_2")] + Gateio2, + #[serde(rename = "bybit_hw")] + BybitHW, + #[serde(rename = "bybit_cw")] + BybitCW, + #[serde(rename = "bitfinex_hw")] + BitfinexHW, + #[serde(rename = "bitfinex_cw")] + BitfinexCW, + #[serde(rename = "kucoin_1")] + KuCoin1, + #[serde(rename = "kucoin_2")] + KuCoin2, + #[serde(rename = "kucoin_3")] + KuCoin3, + #[serde(rename = "kucoin_cw")] + KuCoinCW, + #[serde(rename = "poloniex_hw")] + PoloniexHW, + #[serde(rename = "lbank")] + LBank, + #[serde(rename = "stakecom_hot_wallet")] + StakecomHotWallet, + #[serde(rename = "debridge_vault")] + DeBridgeVault, + #[serde(rename = "revolut_hot_wallet")] + RevolutHotWallet, + #[serde(rename = "bitstamp_hot_wallet")] + BitStampHotWallet, +} + +impl std::fmt::Display for CexName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CexName::CoinbaseHW1 => write!(f, "coinbase_hw1"), + CexName::CoinbaseHW2 => write!(f, "coinbase_hw2"), + CexName::CoinbaseHW3 => write!(f, "coinbase_hw3"), + CexName::CoinbaseHW4 => write!(f, "coinbase_hw4"), + CexName::Coinbase1 => write!(f, "coinbase_1"), + CexName::Coinbase2 => write!(f, "coinbase_2"), + CexName::Coinbase4 => write!(f, "coinbase_4"), + CexName::Coinbase5 => write!(f, "coinbase_5"), + CexName::CoinbasePrime => write!(f, "coinbase_prime"), + CexName::CoinbaseCW1 => write!(f, "coinbase_cw1"), + CexName::CoinbaseCW2 => write!(f, "coinbase_cw2"), + CexName::CoinbaseCW3 => write!(f, "coinbase_cw3"), + CexName::CoinbaseCW4 => write!(f, "coinbase_cw4"), + CexName::CoinbaseCW5 => write!(f, "coinbase_cw5"), + CexName::CoinbaseCW6 => write!(f, "coinbase_cw6"), + CexName::CoinbaseCW7 => write!(f, "coinbase_cw7"), + CexName::CoinbaseCW8 => write!(f, "coinbase_cw8"), + CexName::CoinbaseCW9 => write!(f, "coinbase_cw9"), + CexName::CoinbaseCW10 => write!(f, "coinbase_cw10"), + CexName::CoinbaseCW11 => write!(f, "coinbase_cw11"), + CexName::CoinbaseCW12 => write!(f, "coinbase_cw12"), + CexName::OKXHW1 => write!(f, "okx_hw1"), + CexName::OKXHW2 => write!(f, "okx_hw2"), + CexName::OKX => write!(f, "okx"), + CexName::OKX2 => write!(f, "okx_2"), + CexName::MEXC1 => write!(f, "mexc_1"), + CexName::MEXC2 => write!(f, "mexc_2"), + CexName::Kraken => write!(f, "kraken"), + CexName::KrakenCW => write!(f, "kraken_cw"), + CexName::KrakenCW2 => write!(f, "kraken_cw2"), + CexName::Binance8 => write!(f, "binance_8"), + CexName::Binance1 => write!(f, "binance_1"), + CexName::Binance2 => write!(f, "binance_2"), + CexName::Binance3 => write!(f, "binance_3"), + CexName::BinanceUSHW => write!(f, "binance_us_hw"), + CexName::Binance10 => write!(f, "binance_10"), + CexName::Binance11 => write!(f, "binance_11"), + CexName::BinanceCW => write!(f, "binance_cw"), + CexName::BitgetCW => write!(f, "bitget_cw"), + CexName::BitgetExchange => write!(f, "bitget_exchange"), + CexName::Gateio1 => write!(f, "gateio_1"), + CexName::Gateio2 => write!(f, "gateio_2"), + CexName::BybitHW => write!(f, "bybit_hw"), + CexName::BybitCW => write!(f, "bybit_cw"), + CexName::BitfinexHW => write!(f, "bitfinex_hw"), + CexName::BitfinexCW => write!(f, "bitfinex_cw"), + CexName::KuCoin1 => write!(f, "kucoin_1"), + CexName::KuCoin2 => write!(f, "kucoin_2"), + CexName::KuCoin3 => write!(f, "kucoin_3"), + CexName::KuCoinCW => write!(f, "kucoin_cw"), + CexName::PoloniexHW => write!(f, "poloniex_hw"), + CexName::LBank => write!(f, "lbank"), + CexName::StakecomHotWallet => write!(f, "stakecom_hot_wallet"), + CexName::DeBridgeVault => write!(f, "debridge_vault"), + CexName::RevolutHotWallet => write!(f, "revolut_hot_wallet"), + CexName::BitStampHotWallet => write!(f, "bitstamp_hot_wallet"), + } + } +} + +impl From for String { + fn from(cex: CexName) -> Self { + match cex { + CexName::CoinbaseHW1 => "coinbase_hw1".to_string(), + CexName::CoinbaseHW2 => "coinbase_hw2".to_string(), + CexName::CoinbaseHW3 => "coinbase_hw3".to_string(), + CexName::CoinbaseHW4 => "coinbase_hw4".to_string(), + CexName::Coinbase1 => "coinbase_1".to_string(), + CexName::Coinbase2 => "coinbase_2".to_string(), + CexName::Coinbase4 => "coinbase_4".to_string(), + CexName::Coinbase5 => "coinbase_5".to_string(), + CexName::CoinbasePrime => "coinbase_prime".to_string(), + CexName::CoinbaseCW1 => "coinbase_cw1".to_string(), + CexName::CoinbaseCW2 => "coinbase_cw2".to_string(), + CexName::CoinbaseCW3 => "coinbase_cw3".to_string(), + CexName::CoinbaseCW4 => "coinbase_cw4".to_string(), + CexName::CoinbaseCW5 => "coinbase_cw5".to_string(), + CexName::CoinbaseCW6 => "coinbase_cw6".to_string(), + CexName::CoinbaseCW7 => "coinbase_cw7".to_string(), + CexName::CoinbaseCW8 => "coinbase_cw8".to_string(), + CexName::CoinbaseCW9 => "coinbase_cw9".to_string(), + CexName::CoinbaseCW10 => "coinbase_cw10".to_string(), + CexName::CoinbaseCW11 => "coinbase_cw11".to_string(), + CexName::CoinbaseCW12 => "coinbase_cw12".to_string(), + CexName::OKXHW1 => "okx_hw1".to_string(), + CexName::OKXHW2 => "okx_hw2".to_string(), + CexName::OKX => "okx".to_string(), + CexName::OKX2 => "okx_2".to_string(), + CexName::MEXC1 => "mexc_1".to_string(), + CexName::MEXC2 => "mexc_2".to_string(), + CexName::Kraken => "kraken".to_string(), + CexName::KrakenCW => "kraken_cw".to_string(), + CexName::KrakenCW2 => "kraken_cw2".to_string(), + CexName::Binance8 => "binance_8".to_string(), + CexName::Binance1 => "binance_1".to_string(), + CexName::Binance2 => "binance_2".to_string(), + CexName::Binance3 => "binance_3".to_string(), + CexName::BinanceUSHW => "binance_us_hw".to_string(), + CexName::Binance10 => "binance_10".to_string(), + CexName::Binance11 => "binance_11".to_string(), + CexName::BinanceCW => "binance_cw".to_string(), + CexName::BitgetCW => "bitget_cw".to_string(), + CexName::BitgetExchange => "bitget_exchange".to_string(), + CexName::Gateio1 => "gateio_1".to_string(), + CexName::Gateio2 => "gateio_2".to_string(), + CexName::BybitHW => "bybit_hw".to_string(), + CexName::BybitCW => "bybit_cw".to_string(), + CexName::BitfinexHW => "bitfinex_hw".to_string(), + CexName::BitfinexCW => "bitfinex_cw".to_string(), + CexName::KuCoin1 => "kucoin_1".to_string(), + CexName::KuCoin2 => "kucoin_2".to_string(), + CexName::KuCoin3 => "kucoin_3".to_string(), + CexName::KuCoinCW => "kucoin_cw".to_string(), + CexName::PoloniexHW => "poloniex_hw".to_string(), + CexName::LBank => "lbank".to_string(), + CexName::StakecomHotWallet => "stakecom_hot_wallet".to_string(), + CexName::DeBridgeVault => "debridge_vault".to_string(), + CexName::RevolutHotWallet => "revolut_hot_wallet".to_string(), + CexName::BitStampHotWallet => "bitstamp_hot_wallet".to_string(), + } + } +} + +impl CexName { + pub fn as_str(&self) -> &'static str { + match self { + CexName::CoinbaseHW1 => "coinbase_hw1", + CexName::CoinbaseHW2 => "coinbase_hw2", + CexName::CoinbaseHW3 => "coinbase_hw3", + CexName::CoinbaseHW4 => "coinbase_hw4", + CexName::Coinbase1 => "coinbase_1", + CexName::Coinbase2 => "coinbase_2", + CexName::Coinbase4 => "coinbase_4", + CexName::Coinbase5 => "coinbase_5", + CexName::CoinbasePrime => "coinbase_prime", + CexName::CoinbaseCW1 => "coinbase_cw1", + CexName::CoinbaseCW2 => "coinbase_cw2", + CexName::CoinbaseCW3 => "coinbase_cw3", + CexName::CoinbaseCW4 => "coinbase_cw4", + CexName::CoinbaseCW5 => "coinbase_cw5", + CexName::CoinbaseCW6 => "coinbase_cw6", + CexName::CoinbaseCW7 => "coinbase_cw7", + CexName::CoinbaseCW8 => "coinbase_cw8", + CexName::CoinbaseCW9 => "coinbase_cw9", + CexName::CoinbaseCW10 => "coinbase_cw10", + CexName::CoinbaseCW11 => "coinbase_cw11", + CexName::CoinbaseCW12 => "coinbase_cw12", + CexName::OKXHW1 => "okx_hw1", + CexName::OKXHW2 => "okx_hw2", + CexName::OKX => "okx", + CexName::OKX2 => "okx_2", + CexName::MEXC1 => "mexc_1", + CexName::MEXC2 => "mexc_2", + CexName::Kraken => "kraken", + CexName::KrakenCW => "kraken_cw", + CexName::KrakenCW2 => "kraken_cw2", + CexName::Binance8 => "binance_8", + CexName::Binance1 => "binance_1", + CexName::Binance2 => "binance_2", + CexName::Binance3 => "binance_3", + CexName::BinanceUSHW => "binance_us_hw", + CexName::Binance10 => "binance_10", + CexName::Binance11 => "binance_11", + CexName::BinanceCW => "binance_cw", + CexName::BitgetCW => "bitget_cw", + CexName::BitgetExchange => "bitget_exchange", + CexName::Gateio1 => "gateio_1", + CexName::Gateio2 => "gateio_2", + CexName::BybitHW => "bybit_hw", + CexName::BybitCW => "bybit_cw", + CexName::BitfinexHW => "bitfinex_hw", + CexName::BitfinexCW => "bitfinex_cw", + CexName::KuCoin1 => "kucoin_1", + CexName::KuCoin2 => "kucoin_2", + CexName::KuCoin3 => "kucoin_3", + CexName::KuCoinCW => "kucoin_cw", + CexName::PoloniexHW => "poloniex_hw", + CexName::LBank => "lbank", + CexName::StakecomHotWallet => "stakecom_hot_wallet", + CexName::DeBridgeVault => "debridge_vault", + CexName::RevolutHotWallet => "revolut_hot_wallet", + CexName::BitStampHotWallet => "bitstamp_hot_wallet", + } + } +} diff --git a/src/model/graph.rs b/src/model/graph.rs new file mode 100644 index 0000000..fe31369 --- /dev/null +++ b/src/model/graph.rs @@ -0,0 +1,121 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use petgraph::prelude::*; +use petgraph::Graph; +use serde::Deserialize; +use serde::Serialize; +use solana_pubkey::Pubkey; +use tokio::sync::RwLock; + +use crate::model::cex::Cex; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddressNode { + pub address: solana_pubkey::Pubkey, + pub total_received: f64, + pub total_balance: f64, + pub is_cex: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionEdge { + pub from: solana_pubkey::Pubkey, + pub to: solana_pubkey::Pubkey, + pub amount: f64, + pub timestamp: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct CreatorConnectionGraph { + graph: Graph, + #[serde(skip)] + node_indices: HashMap, +} + +impl CreatorConnectionGraph { + pub fn new() -> Self { + Self { + graph: Graph::new(), + node_indices: HashMap::new(), + } + } + + // Rebuild the node_indices HashMap from the graph (useful after deserialization) + pub fn rebuild_indices(&mut self) { + self.node_indices.clear(); + for node_index in self.graph.node_indices() { + if let Some(node) = self.graph.node_weight(node_index) { + self.node_indices.insert(node.address, node_index); + } + } + } + + // Ensure indices are available (rebuild if empty and graph has nodes) + fn ensure_indices(&mut self) { + if self.node_indices.is_empty() && self.graph.node_count() > 0 { + self.rebuild_indices(); + } + } + + pub fn get_node_count(&self) -> usize { + self.graph.node_count() + } + + pub fn get_edge_count(&self) -> usize { + self.graph.edge_count() + } + + // Get all nodes in the graph + pub fn get_nodes(&self) -> Vec { + self.graph.node_weights().map(|node| node.clone()).collect() + } + + // Get all edges in the graph + pub fn get_edges(&self) -> Vec { + self.graph.edge_weights().map(|edge| edge.clone()).collect() + } +} + +// Thread-safe wrapper for the graph +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SharedCreatorConnectionGraph { + #[serde(skip)] + inner: Arc>, +} + +impl SharedCreatorConnectionGraph { + pub fn new() -> Self { + Self { + inner: Arc::new(RwLock::new(CreatorConnectionGraph::new())), + } + } + + pub async fn get_node_count(&self) -> usize { + self.inner.read().await.get_node_count() + } + + pub async fn get_edge_count(&self) -> usize { + self.inner.read().await.get_edge_count() + } + + pub async fn clone_graph(&self) -> CreatorConnectionGraph { + let mut graph = self.inner.read().await.clone(); + // Ensure indices are rebuilt after cloning (since they're skipped in serialization) + graph.rebuild_indices(); + graph + } + + // Method to ensure indices are available (useful after deserialization) + pub async fn ensure_indices(&self) { + self.inner.write().await.ensure_indices(); + } +} + +impl From for SharedCreatorConnectionGraph { + fn from(graph: CreatorConnectionGraph) -> Self { + Self { + inner: Arc::new(RwLock::new(graph)), + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..f5370a4 --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,3 @@ +pub mod cex; +pub mod graph; +pub mod token; diff --git a/src/model/token.rs b/src/model/token.rs new file mode 100644 index 0000000..556124b --- /dev/null +++ b/src/model/token.rs @@ -0,0 +1,48 @@ +use super::graph::SharedCreatorConnectionGraph; +use serde::{Deserialize, Serialize}; + +// Type for new token created event (matches NewTokenCache from muhafidh) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NewTokenCreatedData { + pub mint: solana_pubkey::Pubkey, + pub bonding_curve: Option, + pub name: String, + pub symbol: String, + pub uri: String, + pub creator: solana_pubkey::Pubkey, + pub created_at: u64, // Unix timestamp in seconds +} + +// Type for token CEX updated event (matches TokenAnalyzedCache from muhafidh) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TokenCexUpdatedData { + pub mint: String, // Mint address as string + pub name: String, + pub uri: String, + pub dev_name: String, // DevName from muhafidh + pub creator: String, // Creator address as string + pub cex_name: String, + pub cex_address: String, + pub bonding_curve: String, // Bonding curve address as string + pub created_at: u64, // Unix timestamp in seconds + pub updated_at: u64, // Unix timestamp in seconds + pub node_count: usize, + pub edge_count: usize, + pub graph: SharedCreatorConnectionGraph, +} + +// Type for max depth reached event (same structure as TokenAnalyzedCache) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MaxDepthReachedData { + pub mint: String, // Mint address as string + pub name: String, + pub uri: String, + pub dev_name: String, // DevName from muhafidh + pub creator: String, // Creator address as string + pub bonding_curve: String, // Bonding curve address as string + pub created_at: u64, // Unix timestamp in seconds + pub updated_at: u64, // Unix timestamp in seconds + pub node_count: usize, + pub edge_count: usize, + pub graph: SharedCreatorConnectionGraph, +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..e1f0f0e --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; + +pub mod redis; +use crate::config::Config; +use crate::error::Result; +use crate::storage::redis::RedisStorage; +use tracing::error; + +#[derive(Clone)] +pub struct StorageEngine { + pub redis: Arc, +} + +impl StorageEngine { + pub async fn new(config: Config) -> Result { + let redis_storage = Arc::new(RedisStorage::new(&config.storage_redis).await.map_err( + |e| { + error!("failed_to_create_redis_storage: {}", e); + e + }, + )?); + Ok(Self { + redis: redis_storage, + }) + } +} diff --git a/src/storage/redis.rs b/src/storage/redis.rs new file mode 100644 index 0000000..3cceb36 --- /dev/null +++ b/src/storage/redis.rs @@ -0,0 +1,73 @@ +use bb8::Pool; +use bb8_redis::RedisConnectionManager; +use redis::aio::PubSub; +use tracing::{error, info, instrument}; + +use crate::config::StorageRedisConfig; +use crate::err_with_loc; +use crate::error::app::AppError; +use crate::error::Result; + +pub type RedisPool = Pool; + +#[derive(Clone)] +pub struct RedisStorage { + pub pool: RedisPool, + redis_url: String, // Store the URL to create new connections +} + +impl RedisStorage { + #[instrument(level = "debug")] + pub async fn new(config: &StorageRedisConfig) -> Result { + let redis_url = format!("redis://{}:{}/?protocol=resp3", config.host, config.port); + + let manager = RedisConnectionManager::new(redis_url.clone()).map_err(|e| { + error!("failed_to_create_redis_manager: {}", e); + err_with_loc!(AppError::Redis(format!( + "failed_to_create_redis_manager: {}", + e + ))) + })?; + + let pool = bb8::Pool::builder() + .max_size(config.pool_size) + .build(manager) + .await + .map_err(|e| { + error!("failed_to_create_redis_pool: {}", e); + err_with_loc!(AppError::Redis(format!( + "failed_to_create_redis_pool: {}", + e + ))) + })?; + + info!("redis::connection_established"); + + Ok(Self { + pool, + redis_url, + }) + } + + /// Create a new independent PubSub connection for subscribers + /// Each subscriber should use its own connection to avoid blocking + pub async fn create_pubsub_connection(&self) -> Result { + let client = redis::Client::open(self.redis_url.clone()).map_err(|e| { + error!("failed_to_create_redis_client_for_pubsub: {}", e); + err_with_loc!(AppError::Redis(format!( + "failed_to_create_redis_client_for_pubsub: {}", + e + ))) + })?; + + let pubsub = client.get_async_pubsub().await.map_err(|e| { + error!("failed_to_get_new_pubsub_connection: {}", e); + err_with_loc!(AppError::Redis(format!( + "failed_to_get_new_pubsub_connection: {}", + e + ))) + })?; + + Ok(pubsub) + } +} diff --git a/src/task/mod.rs b/src/task/mod.rs new file mode 100644 index 0000000..d315865 --- /dev/null +++ b/src/task/mod.rs @@ -0,0 +1,3 @@ +pub mod shutdown; +pub mod subscriber; +pub mod ui; diff --git a/src/task/shutdown.rs b/src/task/shutdown.rs new file mode 100644 index 0000000..a496f41 --- /dev/null +++ b/src/task/shutdown.rs @@ -0,0 +1,33 @@ +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use tokio::sync::Notify; + +#[derive(Debug, Clone)] +pub struct ShutdownSignal { + pub signal: Arc, + shutdown_triggered: Arc, +} + +impl ShutdownSignal { + pub fn new() -> Self { + Self { + signal: Arc::new(Notify::new()), + shutdown_triggered: Arc::new(AtomicBool::new(false)), + } + } + + pub fn shutdown(&self) { + self.shutdown_triggered.store(true, Ordering::SeqCst); + self.signal.notify_waiters(); + } + + pub fn is_shutdown(&self) -> bool { + self.shutdown_triggered.load(Ordering::SeqCst) + } + + pub async fn wait_for_shutdown(&self) { + self.signal.notified().await; + } +} diff --git a/src/task/subscriber.rs b/src/task/subscriber.rs new file mode 100644 index 0000000..1646e19 --- /dev/null +++ b/src/task/subscriber.rs @@ -0,0 +1,186 @@ +use crate::err_with_loc; +use crate::error::app::AppError; +use crate::error::Result; +use crate::handler::token::TokenMetadataHandlerOperator; +use crate::model::token::{MaxDepthReachedData, NewTokenCreatedData, TokenCexUpdatedData}; +use crate::slint_ui::{MainWindow, NewTokenUiData}; +use crate::storage::StorageEngine; +use futures_util::StreamExt; +use slint::Weak; +use std::sync::Arc; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, warn}; + +// ✅ Simplified subscriber that creates its own PubSub connection +pub fn spawn_new_token_subscriber( + token_handler: Arc, + db: Arc, + cancellation_token: CancellationToken, +) -> JoinHandle> { + tokio::spawn(async move { + debug!("new_token_subscriber::starting"); + + // ✅ Create dedicated PubSub connection for this subscriber + let mut pubsub = db.redis.create_pubsub_connection().await?; + + if let Err(e) = pubsub.subscribe("new_token_created").await { + error!("failed_to_subscribe_to_new_token_created: {}", e); + return Err(err_with_loc!(AppError::Redis(format!( + "failed_to_subscribe_to_new_token_created: {}", + e + )))); + } + + // Create a channel for buffering messages - with good capacity for performance + let (buffer_tx, mut buffer_rx) = mpsc::channel::(1000); + + info!("new_token_subscriber::subscribed_successfully"); + let mut msg_stream = pubsub.on_message(); + let cancellation_token = cancellation_token.clone(); + + loop { + tokio::select! { + Some(token) = buffer_rx.recv() => { + // ✅ Just call the existing handler - no duplication! + if let Err(e) = token_handler.process_new_token(token.clone()).await { + error!("failed_to_send_token_to_token_handler::mint::{}::error::{}", token.mint.clone(), e); + } + }, + Some(message) = msg_stream.next() => { + if let Ok(msg) = message.get_payload::() { + if let Ok(token) = serde_json::from_str::(&msg) { + debug!("new_token_received::mint::{}::name::{}::creator::{}", + token.mint, token.name, token.creator); + if let Err(e) = buffer_tx.try_send(token.clone()) { + error!("failed_to_send_token_to_buffer::mint::{}::error::{}", token.mint, e); + } + } + } + }, + _ = cancellation_token.cancelled() => { + warn!("new_token_subscriber::shutdown_signal_received"); + break; + } + } + } + + info!("new_token_subscriber::ended"); + Ok(()) + }) +} + +pub fn spawn_token_cex_updated_subscriber( + token_handler: Arc, + db: Arc, + cancellation_token: CancellationToken, +) -> JoinHandle> { + tokio::spawn(async move { + debug!("token_cex_updated_subscriber::starting"); + + // ✅ Create dedicated PubSub connection for this subscriber + let mut pubsub = db.redis.create_pubsub_connection().await?; + + if let Err(e) = pubsub.subscribe("token_cex_updated").await { + error!("failed_to_subscribe_to_token_cex_updated: {}", e); + return Err(err_with_loc!(AppError::Redis(format!( + "failed_to_subscribe_to_token_cex_updated: {}", + e + )))); + } + + // Create a channel for buffering messages - with good capacity for performance + let (buffer_tx, mut buffer_rx) = mpsc::channel::(1000); + + info!("token_cex_updated_subscriber::subscribed_successfully"); + let mut msg_stream = pubsub.on_message(); + let cancellation_token = cancellation_token.clone(); + + loop { + tokio::select! { + Some(token) = buffer_rx.recv() => { + // ✅ Just call the existing handler - no duplication! + if let Err(e) = token_handler.process_cex_updated(token.clone()).await { + error!("failed_to_send_cex_updated_to_token_handler::mint::{}::error::{}", token.mint, e); + } + }, + Some(message) = msg_stream.next() => { + if let Ok(msg) = message.get_payload::() { + if let Ok(token) = serde_json::from_str::(&msg) { + debug!("token_cex_updated_received::mint::{}::name::{}::cex::{}", + token.mint, token.name, token.cex_name); + if let Err(e) = buffer_tx.try_send(token.clone()) { + error!("failed_to_send_cex_updated_to_buffer::mint::{}::error::{}", token.mint, e); + } + } + } + }, + _ = cancellation_token.cancelled() => { + warn!("token_cex_updated_subscriber::shutdown_signal_received"); + break; + } + } + } + + info!("token_cex_updated_subscriber::ended"); + Ok(()) + }) +} + +pub fn spawn_max_depth_reached_subscriber( + token_handler: Arc, + db: Arc, + cancellation_token: CancellationToken, +) -> JoinHandle> { + tokio::spawn(async move { + debug!("max_depth_reached_subscriber::starting"); + + // ✅ Create dedicated PubSub connection for this subscriber + let mut pubsub = db.redis.create_pubsub_connection().await?; + + if let Err(e) = pubsub.subscribe("max_depth_reached").await { + error!("failed_to_subscribe_to_max_depth_reached: {}", e); + return Err(err_with_loc!(AppError::Redis(format!( + "failed_to_subscribe_to_max_depth_reached: {}", + e + )))); + } + + // Create a channel for buffering messages - with good capacity for performance + let (buffer_tx, mut buffer_rx) = mpsc::channel::(1000); + + info!("max_depth_reached_subscriber::subscribed_successfully"); + let mut msg_stream = pubsub.on_message(); + let cancellation_token = cancellation_token.clone(); + + loop { + tokio::select! { + Some(token) = buffer_rx.recv() => { + // ✅ Just call the existing handler - no duplication! + if let Err(e) = token_handler.process_max_depth_reached(token.clone()).await { + error!("failed_to_send_max_depth_reached_to_token_handler::mint::{}::error::{}", token.mint, e); + } + }, + Some(message) = msg_stream.next() => { + if let Ok(msg) = message.get_payload::() { + if let Ok(token) = serde_json::from_str::(&msg) { + debug!("max_depth_reached_received::mint::{}::name::{}::nodes::{}::edges::{}", + token.mint, token.name, token.node_count, token.edge_count); + if let Err(e) = buffer_tx.try_send(token.clone()) { + error!("failed_to_send_max_depth_reached_to_buffer::mint::{}::error::{}", token.mint, e); + } + } + } + }, + _ = cancellation_token.cancelled() => { + warn!("max_depth_reached_subscriber::shutdown_signal_received"); + break; + } + } + } + + info!("max_depth_reached_subscriber::ended"); + Ok(()) + }) +} diff --git a/src/task/ui.rs b/src/task/ui.rs new file mode 100644 index 0000000..7709906 --- /dev/null +++ b/src/task/ui.rs @@ -0,0 +1,147 @@ +use i_slint_backend_winit::WinitWindowAccessor; +use slint::Weak; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; + +use crate::error::Result; +use crate::get_current_unix_timestamp; +use crate::handler::ui::SlintHandlerUiOperator; +use crate::slint_ui::*; +use crate::{err_with_loc, error::app::AppError}; +use std::sync::Arc; +use tracing::{error, info}; + +pub fn spawn_ui_task( + slint_handler: Arc, + ui_weak: Weak, + cancellation_token: CancellationToken, + shutdown_tx: tokio::sync::mpsc::Sender<()>, +) -> JoinHandle> { + tokio::spawn(async move { + ui_weak + .clone() + .upgrade_in_event_loop(move |ui| { + // Window dragging + ui.on_start_drag_window({ + let ui_weak = ui.as_weak(); + move || { + let _ = ui_weak.upgrade_in_event_loop(|ui| { + let _ = ui.window().with_winit_window( + |winit_window: &winit::window::Window| { + let _ = winit_window.drag_window(); + }, + ); + }); + } + }); + + // Window minimize + ui.on_minimize_window({ + let ui_weak = ui_weak.clone(); + move || { + let _ = ui_weak.upgrade_in_event_loop(|ui| { + let _ = ui.window().with_winit_window( + |winit_window: &winit::window::Window| { + winit_window.set_minimized(true); + }, + ); + }); + } + }); + + // Window maximize/restore + ui.on_maximize_window({ + let ui_weak = ui_weak.clone(); + move || { + let _ = ui_weak.upgrade_in_event_loop(|ui| { + let _ = ui.window().with_winit_window( + |winit_window: &winit::window::Window| { + let is_maximized = winit_window.is_maximized(); + winit_window.set_maximized(!is_maximized); + }, + ); + }); + } + }); + // Theme toggle + ui.on_theme_toggle_clicked({ + let ui_weak = ui_weak.clone(); + move || { + let _ = ui_weak.upgrade_in_event_loop(|ui| { + ui.invoke_toggle_theme(); + info!("Theme toggled"); + }); + } + }); + + // Navigation callback + ui.on_navigation_changed({ + move |page| { + info!("Navigation changed to: {}", page); + } + }); + + let new_tokens_handler = slint_handler.clone(); + ui.on_clear_new_tokens(move || { + let handler = new_tokens_handler.clone(); + handler.clear_new_tokens(); + }); + + let cex_tokens_handler = slint_handler.clone(); + ui.on_clear_cex_tokens(move || { + let handler = cex_tokens_handler.clone(); + handler.clear_cex_tokens(); + }); + + let analysis_tokens_handler = slint_handler.clone(); + ui.on_clear_analysis_tokens(move || { + let handler = analysis_tokens_handler.clone(); + handler.clear_analysis_tokens(); + }); + + // Other callbacks (placeholder implementations) + ui.on_logout_clicked({ + move || { + info!("Logout clicked"); + } + }); + + // Window close - with proper task cleanup + ui.on_close_window({ + let ui_weak = ui_weak.clone(); + let shutdown_tx = shutdown_tx.clone(); + let cancellation_token = cancellation_token.clone(); + move || { + let _ = ui_weak.upgrade_in_event_loop({ + let shutdown_tx = shutdown_tx.clone(); + let cancellation_token = cancellation_token.clone(); + move |ui| { + info!("close_window::shutting_down_all_tasks"); + + // Signal shutdown to all subscriber tasks + cancellation_token.cancel(); + + // Hide window immediately for responsive UI + let _ = ui.window().hide(); + + // Send shutdown signal + let _ = shutdown_tx.try_send(()); + + info!("close_window::all_tasks_cleaned_up"); + + // Quit the event loop after cleanup + let _ = slint::quit_event_loop(); + } + }); + } + }); + }) + .map_err(|e| { + error!("failed_to_setup_ui_callbacks: {}", e); + err_with_loc!(AppError::Slint(format!( + "failed_to_setup_ui_callbacks: {}", + e + ))) + }) + }) +} diff --git a/src/tracing/filter.rs b/src/tracing/filter.rs new file mode 100644 index 0000000..85db26c --- /dev/null +++ b/src/tracing/filter.rs @@ -0,0 +1,73 @@ +use tracing::Level; +use tracing::Metadata; +use tracing_subscriber::layer::Context; +use tracing_subscriber::layer::Filter; +use tracing_subscriber::registry::LookupSpan; + +// Custom filter for exact debug level matching +pub struct DebugOnlyFilter; + +impl Filter for DebugOnlyFilter +where + S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>, +{ + fn enabled(&self, meta: &Metadata<'_>, _ctx: &Context<'_, S>) -> bool { + let target = meta.target(); + meta.level() == &Level::DEBUG && target.starts_with("ziya") + } +} + +// Custom filter for error and warn levels +pub struct ErrorWarnFilter; + +impl Filter for ErrorWarnFilter +where + S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>, +{ + fn enabled(&self, meta: &Metadata<'_>, _ctx: &Context<'_, S>) -> bool { + let target = meta.target(); + (meta.level() == &Level::ERROR || meta.level() == &Level::WARN) + && target.starts_with("ziya") + } +} + +// Custom filter for info levels +#[cfg(feature = "dev")] +pub struct InfoOnlyFilter; + +#[cfg(feature = "dev")] +impl Filter for InfoOnlyFilter +where + S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>, +{ + fn enabled(&self, meta: &Metadata<'_>, _ctx: &Context<'_, S>) -> bool { + let target = meta.target(); + meta.level() == &Level::INFO && target.starts_with("ziya") + } +} + +// Custom filter for error levels +pub struct ErrorOnlyFilter; + +impl Filter for ErrorOnlyFilter +where + S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>, +{ + fn enabled(&self, meta: &Metadata<'_>, _ctx: &Context<'_, S>) -> bool { + let target = meta.target(); + meta.level() == &Level::ERROR && target.starts_with("ziya") + } +} + +// Custom filter for warn levels +pub struct WarnOnlyFilter; + +impl Filter for WarnOnlyFilter +where + S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>, +{ + fn enabled(&self, meta: &Metadata<'_>, _ctx: &Context<'_, S>) -> bool { + let target = meta.target(); + meta.level() == &Level::WARN && target.starts_with("ziya") + } +} diff --git a/src/tracing/format.rs b/src/tracing/format.rs new file mode 100644 index 0000000..3b6013f --- /dev/null +++ b/src/tracing/format.rs @@ -0,0 +1,60 @@ +use tracing::Event; +use tracing_subscriber::fmt::format::Writer; +use tracing_subscriber::fmt::FmtContext; +use tracing_subscriber::fmt::FormatEvent; +use tracing_subscriber::fmt::FormatFields; +use tracing_subscriber::registry::LookupSpan; + +pub struct ZiyaFormat { + pub app_name: String, +} + +// Implement Clone for ZiyaFormat +impl Clone for ZiyaFormat { + fn clone(&self) -> Self { + Self { + app_name: self.app_name.clone(), + } + } +} + +impl FormatEvent for ZiyaFormat +where + S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>, + N: for<'writer> FormatFields<'writer> + 'static, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> std::fmt::Result { + // To get the message, we need to format the fields with a special visitor + let metadata = event.metadata(); + let file = metadata.file().unwrap_or("unknown"); + let line = metadata.line().unwrap_or(0); + + if file == "unknown" && !cfg!(feature = "deep-trace") { + return Ok(()); + } + + let utc_timestamp = chrono::Utc::now(); + let jakarta_timestamp = utc_timestamp.with_timezone(&chrono_tz::Asia::Jakarta); + let timestamp = jakarta_timestamp.format("%Y-%m-%d %H:%M:%S"); + + write!( + writer, + "{} {}::{}::{}::{}::", + metadata.level(), + timestamp, + self.app_name, + file, + line + )?; + + // Format the actual message + ctx.field_format().format_fields(writer.by_ref(), event)?; + + writeln!(writer) + } +} diff --git a/src/tracing/mod.rs b/src/tracing/mod.rs new file mode 100644 index 0000000..2fcf646 --- /dev/null +++ b/src/tracing/mod.rs @@ -0,0 +1,187 @@ +pub mod filter; +pub mod format; + +pub use tracing::*; + +use std::path::Path; + +use self::filter::DebugOnlyFilter; +use self::filter::ErrorOnlyFilter; +use self::filter::ErrorWarnFilter; +#[cfg(feature = "dev")] +use self::filter::InfoOnlyFilter; +use self::format::ZiyaFormat; + +use tracing_appender::rolling::RollingFileAppender; +use tracing_appender::rolling::Rotation; +use tracing_subscriber::prelude::*; +use tracing_subscriber::Layer; + +use crate::config::Config; +use crate::err_with_loc; +use crate::error::app::AppError; +use crate::error::Result; + +pub async fn setup_tracing(config: Config, app_name: &str) -> Result<()> { + // Get logging config + let logging_config = config.logging; + + // Base logs directory + let base_logs_dir = Path::new(&logging_config.directory); + + // Create logs directories if they don't exist + let logs_dirs = [ + base_logs_dir, + &base_logs_dir.join("debug"), + &base_logs_dir.join("error"), + ]; + + for dir in &logs_dirs { + if !dir.exists() { + std::fs::create_dir_all(dir).map_err(|e| { + err_with_loc!(AppError::Config(format!( + "Failed to create logs directory {}: {}", + dir.display(), + e + ))) + })?; + } + } + + // Create file appenders for each log level + #[cfg(feature = "dev")] + let info_appender = + RollingFileAppender::new(Rotation::DAILY, base_logs_dir, format!("{}.log", app_name)); + + let debug_appender = RollingFileAppender::new( + Rotation::DAILY, + base_logs_dir.join("debug"), + format!("{}.log", app_name), + ); + + let error_appender = RollingFileAppender::new( + Rotation::DAILY, + base_logs_dir.join("error"), + format!("{}.log", app_name), + ); + + // Create non-blocking writers + #[cfg(feature = "dev")] + let (non_blocking_info, info_guard) = tracing_appender::non_blocking(info_appender); + let (non_blocking_debug, debug_guard) = tracing_appender::non_blocking(debug_appender); + let (non_blocking_error, error_guard) = tracing_appender::non_blocking(error_appender); + + // Store the guards in statics to keep them alive + #[cfg(feature = "dev")] + static mut INFO_GUARD: Option = None; + static mut DEBUG_GUARD: Option = None; + static mut ERROR_GUARD: Option = None; + + // Create the custom format for all outputs + let format = ZiyaFormat { + app_name: app_name.to_string(), + }; + + // Set up the registry with all outputs + let subscriber = tracing_subscriber::registry() + // DEBUG log file - debug only using custom filter + .with( + tracing_subscriber::fmt::Layer::default() + .with_ansi(false) + .with_file(true) + .with_line_number(true) + .with_target(false) + .event_format(format.clone()) + .with_writer(non_blocking_debug) + .with_filter(DebugOnlyFilter), + ) + // ERROR log file - warn and error only + .with( + tracing_subscriber::fmt::Layer::default() + .with_ansi(false) + .with_file(true) + .with_line_number(true) + .with_target(false) + .event_format(format.clone()) + .with_writer(non_blocking_error) + .with_filter(ErrorWarnFilter), + ); + + #[cfg(feature = "prod")] + let subscriber = subscriber + // Terminal output with custom ZiyaFormat - Error only in production + .with( + tracing_subscriber::fmt::Layer::default() + .with_ansi(true) + .with_file(true) + .with_line_number(true) + .with_target(false) + .event_format(format.clone()) + .with_filter(ErrorOnlyFilter), + ); + + #[cfg(feature = "dev")] + let subscriber = subscriber + // Terminal output with custom ZiyaFormat - INFO and above in development + .with( + tracing_subscriber::fmt::Layer::default() + .with_ansi(true) + .with_file(true) + .with_line_number(true) + .with_target(false) + .event_format(format.clone()) + .with_filter(InfoOnlyFilter), + ) + // INFO log file - info and above + .with( + tracing_subscriber::fmt::Layer::default() + .with_ansi(false) + .with_file(true) + .with_line_number(true) + .with_target(false) + .event_format(format.clone()) + .with_writer(non_blocking_info) + .with_filter(InfoOnlyFilter), + ); + + // Set the subscriber as the global default + match tracing::subscriber::set_global_default(subscriber) { + Ok(_) => { + // Store the guards to keep the loggers alive + #[cfg(feature = "dev")] + unsafe { + INFO_GUARD = Some(info_guard); + } + unsafe { + DEBUG_GUARD = Some(debug_guard); + ERROR_GUARD = Some(error_guard); + } + tracing::info!( + "{}_logging_started::info_logs::{}\\{}.log", + app_name, + base_logs_dir.display(), + app_name + ); + tracing::info!( + "{}_logging_started::debug_logs::{}\\debug\\{}.log", + app_name, + base_logs_dir.display(), + app_name + ); + tracing::info!( + "{}_logging_started::error_logs::{}\\error\\{}.log", + app_name, + base_logs_dir.display(), + app_name + ); + Ok(()) + } + Err(e) => { + error!("failed_to_setup_tracing: {}", e); + Err(err_with_loc!(AppError::Config(format!( + "Failed to setup tracing: {}", + e + )))) + } + } +} diff --git a/ui/app/index.slint b/ui/app/index.slint index 52bf75f..e4f596c 100644 --- a/ui/app/index.slint +++ b/ui/app/index.slint @@ -1,325 +1,268 @@ // App Layer - Main Application Entry Point // This is the root of the application following Feature-Sliced Design -// No std-widgets needed since we're using Layout components - -// Import from shared layer (design system, UI kit) -import { Theme } from "../shared/design-system/tokens/theme.slint"; +// Import std-widgets for Palette +import { Palette } from "std-widgets.slint"; +import { NewTokenUiData, CexUpdatedUiData, MaxDepthReachedUiData } from "../shared/types/token.slint"; // Import widgets (standalone UI blocks) import { NavigationWidget } from "../widgets/navigation/index.slint"; +import { TitleBar } from "../widgets/window-controls/ui/title-bar.slint"; // Import pages import { Dashboard } from "../pages/dashboard/index.slint"; +import { HuntingGroundPage } from "../pages/hunting-ground/index.slint"; +import { TradingPage } from "../pages/trading/index.slint"; +import { PortfolioPage } from "../pages/portfolio/index.slint"; +import { MarketsPage } from "../pages/markets/index.slint"; +import { LoginView } from "../pages/auth/index.slint"; +import { LoadingView } from "../shared/ui/index.slint"; -export component App { - width: 100%; - height: 100%; +export component App inherits Window { + title: "Ziya - One Stop Shop for Your Trading Habit"; + min-width: 1280px; + min-height: 720px; - // Application state + // Disable default background + background: transparent; + + // Theme state + in-out property is-dark-mode: false; + + // Application state management + in-out property app-state: "loading"; // "loading", "login", "authenticated" + in-out property is-authenticated: false; + in-out property user-email: ""; + in-out property sidebar-state: "full"; // "full", "icon-only", or "hidden" + + // Loading state + in-out property is-loading: true; + in-out property has-connection-error: false; + in-out property loading-status: "Initializing your trading environment..."; + + // Navigation state in-out property current-page: "Dashboard"; in-out property user-initials: "JD"; - // Callbacks - callback navigation-changed(string); - callback theme-toggle-clicked(); - callback logout-clicked(); - callback buy-clicked(); - callback sell-clicked(); + // Token data + in-out property <[NewTokenUiData]> new-tokens: []; + in-out property <[CexUpdatedUiData]> cex-tokens: []; + in-out property <[MaxDepthReachedUiData]> analysis-tokens: []; + in-out property current-time: 0; + + // Callbacks for application state + callback health-check-completed(bool); // true if healthy, false if error + callback retry-health-check(); + callback login-attempt(string, string); + callback logout-requested(); + callback authenticate-user(string); + + // Callbacks for window controls callback start-drag-window(); callback minimize-window(); callback maximize-window(); callback close-window(); + callback theme-toggle-clicked(); - // Global drag area - covers entire window but sits behind interactive elements - TouchArea { + // Callbacks for navigation + callback navigation-changed(string); + callback toggle-sidebar(); + + // Callbacks for hunting ground + callback refresh-hunting-ground(); + callback clear-column(string); + + // Other callbacks + callback logout-clicked(); + callback buy-clicked(); + callback sell-clicked(); + callback clear-new-tokens(); + callback clear-cex-tokens(); + callback clear-analysis-tokens(); + + Rectangle { + background: Palette.background; width: 100%; height: 100%; - z: -1; - moved => { - if (self.pressed) { - root.start-drag-window(); - } - } - } - - VerticalLayout { - spacing: 0px; - - // Title Bar - fixed height - Rectangle { - height: 32px; - background: Theme.surface; + // Apply overflow hidden to prevent scrolling + clip: true; + + // Loading Screen + if app-state == "loading": LoadingView { + is-loading: root.is-loading; + has-error: root.has-connection-error; + status-text: root.loading-status; - HorizontalLayout { - padding: 4px; - spacing: 8px; - - // Draggable area (left side) - takes most of the space - Rectangle { - horizontal-stretch: 1; - - Text { - text: "Ziya Trading Platform"; - color: Theme.text-primary; - font-size: 14px; - font-weight: 600; - vertical-alignment: center; - x: 8px; - } - } - - // Theme Toggle Button - fixed size - Rectangle { - width: 40px; - height: 20px; - border-radius: 10px; - background: Theme.is-dark-mode ? Theme.primary : Theme.border; - - Rectangle { - width: 16px; - height: 16px; - border-radius: 8px; - background: white; - x: Theme.is-dark-mode ? 22px : 2px; - y: 2px; - - animate x { duration: 200ms; easing: ease-in-out; } - } - - TouchArea { - clicked => { - theme-toggle-clicked(); - } - } - } - - // Window Controls - fixed size - HorizontalLayout { - spacing: 2px; - - // Minimize Button - Rectangle { - width: 28px; - height: 24px; - border-radius: 2px; - background: minimize-area.pressed ? #30404040 : minimize-area.has-hover ? #20404040 : transparent; - - Text { - text: "−"; - color: Theme.text-primary; - font-size: 14px; - font-weight: 400; - horizontal-alignment: center; - vertical-alignment: center; - } - - minimize-area := TouchArea { - clicked => { minimize-window(); } - } - } - - // Maximize Button - Rectangle { - width: 28px; - height: 24px; - border-radius: 2px; - background: maximize-area.pressed ? #30404040 : maximize-area.has-hover ? #20404040 : transparent; - - Text { - text: "□"; - color: Theme.text-primary; - font-size: 12px; - font-weight: 400; - horizontal-alignment: center; - vertical-alignment: center; - } - - maximize-area := TouchArea { - clicked => { maximize-window(); } - } - } - - // Close Button - Rectangle { - width: 28px; - height: 24px; - border-radius: 2px; - background: close-area.pressed ? #E53E3E : close-area.has-hover ? #C53030 : transparent; - - Text { - text: "×"; - color: close-area.has-hover ? white : Theme.text-primary; - font-size: 16px; - font-weight: 400; - horizontal-alignment: center; - vertical-alignment: center; - } - - close-area := TouchArea { - clicked => { close-window(); } - } - } - } + retry-connection => { + root.retry-health-check(); } } - // Main content area - stretches to fill remaining vertical space - HorizontalLayout { + // Login Screen + if app-state == "login": VerticalLayout { spacing: 0px; - // Navigation Widget (Sidebar) - fixed width - NavigationWidget { - width: 280px; - current-page: root.current-page; - user-initials: root.user-initials; - - navigation-changed(page) => { - root.current-page = page; - root.navigation-changed(page); + // Title Bar (fixed height) + TitleBar { + height: 40px; + is-dark-theme: root.is-dark-mode; + minimize-window => { + root.minimize-window(); + } + maximize-window => { + root.maximize-window(); + } + close-window => { + root.close-window(); + } + toggle-theme => { + root.is-dark-mode = !root.is-dark-mode; + if (root.is-dark-mode) { + Palette.color-scheme = ColorScheme.dark; + } else { + Palette.color-scheme = ColorScheme.light; + } + root.theme-toggle-clicked(); } - - logout-clicked => { root.logout-clicked(); } } - // Main content area - stretches to fill remaining horizontal space - Rectangle { - background: Theme.background; + LoginView { + login-clicked(email, password) => { + root.login-attempt(email, password); + } - // Page routing based on current-page - if current-page == "Dashboard": Dashboard { + navigate-to-dashboard => { + // Demo mode - skip login + root.authenticate-user("demo@ziya.trading"); + } + + back-to-loading => { + root.app-state = "loading"; + root.retry-health-check(); + } + } + } + + // Main Application (Authenticated State) + if app-state == "authenticated": VerticalLayout { + spacing: 0px; + // Title Bar (fixed height) + TitleBar { + height: 40px; + is-dark-theme: root.is-dark-mode; + minimize-window => { + root.minimize-window(); + } + maximize-window => { + root.maximize-window(); + } + close-window => { + root.close-window(); + } + toggle-theme => { + root.is-dark-mode = !root.is-dark-mode; + if (root.is-dark-mode) { + Palette.color-scheme = ColorScheme.dark; + } else { + Palette.color-scheme = ColorScheme.light; + } + root.theme-toggle-clicked(); + } + } + + // Main Content Area (stretches to fill remaining space) + HorizontalLayout { + spacing: 0px; + + // Navigation Sidebar (different states) + if sidebar-state == "full": NavigationWidget { + width: 280px; + current-page: root.current-page; user-initials: root.user-initials; - logout => { root.logout-clicked(); } - trade-buy(asset, amount) => { root.buy-clicked(); } - trade-sell(asset, amount) => { root.sell-clicked(); } - } - - if current-page == "Trading": Rectangle { - background: Theme.background; - - VerticalLayout { - padding: 24px; - spacing: 16px; - alignment: center; - - Text { - text: "🎯 Trading Interface"; - font-size: 32px; - font-weight: 700; - color: Theme.text-primary; - horizontal-alignment: center; - } - - Text { - text: "Advanced trading features coming soon..."; - font-size: 18px; - color: Theme.text-secondary; - horizontal-alignment: center; - } + sidebar-state: root.sidebar-state; + navigation-changed(page) => { + root.current-page = page; + root.navigation-changed(page); + } + logout-clicked => { + root.logout-requested(); + } + toggle-sidebar => { + root.sidebar-state = "icon-only"; } } - if current-page == "Portfolio": Rectangle { - background: Theme.background; - - VerticalLayout { - padding: 24px; - spacing: 16px; - alignment: center; - - Text { - text: "💼 Portfolio Management"; - font-size: 32px; - font-weight: 700; - color: Theme.text-primary; - horizontal-alignment: center; - } - - Text { - text: "Portfolio tracking and management features coming soon..."; - font-size: 18px; - color: Theme.text-secondary; - horizontal-alignment: center; - } + if sidebar-state == "icon-only": NavigationWidget { + width: 80px; + current-page: root.current-page; + user-initials: root.user-initials; + sidebar-state: root.sidebar-state; + navigation-changed(page) => { + root.current-page = page; + root.navigation-changed(page); + } + logout-clicked => { + root.logout-requested(); + } + toggle-sidebar => { + root.sidebar-state = "full"; } } - if current-page == "Markets": Rectangle { - background: Theme.background; + // Page Content (stretches to fill remaining space) + Rectangle { + background: Palette.background; + // Content container with proper centering VerticalLayout { - padding: 24px; - spacing: 16px; - alignment: center; + alignment: stretch; - Text { - text: "📈 Market Data"; - font-size: 32px; - font-weight: 700; - color: Theme.text-primary; - horizontal-alignment: center; + // Route to different pages + if current-page == "Dashboard": Dashboard { + user-initials: root.user-initials; + logout => { + root.logout-requested(); + } } - Text { - text: "Real-time market data and analysis coming soon..."; - font-size: 18px; - color: Theme.text-secondary; - horizontal-alignment: center; - } - } - } - - if current-page == "HuntingGround": Rectangle { - background: Theme.background; - - VerticalLayout { - padding: 24px; - spacing: 16px; - alignment: center; - - Text { - text: "🎯 Token Hunting Ground"; - font-size: 32px; - font-weight: 700; - color: Theme.text-primary; - horizontal-alignment: center; + if current-page == "Hunting Ground": HuntingGroundPage { + new-tokens: root.new-tokens; + cex-tokens: root.cex-tokens; + analysis-tokens: root.analysis-tokens; + current-time: root.current-time; + + clear-new-tokens => { + root.clear-new-tokens(); + } + + clear-cex-tokens => { + root.clear-cex-tokens(); + } + + clear-analysis-tokens => { + root.clear-analysis-tokens(); + } } - Text { - text: "Discover and track new tokens coming soon..."; - font-size: 18px; - color: Theme.text-secondary; - horizontal-alignment: center; - } - } - } - - if current-page == "Profile": Rectangle { - background: Theme.background; - - VerticalLayout { - padding: 24px; - spacing: 16px; - alignment: center; - - Text { - text: "👤 User Profile"; - font-size: 32px; - font-weight: 700; - color: Theme.text-primary; - horizontal-alignment: center; + if current-page == "Trading": TradingPage { + buy-clicked => { + root.buy-clicked(); + } + sell-clicked => { + root.sell-clicked(); + } } - Text { - text: "Profile management and settings coming soon..."; - font-size: 18px; - color: Theme.text-secondary; - horizontal-alignment: center; + if current-page == "Portfolio": PortfolioPage { + } + + if current-page == "Markets": MarketsPage { } } } } } } -} \ No newline at end of file + +} diff --git a/ui/index.slint b/ui/index.slint index 458c015..9b46ea2 100644 --- a/ui/index.slint +++ b/ui/index.slint @@ -3,7 +3,8 @@ // Following the pattern from moonlogs: index -> app -> pages/widgets/entities/shared import { App } from "app/index.slint"; -import { Theme } from "shared/design-system/index.slint"; +import { Palette } from "std-widgets.slint"; +import { NewTokenUiData, CexUpdatedUiData, MaxDepthReachedUiData } from "shared/types/token.slint"; export component MainWindow inherits Window { // Window properties @@ -12,43 +13,141 @@ export component MainWindow inherits Window { min-width: 1080px; min-height: 800px; no-frame: true; - background: Theme.background; + background: Palette.background; - // Properties + // Theme state + in-out property is-dark-mode: true; + + // Application state management + in-out property app-state: "loading"; // "loading", "login", "authenticated" + in-out property is-authenticated: false; + in-out property user-email: ""; + in-out property sidebar-state: "full"; // "full", "icon-only", or "hidden" + + // Loading state + in-out property is-loading: true; + in-out property has-connection-error: false; + in-out property loading-status: "Initializing your trading environment..."; + + // Navigation state in-out property current-page: "Dashboard"; in-out property user-initials: "JD"; - // Callbacks that will be handled by main.rs - callback navigation-changed(string); - callback theme-toggle-clicked(); - callback logout-clicked(); - callback buy-clicked(); - callback sell-clicked(); + // Hunting ground properties - using correct types + in-out property <[NewTokenUiData]> new-tokens: []; + in-out property <[CexUpdatedUiData]> cex-tokens: []; + in-out property <[MaxDepthReachedUiData]> analysis-tokens: []; + in-out property current-time: 0; + + // Callbacks for application state + callback health-check-completed(bool); // true if healthy, false if error + callback retry-health-check(); + callback login-attempt(string, string); + callback logout-requested(); + callback authenticate-user(string); + + // Callbacks for window controls callback start-drag-window(); callback minimize-window(); callback maximize-window(); callback close-window(); + callback theme-toggle-clicked(); + + // Callbacks for navigation + callback navigation-changed(string); + callback toggle-sidebar(); + + // Callbacks for hunting ground + callback refresh-hunting-ground(); + callback clear-column(string); + + // Other callbacks + callback logout-clicked(); + callback buy-clicked(); + callback sell-clicked(); + callback clear-new-tokens(); + callback clear-cex-tokens(); + callback clear-analysis-tokens(); // Public function that can be called from Rust to toggle theme public function toggle_theme() { - Theme.is-dark-mode = !Theme.is-dark-mode; - debug("Theme toggled from Rust. New state: " + (Theme.is-dark-mode ? "dark" : "light")); + root.is-dark-mode = !root.is-dark-mode; + if (root.is-dark-mode) { + Palette.color-scheme = ColorScheme.dark; + } else { + Palette.color-scheme = ColorScheme.light; + } + debug("Theme toggled from Rust. New state: " + (root.is-dark-mode ? "dark" : "light")); } + TouchArea { + width: 100%; + height: 100%; + moved => { + if (self.pressed) { + start-drag-window(); + } + } + } + // App component handles all the UI following FSD layers App { width: 100%; height: 100%; - current-page: root.current-page; + + // Theme state + is-dark-mode: root.is-dark-mode; + + // Application state management + app-state: root.app-state; + is-authenticated: root.is-authenticated; + user-email: root.user-email; + sidebar-state: root.sidebar-state; + + // Loading state + is-loading: root.is-loading; + has-connection-error: root.has-connection-error; + loading-status: root.loading-status; + + // Navigation state + current-page: root.current-page; user-initials: root.user-initials; - + + // Hunting ground properties + new-tokens: root.new-tokens; + cex-tokens: root.cex-tokens; + analysis-tokens: root.analysis-tokens; + current-time: root.current-time; + // Forward all callbacks to main.rs + health-check-completed(healthy) => { + root.health-check-completed(healthy); + } + retry-health-check => { + root.retry-health-check(); + } + login-attempt(email, password) => { + root.login-attempt(email, password); + } + logout-requested => { + root.logout-requested(); + } + authenticate-user(email) => { + root.authenticate-user(email); + } navigation-changed(page) => { root.current-page = page; root.navigation-changed(page); } + toggle-sidebar => { + if (root.sidebar-state == "full") { + root.sidebar-state = "icon-only"; + } else { + root.sidebar-state = "full"; + } + } theme-toggle-clicked => { - root.theme-toggle-clicked(); + root.toggle_theme(); } logout-clicked => { root.logout-clicked(); @@ -71,5 +170,17 @@ export component MainWindow inherits Window { close-window => { root.close-window(); } + refresh-hunting-ground => { + root.refresh-hunting-ground(); + } + clear-new-tokens => { + root.clear-new-tokens(); + } + clear-cex-tokens => { + root.clear-cex-tokens(); + } + clear-analysis-tokens => { + root.clear-analysis-tokens(); + } } } diff --git a/ui/pages/auth/ui/login-page.slint b/ui/pages/auth/ui/login-page.slint index ed23e01..084d0bc 100644 --- a/ui/pages/auth/ui/login-page.slint +++ b/ui/pages/auth/ui/login-page.slint @@ -1,153 +1,135 @@ -import { Button, VerticalBox, HorizontalBox, LineEdit } from "std-widgets.slint"; +import { Palette, Button, VerticalBox, HorizontalBox, LineEdit } from "std-widgets.slint"; +import { TitleBar } from "../../../widgets/window-controls/ui/title-bar.slint"; // Login component with branded interface export component LoginView { - in-out property primary-color: #2563eb; - in-out property background-color: #f8fafc; - in-out property text-color: #1e293b; - in-out property app-version: "0.2.0"; + width: 100%; + height: 100%; + in-out property is-dark-mode: true; - // Login form state - in-out property email: ""; - in-out property password: ""; - - // Callbacks + // Define the input properties for passing data to parent callback login-clicked(string, string); callback navigate-to-dashboard(); - - Rectangle { - background: background-color; + callback back-to-loading(); + callback minimize-window(); + callback maximize-window(); + callback close-window(); + callback theme-toggle-clicked(); + + VerticalLayout { + alignment: center; + spacing: 32px; + padding: 40px; - VerticalBox { - alignment: center; - spacing: 30px; - padding: 40px; + Rectangle { + width: 100%; + height: 100%; + background: Palette.background; - // Logo and branding section - VerticalBox { - alignment: center; - spacing: 10px; - - Rectangle { - width: 80px; - height: 80px; - border-radius: 40px; - background: primary-color.with-alpha(0.1); - - // Logo placeholder - Rectangle { - width: 40px; - height: 40px; - background: primary-color; - border-radius: 20px; - } - } - - Text { - text: "Ziya"; - font-size: 48px; - color: text-color; - font-weight: 700; - } - - Text { - text: "/dˤiˈjaːʔ/, \"zee‑yah\" — Proper noun, meaning \"light\""; - font-size: 14px; - color: text-color; - opacity: 0.6; - } - - Text { - text: "One stop shop trading solution"; - font-size: 20px; - color: text-color; - opacity: 0.8; - } - - Text { - text: "A bismillahDAO creation"; - font-size: 12px; - color: primary-color; - font-weight: 600; - } - } - - // Login form section - VerticalBox { - alignment: center; - spacing: 20px; + // Center container + Rectangle { width: 400px; + height: 500px; - // Email field - VerticalBox { - spacing: 8px; - - Text { - text: "Email"; - color: text-color; - font-weight: 500; - } - - LineEdit { - placeholder-text: "Enter your email"; - text <=> email; - height: 45px; - } - } + background: Palette.alternate-background; + border-radius: 16px; + drop-shadow-blur: 24px; + drop-shadow-color: #000000.with-alpha(0.1); + border-width: 1px; + border-color: Palette.border; - // Password field - VerticalBox { - spacing: 8px; - - Text { - text: "Password"; - color: text-color; - font-weight: 500; - } - - LineEdit { - placeholder-text: "Enter your password"; - text <=> password; - height: 45px; - } - } - - // Login button - Button { - text: "Sign In"; - primary: true; - height: 50px; - clicked => { - login-clicked(email, password); - } - } - - // Quick access button - HorizontalBox { - spacing: 10px; + VerticalLayout { alignment: center; + spacing: 24px; + padding: 32px; - Text { - text: "Quick access:"; - color: text-color; - opacity: 0.7; - font-size: 12px; - } - - Button { - text: "Demo Mode"; - height: 35px; - clicked => { - navigate-to-dashboard(); + // Logo/Icon + Rectangle { + width: 80px; + height: 80px; + border-radius: 40px; + background: Palette.accent-background; + + Text { + text: "🚀"; + font-size: 40px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + // Title + Text { + text: "Ziya"; + font-size: 32px; + font-weight: 700; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + // Subtitle + Text { + text: "Your one-stop trading platform"; + font-size: 16px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + opacity: 0.7; + } + + // Form container + VerticalLayout { + spacing: 16px; + width: 100%; + + // Email field + LineEdit { + placeholder-text: "Enter your email"; + width: 100%; + height: 44px; + } + + // Password field + LineEdit { + placeholder-text: "Enter your password"; + input-type: password; + width: 100%; + height: 44px; + } + + // Login button + Button { + text: "Sign In"; + width: 100%; + height: 44px; + clicked => { + root.login-clicked("demo@ziya.trading", "password123"); + } + } + + // Demo button for development + Button { + text: "Continue to Demo"; + width: 100%; + height: 44px; + clicked => { + root.navigate-to-dashboard(); + } + } + } + + // Back to connection check + Text { + text: "← Back to Connection Check"; + font-size: 14px; + color: Palette.accent-background; + horizontal-alignment: center; + + TouchArea { + clicked => { + root.back-to-loading(); + } } } - } - - Text { - text: "Version " + app-version; - font-size: 10px; - color: text-color; - opacity: 0.5; } } } diff --git a/ui/pages/dashboard/ui/dashboard-page.slint b/ui/pages/dashboard/ui/dashboard-page.slint index 0c04655..9352ad2 100644 --- a/ui/pages/dashboard/ui/dashboard-page.slint +++ b/ui/pages/dashboard/ui/dashboard-page.slint @@ -1,7 +1,6 @@ -import { ScrollView, Button, LineEdit, ComboBox } from "std-widgets.slint"; -import { Theme } from "../../../shared/design-system/tokens/theme.slint"; +import { ScrollView, Button, LineEdit, ComboBox, Palette } from "std-widgets.slint"; -// Professional dashboard component with global theme support +// Professional dashboard component with global palette support export component Dashboard { width: 100%; height: 100%; @@ -12,429 +11,107 @@ export component Dashboard { callback trade-buy(string, string); callback trade-sell(string, string); - // Let the parent layout handle sizing - - VerticalLayout { - spacing: 0px; + Rectangle { + background: Palette.background; - // Top navbar using HorizontalLayout for proper layout - Rectangle { - height: 64px; - background: Theme.surface; - border-width: 1px; - border-color: Theme.border; + VerticalLayout { + alignment: center; + spacing: 20px; + padding: 40px; - HorizontalLayout { - padding: 16px; - alignment: stretch; + // Icon + Rectangle { + width: 80px; + height: 80px; + border-radius: 40px; + background: Palette.accent-background.with-alpha(0.1); Text { - text: "Ziya Dashboard"; - color: Theme.text-primary; - font-size: 20px; - font-weight: 700; + text: "📊"; + font-size: 40px; + horizontal-alignment: center; vertical-alignment: center; } - - // Spacer to push user profile to the right - Rectangle { - horizontal-stretch: 1; - } - - // User profile button - Rectangle { - width: 40px; - height: 40px; - background: Theme.primary; - border-radius: 20px; - - Text { - text: user-initials; - color: white; - font-size: 14px; - font-weight: 700; - horizontal-alignment: center; - vertical-alignment: center; - } - - TouchArea { - clicked => { logout(); } - } - } } - } + + // Title + Text { + text: "Dashboard"; + font-size: 48px; + font-weight: 700; + color: Palette.foreground; + horizontal-alignment: center; + } - // Dashboard content - no scrollbar, just layout - VerticalLayout { - vertical-stretch: 1; - padding: 24px; - spacing: 24px; - - // Stats cards row using HorizontalLayout with proper stretch - HorizontalLayout { - spacing: 24px; - alignment: stretch; - - // Total Balance - Rectangle { - horizontal-stretch: 1; - background: Theme.card-background; - border-radius: 12px; - border-width: 1px; - border-color: Theme.card-border; - drop-shadow-blur: 8px; - drop-shadow-color: Theme.is-dark-mode ? #00000020 : #00000008; - - VerticalLayout { - padding: 20px; - spacing: 8px; - - Text { - text: "Total Balance"; - color: Theme.text-secondary; - font-size: 14px; - font-weight: 500; - horizontal-alignment: left; - } - - Text { - text: "$25,600"; - color: Theme.primary; - font-size: 32px; - font-weight: 700; - horizontal-alignment: left; - } - - Text { - text: "↗︎ 12% (30d)"; - color: Theme.success; - font-size: 13px; - font-weight: 500; - horizontal-alignment: left; - } - } - } - - // Active Positions - Rectangle { - horizontal-stretch: 1; - background: Theme.card-background; - border-radius: 12px; - border-width: 1px; - border-color: Theme.card-border; - drop-shadow-blur: 8px; - drop-shadow-color: Theme.is-dark-mode ? #00000020 : #00000008; - - VerticalLayout { - padding: 20px; - spacing: 8px; - - Text { - text: "Active Positions"; - color: Theme.text-secondary; - font-size: 14px; - font-weight: 500; - horizontal-alignment: left; - } - - Text { - text: "8"; - color: Theme.warning; - font-size: 32px; - font-weight: 700; - horizontal-alignment: left; - } - - Text { - text: "↗︎ 2 new today"; - color: Theme.success; - font-size: 13px; - font-weight: 500; - horizontal-alignment: left; - } - } - } - - // P&L Today - Rectangle { - horizontal-stretch: 1; - background: Theme.card-background; - border-radius: 12px; - border-width: 1px; - border-color: Theme.card-border; - drop-shadow-blur: 8px; - drop-shadow-color: Theme.is-dark-mode ? #00000020 : #00000008; - - VerticalLayout { - padding: 20px; - spacing: 8px; - - Text { - text: "P&L Today"; - color: Theme.text-secondary; - font-size: 14px; - font-weight: 500; - horizontal-alignment: left; - } - - Text { - text: "+$450"; - color: Theme.success; - font-size: 32px; - font-weight: 700; - horizontal-alignment: left; - } - - Text { - text: "↗︎ +2.1%"; - color: Theme.success; - font-size: 13px; - font-weight: 500; - horizontal-alignment: left; - } - } - } + // Coming soon message + Text { + text: "Coming Soon"; + font-size: 24px; + font-weight: 500; + color: Palette.foreground; + horizontal-alignment: center; } - - // Trading interface row using HorizontalLayout - HorizontalLayout { - spacing: 24px; - alignment: stretch; - - // Quick Trade card - Rectangle { - horizontal-stretch: 2; - background: Theme.card-background; - border-radius: 16px; - border-width: 1px; - border-color: Theme.card-border; - drop-shadow-blur: 16px; - drop-shadow-color: Theme.is-dark-mode ? #00000020 : #00000008; - - VerticalLayout { - padding: 24px; - spacing: 20px; - - Text { - text: "Quick Trade"; - color: Theme.text-primary; - font-size: 18px; - font-weight: 700; - horizontal-alignment: left; - } - - // Token selector using HorizontalLayout - HorizontalLayout { - spacing: 12px; - alignment: start; - Text { - text: "Token:"; - color: Theme.text-secondary; - font-size: 14px; - vertical-alignment: center; - } - - ComboBox { - model: ["SOL", "BTC", "ETH", "USDC"]; - current-value: "SOL"; - } - } - - // Amount input using HorizontalLayout - HorizontalLayout { - spacing: 12px; - alignment: start; - - Text { - text: "Amount:"; - color: Theme.text-secondary; - font-size: 14px; - vertical-alignment: center; - } - - LineEdit { - placeholder-text: "0.00"; - horizontal-stretch: 1; - } - } - - // Trade buttons using HorizontalLayout - HorizontalLayout { - spacing: 12px; - alignment: stretch; - - Button { - text: "Buy"; - primary: true; - horizontal-stretch: 1; - clicked => { - trade-buy("SOL", "100"); - } - } - - Button { - text: "Sell"; - horizontal-stretch: 1; - clicked => { - trade-sell("SOL", "50"); - } - } - } - } - } - - // Recent Activity card - Rectangle { - horizontal-stretch: 1; - background: Theme.card-background; - border-radius: 16px; - border-width: 1px; - border-color: Theme.card-border; - drop-shadow-blur: 16px; - drop-shadow-color: Theme.is-dark-mode ? #00000020 : #00000008; - - VerticalLayout { - padding: 24px; - spacing: 16px; - - Text { - text: "Recent Activity"; - color: Theme.text-primary; - font-size: 18px; - font-weight: 700; - horizontal-alignment: left; - } - - // Activity items using VerticalLayout - VerticalLayout { - spacing: 12px; - - // Activity item 1 - HorizontalLayout { - spacing: 12px; - alignment: stretch; - - Rectangle { - width: 8px; - height: 8px; - border-radius: 4px; - background: Theme.success; - } - - VerticalLayout { - spacing: 2px; - horizontal-stretch: 1; - - Text { - text: "Bought 2.5 SOL"; - color: Theme.text-primary; - font-size: 14px; - font-weight: 500; - } - - Text { - text: "2 minutes ago"; - color: Theme.text-secondary; - font-size: 12px; - } - } - } - - // Activity item 2 - HorizontalLayout { - spacing: 12px; - alignment: stretch; - - Rectangle { - width: 8px; - height: 8px; - border-radius: 4px; - background: Theme.error; - } - - VerticalLayout { - spacing: 2px; - horizontal-stretch: 1; - - Text { - text: "Sold 1.0 ETH"; - color: Theme.text-primary; - font-size: 14px; - font-weight: 500; - } - - Text { - text: "1 hour ago"; - color: Theme.text-secondary; - font-size: 12px; - } - } - } - - // Activity item 3 - HorizontalLayout { - spacing: 12px; - alignment: stretch; - - Rectangle { - width: 8px; - height: 8px; - border-radius: 4px; - background: Theme.warning; - } - - VerticalLayout { - spacing: 2px; - horizontal-stretch: 1; - - Text { - text: "Staked 100 USDC"; - color: Theme.text-primary; - font-size: 14px; - font-weight: 500; - } - - Text { - text: "3 hours ago"; - color: Theme.text-secondary; - font-size: 12px; - } - } - } - } - } - } + // Description + Text { + text: "We're working hard to bring you an amazing dashboard experience.\nCheck back soon for portfolio insights, trading analytics, and more!"; + font-size: 16px; + color: Palette.foreground; + horizontal-alignment: center; + opacity: 0.8; } - - // Theme demonstration section + + // Placeholder for future features Rectangle { - background: Theme.surface; + width: 400px; + height: 200px; + background: Palette.alternate-background; border-radius: 12px; border-width: 1px; - border-color: Theme.border; + border-color: Palette.border; VerticalLayout { - padding: 20px; - spacing: 12px; - + alignment: center; + spacing: 10px; + Text { - text: "Theme Status"; - color: Theme.text-primary; - font-size: 16px; + text: "✨ Coming Features:"; + font-size: 18px; font-weight: 600; + color: Palette.alternate-foreground; + horizontal-alignment: center; } - - Text { - text: "Current theme: " + (Theme.is-dark-mode ? "Dark Mode 🌙" : "Light Mode ☀️"); - color: Theme.text-secondary; - font-size: 14px; - } - - Text { - text: "All components are now using the global theme system for consistent theming across the application."; - color: Theme.text-secondary; - font-size: 12px; + + VerticalLayout { + spacing: 8px; + + Text { + text: "• Real-time Portfolio Tracking"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Advanced Trading Analytics"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Performance Metrics"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Custom Widgets"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } } } } diff --git a/ui/pages/hunting-ground/ui/hunting-ground-page.slint b/ui/pages/hunting-ground/ui/hunting-ground-page.slint index aa28c65..4a71fdd 100644 --- a/ui/pages/hunting-ground/ui/hunting-ground-page.slint +++ b/ui/pages/hunting-ground/ui/hunting-ground-page.slint @@ -1,29 +1,79 @@ -import { VerticalBox } from "std-widgets.slint"; -import { Theme } from "../../../shared/design-system/tokens/theme.slint"; +import { Palette, ScrollView } from "std-widgets.slint"; + +// Import proper token data structs from shared types +import { NewTokenUiData, CexUpdatedUiData, MaxDepthReachedUiData } from "../../../shared/types/token.slint"; + +// Import the new token column widgets +import { NewTokensColumn, CexTokensColumn, AnalysisCompleteColumn } from "../../../widgets/token-column/ui/mod.slint"; // Hunting Ground Page Component export component HuntingGroundPage { + + // Properties for token data - using correct types + in property <[NewTokenUiData]> new-tokens: []; + in property <[CexUpdatedUiData]> cex-tokens: []; + in property <[MaxDepthReachedUiData]> analysis-tokens: []; + in property current-time: 0; + + callback clear-new-tokens(); + callback clear-cex-tokens(); + callback clear-analysis-tokens(); + Rectangle { - background: Theme.background; + background: Palette.background; - VerticalBox { - padding: 32px; - spacing: 24px; - alignment: center; + VerticalLayout { + alignment: start; // Explicitly align to top + padding: 16px; // Reduced from 24px + spacing: 16px; // Reduced from 24px - Text { - text: "🎯 Hunting Ground"; - color: Theme.text-primary; - font-size: 32px; - font-weight: 700; - horizontal-alignment: center; + // Header with refresh button + HorizontalLayout { + alignment: space-between; + + Text { + text: "🎯 Token Hunting Ground"; + font-size: 28px; + font-weight: 700; + color: Palette.foreground; + } } - Text { - text: "Advanced token hunting and discovery features"; - color: Theme.text-secondary; - font-size: 16px; - horizontal-alignment: center; + // Three columns layout with fixed widths to prevent binding loops + HorizontalLayout { + spacing: 16px; + padding: 8px; + height: 600px; // Fixed height to prevent overflow + + // Column 1: New Tokens (fixed width) + NewTokensColumn { + width: 350px; // Fixed width instead of calculated + height: 100%; + tokens: root.new-tokens; + clear-tokens => { + root.clear-new-tokens(); + } + } + + // Column 2: CEX Analyst (fixed width) + CexTokensColumn { + width: 350px; // Fixed width instead of calculated + height: 100%; + tokens: root.cex-tokens; + clear-tokens => { + root.clear-cex-tokens(); + } + } + + // Column 3: Analysis Complete (fixed width) + AnalysisCompleteColumn { + width: 350px; // Fixed width instead of calculated + height: 100%; + tokens: root.analysis-tokens; + clear-tokens => { + root.clear-analysis-tokens(); + } + } } } } diff --git a/ui/pages/markets/ui/markets-page.slint b/ui/pages/markets/ui/markets-page.slint index d074156..4b73c8a 100644 --- a/ui/pages/markets/ui/markets-page.slint +++ b/ui/pages/markets/ui/markets-page.slint @@ -1,30 +1,113 @@ -import { VerticalBox } from "std-widgets.slint"; -import { Theme } from "../../../shared/design-system/tokens/theme.slint"; +import { Palette } from "std-widgets.slint"; -// Markets Page Component export component MarketsPage { + width: 100%; + height: 100%; + Rectangle { - background: Theme.background; + background: Palette.background; - VerticalBox { - padding: 32px; - spacing: 24px; + VerticalLayout { alignment: center; + spacing: 20px; + padding: 40px; + // Icon + Rectangle { + width: 80px; + height: 80px; + border-radius: 40px; + background: Palette.accent-background.with-alpha(0.1); + + Text { + text: "📊"; + font-size: 40px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + // Title Text { text: "Markets"; - color: Theme.text-primary; - font-size: 32px; + font-size: 48px; font-weight: 700; + color: Palette.foreground; horizontal-alignment: center; } + // Coming soon message Text { - text: "Market data and analysis will be displayed here"; - color: Theme.text-secondary; - font-size: 16px; + text: "Coming Soon"; + font-size: 24px; + font-weight: 500; + color: Palette.foreground; horizontal-alignment: center; } + + // Description + Text { + text: "Real-time market data and analysis tools are on the way.\nStay tuned for comprehensive market insights!"; + font-size: 16px; + color: Palette.foreground; + horizontal-alignment: center; + opacity: 0.8; + } + + // Placeholder for future features + Rectangle { + width: 400px; + height: 200px; + background: Palette.alternate-background; + border-radius: 12px; + border-width: 1px; + border-color: Palette.border; + + VerticalLayout { + alignment: center; + spacing: 10px; + + Text { + text: "🌍 Coming Features:"; + font-size: 18px; + font-weight: 600; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + VerticalLayout { + spacing: 8px; + + Text { + text: "• Live Price Feeds"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Market Cap Rankings"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Technical Analysis Tools"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• News & Market Sentiment"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + } + } + } } } } \ No newline at end of file diff --git a/ui/pages/portfolio/ui/portfolio-page.slint b/ui/pages/portfolio/ui/portfolio-page.slint index 0199cb7..ad9a2cf 100644 --- a/ui/pages/portfolio/ui/portfolio-page.slint +++ b/ui/pages/portfolio/ui/portfolio-page.slint @@ -1,30 +1,114 @@ -import { VerticalBox } from "std-widgets.slint"; -import { Theme } from "../../../shared/design-system/tokens/theme.slint"; +import { Palette } from "std-widgets.slint"; // Portfolio Page Component export component PortfolioPage { + width: 100%; + height: 100%; + Rectangle { - background: Theme.background; + background: Palette.background; - VerticalBox { - padding: 32px; - spacing: 24px; + VerticalLayout { alignment: center; + spacing: 20px; + padding: 40px; + // Icon + Rectangle { + width: 80px; + height: 80px; + border-radius: 40px; + background: Palette.accent-background.with-alpha(0.1); + + Text { + text: "💼"; + font-size: 40px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + // Title Text { text: "Portfolio"; - color: Theme.text-primary; - font-size: 32px; + font-size: 48px; font-weight: 700; + color: Palette.foreground; horizontal-alignment: center; } + // Coming soon message Text { - text: "Your investment portfolio will be displayed here"; - color: Theme.text-secondary; - font-size: 16px; + text: "Coming Soon"; + font-size: 24px; + font-weight: 500; + color: Palette.foreground; horizontal-alignment: center; } + + // Description + Text { + text: "Track your investments and performance with detailed portfolio analytics.\nComprehensive portfolio management tools are coming soon!"; + font-size: 16px; + color: Palette.foreground; + horizontal-alignment: center; + opacity: 0.8; + } + + // Placeholder for future features + Rectangle { + width: 400px; + height: 200px; + background: Palette.alternate-background; + border-radius: 12px; + border-width: 1px; + border-color: Palette.border; + + VerticalLayout { + alignment: center; + spacing: 10px; + + Text { + text: "📊 Coming Features:"; + font-size: 18px; + font-weight: 600; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + VerticalLayout { + spacing: 8px; + + Text { + text: "• Asset Allocation Overview"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Performance Analytics"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Historical P&L Reports"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Portfolio Rebalancing Tools"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + } + } + } } } } \ No newline at end of file diff --git a/ui/pages/trading/ui/trading-page.slint b/ui/pages/trading/ui/trading-page.slint index 36fdbef..89c8044 100644 --- a/ui/pages/trading/ui/trading-page.slint +++ b/ui/pages/trading/ui/trading-page.slint @@ -1,75 +1,113 @@ -import { VerticalBox, HorizontalBox } from "std-widgets.slint"; -import { Theme } from "../../../shared/design-system/tokens/theme.slint"; +import { Palette } from "std-widgets.slint"; -// Trading Page Component export component TradingPage { + width: 100%; + height: 100%; + callback buy-clicked(); callback sell-clicked(); Rectangle { - background: Theme.background; + background: Palette.background; - VerticalBox { - padding: 32px; - spacing: 24px; + VerticalLayout { alignment: center; + spacing: 20px; + padding: 40px; - Text { - text: "Trading Page"; - color: Theme.text-primary; - font-size: 32px; - font-weight: 700; - horizontal-alignment: center; - } - - Text { - text: "Advanced trading features coming soon..."; - color: Theme.text-secondary; - font-size: 16px; - horizontal-alignment: center; - } - - HorizontalBox { - spacing: 16px; - alignment: center; + // Icon + Rectangle { + width: 80px; + height: 80px; + border-radius: 40px; + background: Palette.accent-background.with-alpha(0.1); - Rectangle { - width: 120px; - height: 40px; - background: #10b981; - border-radius: 8px; - - Text { - text: "Buy"; - color: white; - font-size: 14px; - font-weight: 600; - horizontal-alignment: center; - vertical-alignment: center; - } - - TouchArea { - clicked => { buy-clicked(); } - } + Text { + text: "📈"; + font-size: 40px; + horizontal-alignment: center; + vertical-alignment: center; } + } + + // Title + Text { + text: "Trading"; + font-size: 48px; + font-weight: 700; + color: Palette.foreground; + horizontal-alignment: center; + } + + // Coming soon message + Text { + text: "Coming Soon"; + font-size: 24px; + font-weight: 500; + color: Palette.foreground; + horizontal-alignment: center; + } + + // Description + Text { + text: "Advanced trading interface with real-time market data is coming.\nGet ready for seamless trading experience!"; + font-size: 16px; + color: Palette.foreground; + horizontal-alignment: center; + opacity: 0.8; + } + + // Placeholder for future features + Rectangle { + width: 400px; + height: 200px; + background: Palette.alternate-background; + border-radius: 12px; + border-width: 1px; + border-color: Palette.border; - Rectangle { - width: 120px; - height: 40px; - background: #ef4444; - border-radius: 8px; + VerticalLayout { + alignment: center; + spacing: 10px; Text { - text: "Sell"; - color: white; - font-size: 14px; + text: "🚀 Coming Features:"; + font-size: 18px; font-weight: 600; + color: Palette.alternate-foreground; horizontal-alignment: center; - vertical-alignment: center; } - TouchArea { - clicked => { sell-clicked(); } + VerticalLayout { + spacing: 8px; + + Text { + text: "• One-Click Trading"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Advanced Order Types"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Portfolio Integration"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } + + Text { + text: "• Risk Management Tools"; + font-size: 14px; + color: Palette.alternate-foreground; + horizontal-alignment: center; + } } } } diff --git a/ui/shared/config/index.slint b/ui/shared/config/index.slint index 05182d2..5516128 100644 --- a/ui/shared/config/index.slint +++ b/ui/shared/config/index.slint @@ -1,5 +1,5 @@ // Shared Configuration Public API // Export application configuration - + // Currently no config defined // export * from "app-config.slint"; \ No newline at end of file diff --git a/ui/shared/design-system/components/design-system.slint b/ui/shared/design-system/components/design-system.slint deleted file mode 100644 index 51fcafd..0000000 --- a/ui/shared/design-system/components/design-system.slint +++ /dev/null @@ -1,971 +0,0 @@ -// Fluent 2 Design System for Ziya Trading Platform -// Based on Microsoft Fluent 2 Design Language -// https://fluent2.microsoft.design/ - -// Complete Fluent UI Color System - All Themes Support -// Theme mode enumeration -export enum ColorMode { light, dark, team-light, team-dark } - -export global FluentColors { - // Current theme mode - in-out property current-mode: ColorMode.light; - - // Theme switching callback - callback switch-theme(ColorMode); - - // ===== LIGHT THEME COLORS ===== - - // Neutral Colors - Light - out property neutral-foreground-1-light: #242424; - out property neutral-foreground-2-light: #424242; - out property neutral-foreground-3-light: #616161; - out property neutral-foreground-4-light: #757575; - out property neutral-foreground-disabled-light: #bdbdbd; - out property neutral-foreground-inverted-light: #ffffff; - out property neutral-foreground-inverted-2-light: #f5f5f5; - out property neutral-foreground-on-brand-light: #ffffff; - out property neutral-foreground-static-inverted-light: #ffffff; - out property neutral-foreground-inverted-disabled-light: #ffffff; - - // Interactive states - Light theme - out property neutral-foreground-1-hover-light: #424242; - out property neutral-foreground-1-pressed-light: #242424; - out property neutral-foreground-2-hover-light: #616161; - out property neutral-foreground-2-pressed-light: #424242; - out property neutral-foreground-2-brand-hover-light: #0078d4; - out property neutral-foreground-2-brand-pressed-light: #005a9e; - out property neutral-foreground-2-brand-selected-light: #0078d4; - - // Link colors - Light theme - out property neutral-foreground-link-light: #0078d4; - out property neutral-foreground-link-hover-light: #106ebe; - out property neutral-foreground-link-pressed-light: #005a9e; - out property neutral-foreground-link-selected-light: #0078d4; - - out property neutral-background-1-light: #ffffff; - out property neutral-background-2-light: #fafafa; - out property neutral-background-3-light: #f5f5f5; - out property neutral-background-4-light: #f0f0f0; - out property neutral-background-5-light: #ebebeb; - out property neutral-background-6-light: #e1e1e1; - out property neutral-background-inverted-light: #292929; - out property neutral-background-static-light: #f0f0f0; - out property neutral-background-alpha-light: #ffffff; - out property neutral-background-alpha-2-light: #ffffff; - - // Background interactive states - Light theme - out property neutral-background-1-hover-light: #f5f5f5; - out property neutral-background-1-pressed-light: #f0f0f0; - out property neutral-background-1-selected-light: #ebebeb; - out property neutral-background-2-hover-light: #f0f0f0; - out property neutral-background-2-pressed-light: #ebebeb; - out property neutral-background-2-selected-light: #e1e1e1; - out property neutral-background-3-hover-light: #ebebeb; - out property neutral-background-3-pressed-light: #e1e1e1; - out property neutral-background-3-selected-light: #c7c7c7; - - out property neutral-stroke-1-light: #e1e1e1; - out property neutral-stroke-2-light: #c7c7c7; - out property neutral-stroke-3-light: #b3b3b3; - out property neutral-stroke-accessible-light: #616161; - out property neutral-stroke-focus-1-light: #ffffff; - out property neutral-stroke-focus-2-light: #000000; - out property neutral-stroke-disabled-light: #e1e1e1; - - // Stroke interactive states - Light theme - out property neutral-stroke-1-hover-light: #c7c7c7; - out property neutral-stroke-1-pressed-light: #b3b3b3; - out property neutral-stroke-1-selected-light: #b3b3b3; - out property neutral-stroke-2-hover-light: #b3b3b3; - out property neutral-stroke-2-pressed-light: #9e9e9e; - - // ===== DARK THEME COLORS ===== - - // Neutral Colors - Dark - out property neutral-foreground-1-dark: #ffffff; - out property neutral-foreground-2-dark: #f5f5f5; - out property neutral-foreground-3-dark: #ebebeb; - out property neutral-foreground-4-dark: #cccccc; - out property neutral-foreground-disabled-dark: #858585; - out property neutral-foreground-inverted-dark: #242424; - out property neutral-foreground-inverted-2-dark: #292929; - out property neutral-foreground-on-brand-dark: #ffffff; - out property neutral-foreground-static-inverted-dark: #ffffff; - out property neutral-foreground-inverted-disabled-dark: #858585; - - // Interactive states - Dark theme - out property neutral-foreground-1-hover-dark: #f5f5f5; - out property neutral-foreground-1-pressed-dark: #ffffff; - out property neutral-foreground-2-hover-dark: #ebebeb; - out property neutral-foreground-2-pressed-dark: #f5f5f5; - out property neutral-foreground-2-brand-hover-dark: #62abf5; - out property neutral-foreground-2-brand-pressed-dark: #77b7f7; - out property neutral-foreground-2-brand-selected-dark: #479ef5; - - // Link colors - Dark theme - out property neutral-foreground-link-dark: #479ef5; - out property neutral-foreground-link-hover-dark: #62abf5; - out property neutral-foreground-link-pressed-dark: #77b7f7; - out property neutral-foreground-link-selected-dark: #479ef5; - - out property neutral-background-1-dark: #1c1c1c; - out property neutral-background-2-dark: #242424; - out property neutral-background-3-dark: #292929; - out property neutral-background-4-dark: #333333; - out property neutral-background-5-dark: #3d3d3d; - out property neutral-background-6-dark: #474747; - out property neutral-background-inverted-dark: #ffffff; - out property neutral-background-static-dark: #333333; - out property neutral-background-alpha-dark: #1c1c1c; - out property neutral-background-alpha-2-dark: #1c1c1c; - - // Background interactive states - Dark theme - out property neutral-background-1-hover-dark: #242424; - out property neutral-background-1-pressed-dark: #292929; - out property neutral-background-1-selected-dark: #333333; - out property neutral-background-2-hover-dark: #292929; - out property neutral-background-2-pressed-dark: #333333; - out property neutral-background-2-selected-dark: #3d3d3d; - out property neutral-background-3-hover-dark: #333333; - out property neutral-background-3-pressed-dark: #3d3d3d; - out property neutral-background-3-selected-dark: #474747; - - out property neutral-stroke-1-dark: #3d3d3d; - out property neutral-stroke-2-dark: #525252; - out property neutral-stroke-3-dark: #666666; - out property neutral-stroke-accessible-dark: #cccccc; - out property neutral-stroke-focus-1-dark: #ffffff; - out property neutral-stroke-focus-2-dark: #000000; - out property neutral-stroke-disabled-dark: #3d3d3d; - - // Stroke interactive states - Dark theme - out property neutral-stroke-1-hover-dark: #525252; - out property neutral-stroke-1-pressed-dark: #666666; - out property neutral-stroke-1-selected-dark: #666666; - out property neutral-stroke-2-hover-dark: #666666; - out property neutral-stroke-2-pressed-dark: #757575; - - // ===== TEAM LIGHT THEME COLORS ===== - - // Neutral Colors - Team Light - out property neutral-foreground-1-team-light: #242424; - out property neutral-foreground-2-team-light: #424242; - out property neutral-foreground-3-team-light: #616161; - out property neutral-foreground-4-team-light: #757575; - out property neutral-foreground-disabled-team-light: #bdbdbd; - out property neutral-foreground-inverted-team-light: #ffffff; - out property neutral-foreground-inverted-2-team-light: #f5f5f5; - out property neutral-foreground-on-brand-team-light: #ffffff; - out property neutral-foreground-static-inverted-team-light: #ffffff; - out property neutral-foreground-inverted-disabled-team-light: #ffffff; - - // Interactive states - Team Light theme - out property neutral-foreground-1-hover-team-light: #424242; - out property neutral-foreground-1-pressed-team-light: #242424; - out property neutral-foreground-2-hover-team-light: #616161; - out property neutral-foreground-2-pressed-team-light: #424242; - out property neutral-foreground-2-brand-hover-team-light: #585a96; - out property neutral-foreground-2-brand-pressed-team-light: #4c4e85; - out property neutral-foreground-2-brand-selected-team-light: #6264a7; - - // Link colors - Team Light theme - out property neutral-foreground-link-team-light: #6264a7; - out property neutral-foreground-link-hover-team-light: #585a96; - out property neutral-foreground-link-pressed-team-light: #4c4e85; - out property neutral-foreground-link-selected-team-light: #6264a7; - - out property neutral-background-1-team-light: #ffffff; - out property neutral-background-2-team-light: #f8f8f8; - out property neutral-background-3-team-light: #f3f2f1; - out property neutral-background-4-team-light: #edebe9; - out property neutral-background-5-team-light: #e1dfdd; - out property neutral-background-6-team-light: #d2d0ce; - out property neutral-background-inverted-team-light: #292929; - out property neutral-background-static-team-light: #edebe9; - out property neutral-background-alpha-team-light: #ffffff; - out property neutral-background-alpha-2-team-light: #ffffff; - - // Background interactive states - Team Light theme - out property neutral-background-1-hover-team-light: #f8f8f8; - out property neutral-background-1-pressed-team-light: #f3f2f1; - out property neutral-background-1-selected-team-light: #edebe9; - out property neutral-background-2-hover-team-light: #f3f2f1; - out property neutral-background-2-pressed-team-light: #edebe9; - out property neutral-background-2-selected-team-light: #e1dfdd; - out property neutral-background-3-hover-team-light: #edebe9; - out property neutral-background-3-pressed-team-light: #e1dfdd; - out property neutral-background-3-selected-team-light: #d2d0ce; - - out property neutral-stroke-1-team-light: #d2d0ce; - out property neutral-stroke-2-team-light: #c8c6c4; - out property neutral-stroke-3-team-light: #b3b0ad; - out property neutral-stroke-accessible-team-light: #605e5c; - out property neutral-stroke-focus-1-team-light: #ffffff; - out property neutral-stroke-focus-2-team-light: #000000; - out property neutral-stroke-disabled-team-light: #d2d0ce; - - // Stroke interactive states - Team Light theme - out property neutral-stroke-1-hover-team-light: #c8c6c4; - out property neutral-stroke-1-pressed-team-light: #b3b0ad; - out property neutral-stroke-1-selected-team-light: #b3b0ad; - out property neutral-stroke-2-hover-team-light: #b3b0ad; - out property neutral-stroke-2-pressed-team-light: #a19f9d; - - // ===== TEAM DARK THEME COLORS ===== - - // Neutral Colors - Team Dark - out property neutral-foreground-1-team-dark: #ffffff; - out property neutral-foreground-2-team-dark: #f3f2f1; - out property neutral-foreground-3-team-dark: #edebe9; - out property neutral-foreground-4-team-dark: #d2d0ce; - out property neutral-foreground-disabled-team-dark: #8a8886; - out property neutral-foreground-inverted-team-dark: #242424; - out property neutral-foreground-inverted-2-team-dark: #292929; - out property neutral-foreground-on-brand-team-dark: #ffffff; - out property neutral-foreground-static-inverted-team-dark: #ffffff; - out property neutral-foreground-inverted-disabled-team-dark: #8a8886; - - // Interactive states - Team Dark theme - out property neutral-foreground-1-hover-team-dark: #f3f2f1; - out property neutral-foreground-1-pressed-team-dark: #ffffff; - out property neutral-foreground-2-hover-team-dark: #edebe9; - out property neutral-foreground-2-pressed-team-dark: #f3f2f1; - out property neutral-foreground-2-brand-hover-team-dark: #9a9bd2; - out property neutral-foreground-2-brand-pressed-team-dark: #a8a9db; - out property neutral-foreground-2-brand-selected-team-dark: #8b8cc8; - - // Link colors - Team Dark theme - out property neutral-foreground-link-team-dark: #8b8cc8; - out property neutral-foreground-link-hover-team-dark: #9a9bd2; - out property neutral-foreground-link-pressed-team-dark: #a8a9db; - out property neutral-foreground-link-selected-team-dark: #8b8cc8; - - out property neutral-background-1-team-dark: #252423; - out property neutral-background-2-team-dark: #2d2c2b; - out property neutral-background-3-team-dark: #323130; - out property neutral-background-4-team-dark: #3b3a39; - out property neutral-background-5-team-dark: #484644; - out property neutral-background-6-team-dark: #605e5c; - out property neutral-background-inverted-team-dark: #ffffff; - out property neutral-background-static-team-dark: #3b3a39; - out property neutral-background-alpha-team-dark: #252423; - out property neutral-background-alpha-2-team-dark: #252423; - - // Background interactive states - Team Dark theme - out property neutral-background-1-hover-team-dark: #2d2c2b; - out property neutral-background-1-pressed-team-dark: #323130; - out property neutral-background-1-selected-team-dark: #3b3a39; - out property neutral-background-2-hover-team-dark: #323130; - out property neutral-background-2-pressed-team-dark: #3b3a39; - out property neutral-background-2-selected-team-dark: #484644; - out property neutral-background-3-hover-team-dark: #3b3a39; - out property neutral-background-3-pressed-team-dark: #484644; - out property neutral-background-3-selected-team-dark: #605e5c; - - out property neutral-stroke-1-team-dark: #484644; - out property neutral-stroke-2-team-dark: #605e5c; - out property neutral-stroke-3-team-dark: #797775; - out property neutral-stroke-accessible-team-dark: #d2d0ce; - out property neutral-stroke-focus-1-team-dark: #ffffff; - out property neutral-stroke-focus-2-team-dark: #000000; - out property neutral-stroke-disabled-team-dark: #484644; - - // Stroke interactive states - Team Dark theme - out property neutral-stroke-1-hover-team-dark: #605e5c; - out property neutral-stroke-1-pressed-team-dark: #797775; - out property neutral-stroke-1-selected-team-dark: #797775; - out property neutral-stroke-2-hover-team-dark: #797775; - out property neutral-stroke-2-pressed-team-dark: #8a8886; - - // ===== BRAND COLORS (All Themes) ===== - - // Brand Colors - Light - out property brand-background-1-light: #0078d4; - out property brand-background-2-light: #106ebe; - out property brand-background-3-light: #005a9e; - out property brand-foreground-1-light: #ffffff; - out property brand-foreground-2-light: #f3f2f1; - out property brand-stroke-1-light: #0078d4; - out property brand-stroke-2-light: #106ebe; - - // Brand interactive states - Light theme - out property brand-background-1-hover-light: #106ebe; - out property brand-background-1-pressed-light: #005a9e; - out property brand-background-1-selected-light: #0078d4; - out property brand-background-2-hover-light: #005a9e; - out property brand-background-2-pressed-light: #004578; - out property brand-stroke-1-hover-light: #106ebe; - out property brand-stroke-1-pressed-light: #005a9e; - - // Brand Colors - Dark - out property brand-background-1-dark: #479ef5; - out property brand-background-2-dark: #62abf5; - out property brand-background-3-dark: #77b7f7; - out property brand-foreground-1-dark: #ffffff; - out property brand-foreground-2-dark: #f3f2f1; - out property brand-stroke-1-dark: #479ef5; - out property brand-stroke-2-dark: #62abf5; - - // Brand interactive states - Dark theme - out property brand-background-1-hover-dark: #62abf5; - out property brand-background-1-pressed-dark: #77b7f7; - out property brand-background-1-selected-dark: #479ef5; - out property brand-background-2-hover-dark: #77b7f7; - out property brand-background-2-pressed-dark: #8cc8f8; - out property brand-stroke-1-hover-dark: #62abf5; - out property brand-stroke-1-pressed-dark: #77b7f7; - - // Brand Colors - Team Light - out property brand-background-1-team-light: #6264a7; - out property brand-background-2-team-light: #585a96; - out property brand-background-3-team-light: #4c4e85; - out property brand-foreground-1-team-light: #ffffff; - out property brand-foreground-2-team-light: #f3f2f1; - out property brand-stroke-1-team-light: #6264a7; - out property brand-stroke-2-team-light: #585a96; - - // Brand interactive states - Team Light theme - out property brand-background-1-hover-team-light: #585a96; - out property brand-background-1-pressed-team-light: #4c4e85; - out property brand-background-1-selected-team-light: #6264a7; - out property brand-background-2-hover-team-light: #4c4e85; - out property brand-background-2-pressed-team-light: #414374; - out property brand-stroke-1-hover-team-light: #585a96; - out property brand-stroke-1-pressed-team-light: #4c4e85; - - // Brand Colors - Team Dark - out property brand-background-1-team-dark: #8b8cc8; - out property brand-background-2-team-dark: #9a9bd2; - out property brand-background-3-team-dark: #a8a9db; - out property brand-foreground-1-team-dark: #ffffff; - out property brand-foreground-2-team-dark: #f3f2f1; - out property brand-stroke-1-team-dark: #8b8cc8; - out property brand-stroke-2-team-dark: #9a9bd2; - - // Brand interactive states - Team Dark theme - out property brand-background-1-hover-team-dark: #9a9bd2; - out property brand-background-1-pressed-team-dark: #a8a9db; - out property brand-background-1-selected-team-dark: #8b8cc8; - out property brand-background-2-hover-team-dark: #a8a9db; - out property brand-background-2-pressed-team-dark: #b6b7e4; - out property brand-stroke-1-hover-team-dark: #9a9bd2; - out property brand-stroke-1-pressed-team-dark: #a8a9db; - - // ===== SEMANTIC COLORS (All Themes) ===== - - // Success Colors - All Themes - out property success-background-1-light: #107c10; - out property success-background-2-light: #0e6e0e; - out property success-foreground-1-light: #ffffff; - out property success-stroke-1-light: #107c10; - - out property success-background-1-dark: #54b054; - out property success-background-2-dark: #6bb26b; - out property success-foreground-1-dark: #ffffff; - out property success-stroke-1-dark: #54b054; - - out property success-background-1-team-light: #237b4b; - out property success-background-2-team-light: #1e6f42; - out property success-foreground-1-team-light: #ffffff; - out property success-stroke-1-team-light: #237b4b; - - out property success-background-1-team-dark: #5bb85b; - out property success-background-2-team-dark: #6fc46f; - out property success-foreground-1-team-dark: #ffffff; - out property success-stroke-1-team-dark: #5bb85b; - - // Warning Colors - All Themes - out property warning-background-1-light: #fde047; - out property warning-background-2-light: #facc15; - out property warning-foreground-1-light: #323130; - out property warning-stroke-1-light: #f7c52d; - - out property warning-background-1-dark: #ffb900; - out property warning-background-2-dark: #ffc328; - out property warning-foreground-1-dark: #323130; - out property warning-stroke-1-dark: #ffb900; - - out property warning-background-1-team-light: #c19c00; - out property warning-background-2-team-light: #a18600; - out property warning-foreground-1-team-light: #ffffff; - out property warning-stroke-1-team-light: #c19c00; - - out property warning-background-1-team-dark: #ffd43a; - out property warning-background-2-team-dark: #ffda56; - out property warning-foreground-1-team-dark: #323130; - out property warning-stroke-1-team-dark: #ffd43a; - - // Critical/Error Colors - All Themes - out property critical-background-1-light: #d13438; - out property critical-background-2-light: #b91c1c; - out property critical-foreground-1-light: #ffffff; - out property critical-stroke-1-light: #d13438; - - out property critical-background-1-dark: #ff6b6b; - out property critical-background-2-dark: #ff8080; - out property critical-foreground-1-dark: #ffffff; - out property critical-stroke-1-dark: #ff6b6b; - - out property critical-background-1-team-light: #c50e1f; - out property critical-background-2-team-light: #a80e1b; - out property critical-foreground-1-team-light: #ffffff; - out property critical-stroke-1-team-light: #c50e1f; - - out property critical-background-1-team-dark: #ff8080; - out property critical-background-2-team-dark: #ff9494; - out property critical-foreground-1-team-dark: #ffffff; - out property critical-stroke-1-team-dark: #ff8080; - - // ===== CURRENT THEME ACCESSORS ===== - // These automatically switch based on current-mode - - // Current Neutral Colors - out property neutral-foreground-1: current-mode == ColorMode.light ? neutral-foreground-1-light : - current-mode == ColorMode.dark ? neutral-foreground-1-dark : - current-mode == ColorMode.team-light ? neutral-foreground-1-team-light : neutral-foreground-1-team-dark; - - out property neutral-foreground-2: current-mode == ColorMode.light ? neutral-foreground-2-light : - current-mode == ColorMode.dark ? neutral-foreground-2-dark : - current-mode == ColorMode.team-light ? neutral-foreground-2-team-light : neutral-foreground-2-team-dark; - - out property neutral-foreground-3: current-mode == ColorMode.light ? neutral-foreground-3-light : - current-mode == ColorMode.dark ? neutral-foreground-3-dark : - current-mode == ColorMode.team-light ? neutral-foreground-3-team-light : neutral-foreground-3-team-dark; - - out property neutral-foreground-disabled: current-mode == ColorMode.light ? neutral-foreground-disabled-light : - current-mode == ColorMode.dark ? neutral-foreground-disabled-dark : - current-mode == ColorMode.team-light ? neutral-foreground-disabled-team-light : neutral-foreground-disabled-team-dark; - - out property neutral-background-1: current-mode == ColorMode.light ? neutral-background-1-light : - current-mode == ColorMode.dark ? neutral-background-1-dark : - current-mode == ColorMode.team-light ? neutral-background-1-team-light : neutral-background-1-team-dark; - - out property neutral-background-2: current-mode == ColorMode.light ? neutral-background-2-light : - current-mode == ColorMode.dark ? neutral-background-2-dark : - current-mode == ColorMode.team-light ? neutral-background-2-team-light : neutral-background-2-team-dark; - - out property neutral-background-3: current-mode == ColorMode.light ? neutral-background-3-light : - current-mode == ColorMode.dark ? neutral-background-3-dark : - current-mode == ColorMode.team-light ? neutral-background-3-team-light : neutral-background-3-team-dark; - - out property neutral-background-4: current-mode == ColorMode.light ? neutral-background-4-light : - current-mode == ColorMode.dark ? neutral-background-4-dark : - current-mode == ColorMode.team-light ? neutral-background-4-team-light : neutral-background-4-team-dark; - - out property neutral-stroke-1: current-mode == ColorMode.light ? neutral-stroke-1-light : - current-mode == ColorMode.dark ? neutral-stroke-1-dark : - current-mode == ColorMode.team-light ? neutral-stroke-1-team-light : neutral-stroke-1-team-dark; - - out property neutral-stroke-2: current-mode == ColorMode.light ? neutral-stroke-2-light : - current-mode == ColorMode.dark ? neutral-stroke-2-dark : - current-mode == ColorMode.team-light ? neutral-stroke-2-team-light : neutral-stroke-2-team-dark; - - out property neutral-stroke-accessible: current-mode == ColorMode.light ? neutral-stroke-accessible-light : - current-mode == ColorMode.dark ? neutral-stroke-accessible-dark : - current-mode == ColorMode.team-light ? neutral-stroke-accessible-team-light : neutral-stroke-accessible-team-dark; - - // Current Interactive States - out property neutral-foreground-1-hover: current-mode == ColorMode.light ? neutral-foreground-1-hover-light : - current-mode == ColorMode.dark ? neutral-foreground-1-hover-dark : - current-mode == ColorMode.team-light ? neutral-foreground-1-hover-team-light : neutral-foreground-1-hover-team-dark; - - out property neutral-foreground-2-brand-hover: current-mode == ColorMode.light ? neutral-foreground-2-brand-hover-light : - current-mode == ColorMode.dark ? neutral-foreground-2-brand-hover-dark : - current-mode == ColorMode.team-light ? neutral-foreground-2-brand-hover-team-light : neutral-foreground-2-brand-hover-team-dark; - - // Current Link Colors - out property neutral-foreground-link: current-mode == ColorMode.light ? neutral-foreground-link-light : - current-mode == ColorMode.dark ? neutral-foreground-link-dark : - current-mode == ColorMode.team-light ? neutral-foreground-link-team-light : neutral-foreground-link-team-dark; - - out property neutral-foreground-link-hover: current-mode == ColorMode.light ? neutral-foreground-link-hover-light : - current-mode == ColorMode.dark ? neutral-foreground-link-hover-dark : - current-mode == ColorMode.team-light ? neutral-foreground-link-hover-team-light : neutral-foreground-link-hover-team-dark; - - out property neutral-foreground-link-pressed: current-mode == ColorMode.light ? neutral-foreground-link-pressed-light : - current-mode == ColorMode.dark ? neutral-foreground-link-pressed-dark : - current-mode == ColorMode.team-light ? neutral-foreground-link-pressed-team-light : neutral-foreground-link-pressed-team-dark; - - // Current Background Interactive States - out property neutral-background-1-hover: current-mode == ColorMode.light ? neutral-background-1-hover-light : - current-mode == ColorMode.dark ? neutral-background-1-hover-dark : - current-mode == ColorMode.team-light ? neutral-background-1-hover-team-light : neutral-background-1-hover-team-dark; - - out property neutral-background-1-pressed: current-mode == ColorMode.light ? neutral-background-1-pressed-light : - current-mode == ColorMode.dark ? neutral-background-1-pressed-dark : - current-mode == ColorMode.team-light ? neutral-background-1-pressed-team-light : neutral-background-1-pressed-team-dark; - - // Current Stroke Interactive States - out property neutral-stroke-1-hover: current-mode == ColorMode.light ? neutral-stroke-1-hover-light : - current-mode == ColorMode.dark ? neutral-stroke-1-hover-dark : - current-mode == ColorMode.team-light ? neutral-stroke-1-hover-team-light : neutral-stroke-1-hover-team-dark; - - out property neutral-stroke-1-pressed: current-mode == ColorMode.light ? neutral-stroke-1-pressed-light : - current-mode == ColorMode.dark ? neutral-stroke-1-pressed-dark : - current-mode == ColorMode.team-light ? neutral-stroke-1-pressed-team-light : neutral-stroke-1-pressed-team-dark; - - // Current Brand Colors - out property brand-background-1: current-mode == ColorMode.light ? brand-background-1-light : - current-mode == ColorMode.dark ? brand-background-1-dark : - current-mode == ColorMode.team-light ? brand-background-1-team-light : brand-background-1-team-dark; - - out property brand-background-2: current-mode == ColorMode.light ? brand-background-2-light : - current-mode == ColorMode.dark ? brand-background-2-dark : - current-mode == ColorMode.team-light ? brand-background-2-team-light : brand-background-2-team-dark; - - out property brand-foreground-1: current-mode == ColorMode.light ? brand-foreground-1-light : - current-mode == ColorMode.dark ? brand-foreground-1-dark : - current-mode == ColorMode.team-light ? brand-foreground-1-team-light : brand-foreground-1-team-dark; - - // Current Brand Interactive States - out property brand-background-1-hover: current-mode == ColorMode.light ? brand-background-1-hover-light : - current-mode == ColorMode.dark ? brand-background-1-hover-dark : - current-mode == ColorMode.team-light ? brand-background-1-hover-team-light : brand-background-1-hover-team-dark; - - out property brand-background-1-pressed: current-mode == ColorMode.light ? brand-background-1-pressed-light : - current-mode == ColorMode.dark ? brand-background-1-pressed-dark : - current-mode == ColorMode.team-light ? brand-background-1-pressed-team-light : brand-background-1-pressed-team-dark; - - // Current Semantic Colors - out property success-background-1: current-mode == ColorMode.light ? success-background-1-light : - current-mode == ColorMode.dark ? success-background-1-dark : - current-mode == ColorMode.team-light ? success-background-1-team-light : success-background-1-team-dark; - - out property warning-background-1: current-mode == ColorMode.light ? warning-background-1-light : - current-mode == ColorMode.dark ? warning-background-1-dark : - current-mode == ColorMode.team-light ? warning-background-1-team-light : warning-background-1-team-dark; - - out property critical-background-1: current-mode == ColorMode.light ? critical-background-1-light : - current-mode == ColorMode.dark ? critical-background-1-dark : - current-mode == ColorMode.team-light ? critical-background-1-team-light : critical-background-1-team-dark; - - // Legacy compatibility and Trading-specific colors - out property neutral-foreground-rest: neutral-foreground-1; - out property profit-green: success-background-1; - out property loss-red: critical-background-1; - out property neutral-gray: neutral-foreground-3; - - // Accent colors for trading UI - out property accent-purple: #f000b8; - out property accent-teal: #37cdbe; - out property accent-blue: brand-background-1; -} - -// Typography System - Official Fluent 2 Typography Tokens -// Based on: https://fluent2.microsoft.design/typography -export global FluentTypography { - // ===== FLUENT FONT STACKS ===== - // Segoe - unmistakably Microsoft (Primary Web/Windows font stack) - out property font-family-base: "Segoe UI Variable, Segoe UI, system-ui, sans-serif"; - out property font-family-web: "Segoe UI Variable, Segoe UI, system-ui, sans-serif"; - out property font-family-windows: "Segoe UI Variable, Segoe UI, system-ui, sans-serif"; - - // Platform-specific native font stacks for cross-platform consistency - out property font-family-macos: "SF Pro Display, SF Pro Text, system-ui, sans-serif"; - out property font-family-ios: "SF Pro Display, SF Pro Text, system-ui, sans-serif"; - out property font-family-android: "Roboto, system-ui, sans-serif"; - - // Specialized font families - out property font-family-monospace: "Cascadia Code, Cascadia Mono, Consolas, Courier New, monospace"; - out property font-family-numeric: "Bahnschrift, Segoe UI Variable, Segoe UI, system-ui, sans-serif"; - - // ===== FONT WEIGHTS - Fluent 2 Weight Scale ===== - out property font-weight-regular: 400; // Regular - out property font-weight-medium: 500; // Medium (Android) - out property font-weight-semibold: 600; // Semibold - out property font-weight-bold: 700; // Bold - - // ===== WEB TYPE RAMP - Official Fluent 2 Web Typography ===== - // Caption styles - out property font-size-caption-2: 10px; // Caption 2 - out property line-height-caption-2: 14px; - out property font-weight-caption-2: font-weight-regular; - out property font-weight-caption-2-strong: font-weight-semibold; - - out property font-size-caption-1: 12px; // Caption 1 - out property line-height-caption-1: 16px; - out property font-weight-caption-1: font-weight-regular; - out property font-weight-caption-1-strong: font-weight-semibold; - out property font-weight-caption-1-stronger: font-weight-bold; - - // Body styles - out property font-size-body-1: 14px; // Body 1 (Default) - out property line-height-body-1: 20px; - out property font-weight-body-1: font-weight-regular; - out property font-weight-body-1-strong: font-weight-semibold; - out property font-weight-body-1-stronger: font-weight-bold; - - // Subtitle styles - out property font-size-subtitle-2: 16px; // Subtitle 2 - out property line-height-subtitle-2: 22px; - out property font-weight-subtitle-2: font-weight-semibold; - out property font-weight-subtitle-2-stronger: font-weight-bold; - - out property font-size-subtitle-1: 20px; // Subtitle 1 - out property line-height-subtitle-1: 26px; - out property font-weight-subtitle-1: font-weight-semibold; - - // Title styles - out property font-size-title-3: 24px; // Title 3 - out property line-height-title-3: 32px; - out property font-weight-title-3: font-weight-semibold; - - out property font-size-title-2: 28px; // Title 2 - out property line-height-title-2: 36px; - out property font-weight-title-2: font-weight-semibold; - - out property font-size-title-1: 32px; // Title 1 - out property line-height-title-1: 40px; - out property font-weight-title-1: font-weight-semibold; - - // Large title and display - out property font-size-large-title: 40px; // Large Title - out property line-height-large-title: 52px; - out property font-weight-large-title: font-weight-semibold; - - out property font-size-display: 68px; // Display - out property line-height-display: 92px; - out property font-weight-display: font-weight-semibold; - - // ===== WINDOWS TYPE RAMP - Official Fluent 2 Windows Typography ===== - // Windows-specific variants using Segoe UI Variable - out property font-size-windows-caption: 12px; // Caption - out property line-height-windows-caption: 16px; - out property font-weight-windows-caption: font-weight-regular; - - out property font-size-windows-body: 14px; // Body - out property line-height-windows-body: 20px; - out property font-weight-windows-body: font-weight-regular; - out property font-weight-windows-body-strong: font-weight-semibold; - - out property font-size-windows-body-large: 18px; // Body Large - out property line-height-windows-body-large: 24px; - out property font-weight-windows-body-large: font-weight-regular; - - out property font-size-windows-subtitle: 20px; // Subtitle - out property line-height-windows-subtitle: 28px; - out property font-weight-windows-subtitle: font-weight-semibold; - - out property font-size-windows-title: 28px; // Title - out property line-height-windows-title: 36px; - out property font-weight-windows-title: font-weight-semibold; - - out property font-size-windows-large-title: 40px; // Large Title - out property line-height-windows-large-title: 52px; - out property font-weight-windows-large-title: font-weight-semibold; - - out property font-size-windows-display: 68px; // Display - out property line-height-windows-display: 92px; - out property font-weight-windows-display: font-weight-semibold; - - // ===== LEGACY NUMERIC TOKENS - For backward compatibility ===== - out property font-size-100: font-size-caption-2; // 10px - out property font-size-200: font-size-caption-1; // 12px - out property font-size-300: font-size-body-1; // 14px - out property font-size-400: font-size-subtitle-2; // 16px - out property font-size-500: 18px; // Custom - out property font-size-600: font-size-subtitle-1; // 20px - out property font-size-700: font-size-title-3; // 24px - out property font-size-800: font-size-title-2; // 28px - out property font-size-900: font-size-title-1; // 32px - out property font-size-1000: font-size-large-title; // 40px - out property font-size-1100: 48px; // Custom Large - out property font-size-1200: font-size-display; // 68px - - out property line-height-100: line-height-caption-2; // 14px - out property line-height-200: line-height-caption-1; // 16px - out property line-height-300: line-height-body-1; // 20px - out property line-height-400: line-height-subtitle-2; // 22px - out property line-height-500: 24px; // Custom - out property line-height-600: line-height-subtitle-1; // 26px - out property line-height-700: line-height-title-3; // 32px - out property line-height-800: line-height-title-2; // 36px - out property line-height-900: line-height-title-1; // 40px - out property line-height-1000: line-height-large-title; // 52px - out property line-height-1100: 60px; // Custom - out property line-height-1200: line-height-display; // 92px - - // ===== TEXT STYLING UTILITIES ===== - // Letter spacing for fine typography control - out property letter-spacing-tight: -0.16px; // For large text - out property letter-spacing-normal: 0px; // Standard - out property letter-spacing-wide: 0.32px; // For small text - - // Text decoration - out property text-decoration-none: "none"; - out property text-decoration-underline: "underline"; - out property text-decoration-line-through: "line-through"; - - // Text transform (following Fluent guidance for sentence case) - out property text-transform-none: "none"; - out property text-transform-uppercase: "uppercase"; - out property text-transform-lowercase: "lowercase"; - out property text-transform-capitalize: "capitalize"; - - // ===== USAGE EXAMPLES ===== - // Caption 2: font-size: FluentTypography.font-size-caption-2; line-height: FluentTypography.line-height-caption-2; font-weight: FluentTypography.font-weight-caption-2; - // Body 1: font-size: FluentTypography.font-size-body-1; line-height: FluentTypography.line-height-body-1; font-weight: FluentTypography.font-weight-body-1; - // Title 1: font-size: FluentTypography.font-size-title-1; line-height: FluentTypography.line-height-title-1; font-weight: FluentTypography.font-weight-title-1; - // Display: font-size: FluentTypography.font-size-display; line-height: FluentTypography.line-height-display; font-weight: FluentTypography.font-weight-display; -} - -// Spacing System - Fluent 2 Spacing Scale -export global FluentSpacing { - out property space-2: 2px; - out property space-4: 4px; - out property space-6: 6px; - out property space-8: 8px; - out property space-10: 10px; - out property space-12: 12px; - out property space-16: 16px; - out property space-20: 20px; - out property space-24: 24px; - out property space-28: 28px; - out property space-32: 32px; - out property space-36: 36px; - out property space-40: 40px; - out property space-48: 48px; - out property space-64: 64px; - out property space-80: 80px; - out property space-96: 96px; - out property space-120: 120px; -} - -// Border Radius - Complete Fluent UI Border Radius System -export global FluentRadius { - // Corner Radius Scale - out property border-radius-none: 0px; - out property border-radius-small: 2px; - out property border-radius-medium: 4px; - out property border-radius-large: 6px; - out property border-radius-x-large: 8px; - out property border-radius-circular: 10000px; - - // Legacy support - out property radius-none: 0px; - out property radius-small: 2px; - out property radius-medium: 4px; - out property radius-large: 6px; - out property radius-xlarge: 8px; - out property radius-circular: 10000px; -} - -// Shadows - Complete Fluent UI Shadow System -// Based on: https://react.fluentui.dev/?path=/docs/theme-shadows--docs -export global FluentShadows { - // ===== SHADOW BRAND TOKENS ===== - // These are the primary shadow tokens used throughout Fluent UI - - // Shadow 2 - Subtle elevation (tooltips, dropdowns) - out property shadow-2-x: 0px; - out property shadow-2-y: 1px; - out property shadow-2-blur: 2px; - out property shadow-2-spread: 0px; - out property shadow-2-color: #00000014; // 8% opacity black - - // Shadow 4 - Low elevation (cards, buttons) - out property shadow-4-x: 0px; - out property shadow-4-y: 2px; - out property shadow-4-blur: 4px; - out property shadow-4-spread: 0px; - out property shadow-4-color: #0000001f; // 12% opacity black - - // Shadow 8 - Medium elevation (dialogs, menus) - out property shadow-8-x: 0px; - out property shadow-8-y: 4px; - out property shadow-8-blur: 8px; - out property shadow-8-spread: 0px; - out property shadow-8-color: #0000001f; // 12% opacity black - - // Shadow 16 - High elevation (modals, flyouts) - out property shadow-16-x: 0px; - out property shadow-16-y: 8px; - out property shadow-16-blur: 16px; - out property shadow-16-spread: 0px; - out property shadow-16-color: #00000024; // 14% opacity black - - // Shadow 28 - Very high elevation (teaching callouts) - out property shadow-28-x: 0px; - out property shadow-28-y: 14px; - out property shadow-28-blur: 28px; - out property shadow-28-spread: 0px; - out property shadow-28-color: #00000024; // 14% opacity black - - // Shadow 64 - Maximum elevation (panels, navigation) - out property shadow-64-x: 0px; - out property shadow-64-y: 32px; - out property shadow-64-blur: 64px; - out property shadow-64-spread: 0px; - out property shadow-64-color: #00000033; // 20% opacity black - - // ===== THEME-AWARE SHADOW COLORS ===== - // Light theme shadow colors - out property shadow-light-ambient: #0000000f; // 6% opacity black - out property shadow-light-key: #00000014; // 8% opacity black - out property shadow-light-ambient-darker: #00000014; // 8% opacity black - out property shadow-light-key-darker: #0000001f; // 12% opacity black - out property shadow-light-ambient-darkest: #0000001f; // 12% opacity black - out property shadow-light-key-darkest: #00000024; // 14% opacity black - - // Dark theme shadow colors - out property shadow-dark-ambient: #0000001f; // 12% opacity black - out property shadow-dark-key: #00000024; // 14% opacity black - out property shadow-dark-ambient-darker: #00000024; // 14% opacity black - out property shadow-dark-key-darker: #00000033; // 20% opacity black - out property shadow-dark-ambient-darkest: #00000033; // 20% opacity black - out property shadow-dark-key-darkest: #00000047; // 28% opacity black - - // ===== SPECIALIZED SHADOW TOKENS ===== - // Brand shadow (with brand color tint) - out property shadow-brand-x: 0px; - out property shadow-brand-y: 2px; - out property shadow-brand-blur: 8px; - out property shadow-brand-spread: 0px; - out property shadow-brand-color: #0078d414; // Brand blue with 8% opacity - - // Inset shadow (for pressed states) - out property shadow-inset-x: 0px; - out property shadow-inset-y: 1px; - out property shadow-inset-blur: 2px; - out property shadow-inset-spread: 0px; - out property shadow-inset-color: #0000001f; // 12% opacity black - - // Focus shadow (for accessibility) - out property shadow-focus-x: 0px; - out property shadow-focus-y: 0px; - out property shadow-focus-blur: 0px; - out property shadow-focus-spread: 2px; - out property shadow-focus-color: #0078d4; // Brand blue - - // ===== COMPONENT-SPECIFIC SHADOW TOKENS ===== - // Button shadows - out property shadow-button-rest: shadow-2-color; - out property shadow-button-hover: shadow-4-color; - out property shadow-button-pressed: shadow-inset-color; - - // Card shadows - out property shadow-card-rest: shadow-4-color; - out property shadow-card-hover: shadow-8-color; - - // Dialog shadows - out property shadow-dialog: shadow-64-color; - - // Flyout shadows - out property shadow-flyout: shadow-16-color; - - // Tooltip shadows - out property shadow-tooltip: shadow-8-color; - - // ===== LEGACY SHADOW SUPPORT ===== - // Note: shadow-*-blur properties are already defined above in the main shadow tokens - // They can be used directly for backward compatibility - - // ===== SHADOW UTILITY FUNCTIONS ===== - // Helper properties for common shadow combinations - - // Subtle shadow (for subtle elevation) - out property subtle-shadow-x: shadow-2-x; - out property subtle-shadow-y: shadow-2-y; - out property subtle-shadow-blur: shadow-2-blur; - out property subtle-shadow-color: shadow-2-color; - - // Default shadow (for standard elevation) - out property default-shadow-x: shadow-4-x; - out property default-shadow-y: shadow-4-y; - out property default-shadow-blur: shadow-4-blur; - out property default-shadow-color: shadow-4-color; - - // Deep shadow (for high elevation) - out property deep-shadow-x: shadow-16-x; - out property deep-shadow-y: shadow-16-y; - out property deep-shadow-blur: shadow-16-blur; - out property deep-shadow-color: shadow-16-color; - - // ===== USAGE NOTES ===== - // To apply shadows in components, use the individual tokens: - // Example: Rectangle { drop-shadow-offset-x: FluentShadows.shadow-4-x; drop-shadow-offset-y: FluentShadows.shadow-4-y; drop-shadow-blur: FluentShadows.shadow-4-blur; drop-shadow-color: FluentShadows.shadow-4-color; } - // For focus states: Rectangle { border-width: FluentShadows.shadow-focus-spread; border-color: FluentShadows.shadow-focus-color; } -} - -// Component Tokens - Fluent 2 Component Specifications -export global FluentComponents { - // Button - out property button-height-small: 24px; - out property button-height-medium: 32px; - out property button-height-large: 40px; - out property button-padding-horizontal: 16px; - out property button-border-radius: FluentRadius.border-radius-medium; - - // Input - out property input-height: 32px; - out property input-padding-horizontal: 12px; - out property input-border-radius: FluentRadius.border-radius-medium; - out property input-border-width: 1px; - - // Card - out property card-padding: FluentSpacing.space-16; - out property card-border-radius: FluentRadius.border-radius-large; - out property card-border-width: 1px; - - // Navigation - out property nav-item-height: 40px; - out property nav-item-padding: FluentSpacing.space-12; - out property nav-item-border-radius: FluentRadius.border-radius-medium; - - // Title Bar - out property titlebar-height: 32px; - out property titlebar-button-width: 46px; - - // Sidebar - out property sidebar-width: 280px; - out property sidebar-collapsed-width: 48px; -} - -// Animation & Motion - Fluent 2 Motion System -export global FluentMotion { - out property duration-ultra-fast: 50ms; - out property duration-faster: 100ms; - out property duration-fast: 150ms; - out property duration-normal: 200ms; - out property duration-slow: 300ms; - out property duration-slower: 400ms; - out property duration-ultra-slow: 500ms; - - // Easing curves (represented as animation properties) - out property ease-accelerate: ease-in; - out property ease-decelerate: ease-out; - out property ease-standard: ease-in-out; - out property ease-max: ease; -} - -// Layout Grid - Fluent 2 Layout System -export global FluentLayout { - // Breakpoints - out property breakpoint-small: 480px; - out property breakpoint-medium: 768px; - out property breakpoint-large: 1024px; - out property breakpoint-xlarge: 1440px; - - // Container max widths - out property container-small: 640px; - out property container-medium: 768px; - out property container-large: 1024px; - out property container-xlarge: 1280px; - - // Grid - out property grid-gutter: FluentSpacing.space-24; - out property grid-columns: 12; -} - -// Z-Index Scale -export global FluentLayers { - out property layer-base: 0; - out property layer-dropdown: 1000; - out property layer-sticky: 1020; - out property layer-banner: 1030; - out property layer-overlay: 1040; - out property layer-modal: 1050; - out property layer-popover: 1060; - out property layer-tooltip: 1070; - out property layer-toast: 1080; -} \ No newline at end of file diff --git a/ui/shared/design-system/index.slint b/ui/shared/design-system/index.slint deleted file mode 100644 index 9d6da7a..0000000 --- a/ui/shared/design-system/index.slint +++ /dev/null @@ -1,8 +0,0 @@ -// Design System Public API -// This file exports all design tokens and components from the design system - -// Export design tokens -export { Theme } from "tokens/theme.slint"; - -// Export design system components -export * from "components/design-system.slint"; \ No newline at end of file diff --git a/ui/shared/design-system/tokens/theme.slint b/ui/shared/design-system/tokens/theme.slint deleted file mode 100644 index f904bb9..0000000 --- a/ui/shared/design-system/tokens/theme.slint +++ /dev/null @@ -1,29 +0,0 @@ -// Global theme management system -export global Theme { - in-out property is-dark-mode: false; - - // Color scheme properties that automatically update based on theme - out property background: is-dark-mode ? #1a1a1a : #ffffff; - out property surface: is-dark-mode ? #2a2a2a : #f5f5f5; - out property primary: #3b82f6; - out property primary-variant: is-dark-mode ? #60a5fa : #2563eb; - out property secondary: is-dark-mode ? #64748b : #475569; - out property text-primary: is-dark-mode ? #ffffff : #1f2937; - out property text-secondary: is-dark-mode ? #d1d5db : #6b7280; - out property border: is-dark-mode ? #374151 : #e5e7eb; - out property accent: is-dark-mode ? #22c55e : #16a34a; - out property error: #ef4444; - out property warning: #f59e0b; - out property success: #10b981; - - // Card and container colors - out property card-background: is-dark-mode ? #2a2a2a : #ffffff; - out property card-border: is-dark-mode ? #404040 : #e5e7eb; - - // Navigation colors - out property nav-background: is-dark-mode ? #1f1f1f : #f8fafc; - out property nav-active: primary; - out property nav-hover: is-dark-mode ? #374151 : #f1f5f9; - - // Theme state is directly modified by components and Rust code -} \ No newline at end of file diff --git a/ui/shared/index.slint b/ui/shared/index.slint index 9fb8b37..2317764 100644 --- a/ui/shared/index.slint +++ b/ui/shared/index.slint @@ -5,7 +5,7 @@ export { Theme, FluentColors, FluentTypography, FluentSpacing, FluentRadius, FluentShadows, FluentComponents, FluentMotion, FluentLayout, FluentLayers, ColorMode } from "design-system/index.slint"; // Export UI components -export { Button, Card, Input, LoadingView, Container, Grid } from "ui/index.slint"; +export { LoadingView } from "ui/index.slint"; // Export utilities and config - currently commented out in their index files // export * from "lib/index.slint"; diff --git a/ui/shared/lib/index.slint b/ui/shared/lib/index.slint index 2e792a7..9656d2e 100644 --- a/ui/shared/lib/index.slint +++ b/ui/shared/lib/index.slint @@ -1,5 +1,5 @@ // Shared Library Public API // Export utility functions and helpers - + // Currently no utilities defined // export * from "utils/common-utils.slint"; \ No newline at end of file diff --git a/ui/shared/types/token.slint b/ui/shared/types/token.slint new file mode 100644 index 0000000..82ef8c5 --- /dev/null +++ b/ui/shared/types/token.slint @@ -0,0 +1,43 @@ +// Shared token data structures for Slint UI - matching muhafidh Redis models + +// For new tokens from NewTokenCache +export struct NewTokenUiData { + mint: string, + bonding_curve: string, // Optional converted to string, empty if None + name: string, + symbol: string, + uri: string, + creator: string, + created_at: string, +} + +// For analyzed tokens from TokenAnalyzedCache (covers both CEX and analysis complete) +export struct CexUpdatedUiData { + mint: string, + name: string, + uri: string, + dev_name: string, + creator: string, + cex_name: string, + cex_address: string, + bonding_curve: string, + created_at: string, + updated_at: string, + node_count: int, + edge_count: int, + // Note: graph field omitted as it's complex and not needed for UI display +} + +export struct MaxDepthReachedUiData { + mint: string, + name: string, + uri: string, + dev_name: string, + creator: string, + bonding_curve: string, + created_at: string, + updated_at: string, + node_count: int, + edge_count: int, + // Note: graph field omitted as it's complex and not needed for UI display +} \ No newline at end of file diff --git a/ui/shared/ui/button/button.slint b/ui/shared/ui/button/button.slint deleted file mode 100644 index d793e6c..0000000 --- a/ui/shared/ui/button/button.slint +++ /dev/null @@ -1,50 +0,0 @@ -import { VerticalBox, HorizontalBox } from "std-widgets.slint"; - -// Basic Button Component -export component Button { - in property text; - in property enabled: true; - in property background-color: #2563eb; - in property text-color: white; - in property hover-color: #1d4ed8; - in property disabled-color: #94a3b8; - in property border-radius: 6px; - in property padding-horizontal: 16px; - in property padding-vertical: 8px; - - callback clicked(); - - min-width: 80px; - min-height: 36px; - - Rectangle { - background: enabled ? (touch-area.has-hover ? hover-color : background-color) : disabled-color; - border-radius: border-radius; - - HorizontalBox { - padding-left: padding-horizontal; - padding-right: padding-horizontal; - padding-top: padding-vertical; - padding-bottom: padding-vertical; - alignment: center; - - Text { - text: root.text; - color: text-color; - font-size: 14px; - font-weight: 500; - horizontal-alignment: center; - vertical-alignment: center; - } - } - - touch-area := TouchArea { - enabled: root.enabled; - clicked => { - if (root.enabled) { - root.clicked(); - } - } - } - } -} \ No newline at end of file diff --git a/ui/shared/ui/button/index.slint b/ui/shared/ui/button/index.slint deleted file mode 100644 index 161fef5..0000000 --- a/ui/shared/ui/button/index.slint +++ /dev/null @@ -1,2 +0,0 @@ -// Button Component Public API -export { Button } from "button.slint"; \ No newline at end of file diff --git a/ui/shared/ui/card/card.slint b/ui/shared/ui/card/card.slint deleted file mode 100644 index 0e3055f..0000000 --- a/ui/shared/ui/card/card.slint +++ /dev/null @@ -1,27 +0,0 @@ -import { VerticalBox } from "std-widgets.slint"; - -// Basic Card Component -export component Card { - in property background-color: white; - in property border-color: #e2e8f0; - in property border-radius: 8px; - in property border-width: 1px; - in property card-padding: 16px; - in property shadow-blur: 4px; - in property shadow-color: #00000010; - - Rectangle { - background: background-color; - border-radius: border-radius; - border-width: border-width; - border-color: border-color; - drop-shadow-blur: shadow-blur; - drop-shadow-color: shadow-color; - drop-shadow-offset-y: 2px; - - VerticalBox { - padding: root.card-padding; - @children - } - } -} \ No newline at end of file diff --git a/ui/shared/ui/card/index.slint b/ui/shared/ui/card/index.slint deleted file mode 100644 index 52af053..0000000 --- a/ui/shared/ui/card/index.slint +++ /dev/null @@ -1,2 +0,0 @@ -// Card Component Public API -export { Card } from "card.slint"; \ No newline at end of file diff --git a/ui/shared/ui/index.slint b/ui/shared/ui/index.slint index 8a63cbc..250c5c9 100644 --- a/ui/shared/ui/index.slint +++ b/ui/shared/ui/index.slint @@ -1,9 +1,5 @@ // UI Kit Public API // This file exports all shared UI components -// Export basic components -export { Button } from "button/index.slint"; -export { Card } from "card/index.slint"; -export { Input } from "input/index.slint"; -export { LoadingView } from "loading/index.slint"; -export { Container, Grid } from "layout/index.slint"; \ No newline at end of file +// Export shared components +export { LoadingView } from "loading/loading.slint"; \ No newline at end of file diff --git a/ui/shared/ui/input/index.slint b/ui/shared/ui/input/index.slint deleted file mode 100644 index c714d2d..0000000 --- a/ui/shared/ui/input/index.slint +++ /dev/null @@ -1,2 +0,0 @@ -// Input Component Public API -export { Input } from "input.slint"; \ No newline at end of file diff --git a/ui/shared/ui/input/input.slint b/ui/shared/ui/input/input.slint deleted file mode 100644 index 856bc4f..0000000 --- a/ui/shared/ui/input/input.slint +++ /dev/null @@ -1,20 +0,0 @@ -import { LineEdit } from "std-widgets.slint"; - -// Basic Input Component -export component Input { - in-out property text; - in property placeholder; - in property enabled: true; - - callback edited(string); - - LineEdit { - text: root.text; - placeholder-text: root.placeholder; - enabled: root.enabled; - edited(t) => { - root.text = t; - root.edited(t); - } - } -} \ No newline at end of file diff --git a/ui/shared/ui/layout/index.slint b/ui/shared/ui/layout/index.slint deleted file mode 100644 index c0019de..0000000 --- a/ui/shared/ui/layout/index.slint +++ /dev/null @@ -1,2 +0,0 @@ -// Layout Components Public API -export { Container, Grid } from "layout.slint"; \ No newline at end of file diff --git a/ui/shared/ui/layout/layout.slint b/ui/shared/ui/layout/layout.slint deleted file mode 100644 index f719f40..0000000 --- a/ui/shared/ui/layout/layout.slint +++ /dev/null @@ -1,32 +0,0 @@ -import { VerticalBox, HorizontalBox } from "std-widgets.slint"; - -// Container Component -export component Container { - in property container-max-width: 1200px; - in property container-padding: 16px; - - HorizontalBox { - alignment: center; - - Rectangle { - max-width: root.container-max-width; - - VerticalBox { - padding: root.container-padding; - @children - } - } - } -} - -// Grid Component -export component Grid { - in property gap: 16px; - in property columns: 2; - - // Basic grid implementation - can be enhanced - VerticalBox { - spacing: root.gap; - @children - } -} \ No newline at end of file diff --git a/ui/shared/ui/loading/index.slint b/ui/shared/ui/loading/index.slint deleted file mode 100644 index 60c0715..0000000 --- a/ui/shared/ui/loading/index.slint +++ /dev/null @@ -1,2 +0,0 @@ -// Loading Component Public API -export { LoadingView } from "loading.slint"; \ No newline at end of file diff --git a/ui/shared/ui/loading/loading.slint b/ui/shared/ui/loading/loading.slint index b988e60..7d5c05c 100644 --- a/ui/shared/ui/loading/loading.slint +++ b/ui/shared/ui/loading/loading.slint @@ -1,53 +1,177 @@ -import { VerticalBox } from "std-widgets.slint"; +import { ProgressIndicator, Button, Palette } from "std-widgets.slint"; -// Loading component for app initialization +// Loading component for app initialization with health check export component LoadingView { - in-out property primary-color: #2563eb; - in-out property background-color: #f8fafc; - in-out property text-color: #1e293b; + // Health check state + in-out property is-loading: true; + in-out property has-error: false; + in-out property error-message: "Connection problem"; + in-out property status-text: "Initializing your trading environment..."; in-out property app-version: "0.2.0"; + + // Callbacks + callback retry-connection(); + callback health-check-complete(); Rectangle { - background: background-color; + background: Palette.background; - VerticalBox { + VerticalLayout { alignment: center; - spacing: 20px; + spacing: 32px; + padding: 40px; - // Loading spinner - Rectangle { - width: 60px; - height: 60px; - border-radius: 30px; - background: primary-color; + // Logo section + VerticalLayout { + alignment: center; + spacing: 16px; - // Pulsing animation effect - animate background { - duration: 1s; - iteration-count: -1; - easing: ease-in-out; + Rectangle { + width: 120px; + height: 120px; + border-radius: 60px; + background: Palette.accent-background; + + states [ + error when has-error: { + background: #dc2626; + in { + animate background { duration: 300ms; } + } + } + ] + + Text { + text: "🚀"; + font-size: 48px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + Text { + text: "Ziya"; + font-size: 48px; + color: Palette.foreground; + font-weight: 700; + horizontal-alignment: center; + } + + Text { + text: "Your one-stop trading platform"; + font-size: 16px; + color: Palette.foreground; + opacity: 0.7; + horizontal-alignment: center; } } - Text { - text: "Loading Ziya"; - font-size: 24px; - color: text-color; - font-weight: 600; - } - - Text { - text: "Initializing your trading environment..."; - font-size: 14px; - color: text-color; - opacity: 0.7; + // Status section + VerticalLayout { + alignment: center; + spacing: 24px; + + if is-loading && !has-error: VerticalLayout { + alignment: center; + spacing: 16px; + + Text { + text: "Loading Ziya"; + font-size: 24px; + color: Palette.foreground; + font-weight: 600; + horizontal-alignment: center; + } + + // Using std ProgressIndicator + ProgressIndicator { + width: 200px; + height: 8px; + indeterminate: true; + } + + Text { + text: status-text; + font-size: 14px; + color: Palette.foreground; + opacity: 0.7; + horizontal-alignment: center; + } + } + + if has-error: VerticalLayout { + alignment: center; + spacing: 20px; + + Text { + text: "⚠️ Connection Failed"; + font-size: 24px; + color: #dc2626; + font-weight: 600; + horizontal-alignment: center; + } + + Text { + text: error-message; + font-size: 16px; + color: #dc2626; + horizontal-alignment: center; + } + + Text { + text: "Please check your internet connection and try again"; + font-size: 12px; + color: Palette.foreground; + opacity: 0.6; + horizontal-alignment: center; + } + + Rectangle { + height: 45px; + width: 200px; + border-radius: 8px; + background: Palette.accent-background; + + states [ + pressed when retry-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when retry-touch-area.has-hover: { + background: Palette.control-background; + in { + animate background { duration: 150ms; } + } + } + ] + + Text { + text: "🔄 Retry Connection"; + color: Palette.accent-foreground; + font-size: 14px; + font-weight: 600; + horizontal-alignment: center; + vertical-alignment: center; + } + + retry-touch-area := TouchArea { + clicked => { + retry-connection(); + } + } + } + } } + // Version info at bottom Text { text: "Version " + app-version; font-size: 10px; - color: text-color; - opacity: 0.5; + color: Palette.foreground; + opacity: 0.4; + horizontal-alignment: center; } } } diff --git a/ui/widgets/navigation/ui/navigation-widget.slint b/ui/widgets/navigation/ui/navigation-widget.slint index 1ccc9d8..c2fa953 100644 --- a/ui/widgets/navigation/ui/navigation-widget.slint +++ b/ui/widgets/navigation/ui/navigation-widget.slint @@ -1,97 +1,210 @@ -import { Theme } from "../../../shared/design-system/tokens/theme.slint"; +import { Palette, ScrollView } from "std-widgets.slint"; // Navigation Widget (Sidebar) export component NavigationWidget { + width: 280px; + height: 100%; + in-out property current-page: "Dashboard"; - in property user-initials: "JD"; + in-out property user-initials: "JD"; + in-out property sidebar-state: "full"; callback navigation-changed(string); callback logout-clicked(); + callback toggle-sidebar(); Rectangle { - width: 100%; - height: 100%; - background: Theme.nav-background; + background: Palette.alternate-background; + + // Right border using a separate rectangle + Rectangle { + width: 1px; + height: 100%; + x: parent.width - 1px; + background: Palette.border; + } + + // Toggle button positioned in the middle of the sidebar edge + Rectangle { + width: 24px; + height: 80px; + x: parent.width - 12px; // Half of the button extends outside the sidebar + y: (parent.height - 80px) / 2; // Center vertically + z: 10; // Ensure it's above other elements + border-radius: 12px; + background: Palette.alternate-background; + border-width: 1px; + border-color: Palette.border; + + states [ + pressed when toggle-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when toggle-touch-area.has-hover: { + background: Palette.accent-background; + x: parent.width - 18px; // Slide out slightly on hover + in { + animate background { duration: 150ms; } + animate x { duration: 150ms; } + } + } + ] + + Text { + text: sidebar-state == "full" ? "‹" : "›"; + color: Palette.alternate-foreground; + font-size: 16px; + font-weight: 600; + horizontal-alignment: center; + vertical-alignment: center; + } + + toggle-touch-area := TouchArea { + clicked => { + root.toggle-sidebar(); + } + } + } VerticalLayout { - padding: 16px; spacing: 20px; - alignment: space-between; + padding: 20px; + padding-bottom: 40px; // Extra padding to prevent logout cut-off - // Top section with logo and navigation - VerticalLayout { - spacing: 20px; + // User Profile Section - Only show in full mode + if sidebar-state == "full": Rectangle { + height: 80px; + background: Palette.control-background; + border-radius: 12px; + border-width: 1px; + border-color: Palette.border; - // Logo Section HorizontalLayout { spacing: 12px; + padding: 16px; alignment: start; - padding-top: 16px; + // User Avatar Rectangle { - width: 40px; - height: 40px; - border-radius: 20px; - background: Theme.primary; + width: 48px; + height: 48px; + border-radius: 24px; + background: Palette.accent-background; Text { - text: "Z"; - color: white; - font-size: 20px; - font-weight: 700; + text: user-initials; + color: Palette.accent-foreground; + font-size: 18px; + font-weight: 600; horizontal-alignment: center; vertical-alignment: center; } } + // User Info VerticalLayout { - spacing: 2px; + spacing: 4px; + alignment: center; Text { - text: "Ziya"; - color: Theme.text-primary; - font-size: 18px; - font-weight: 700; + text: "Trading Account"; + color: Palette.control-foreground; + font-size: 14px; + font-weight: 600; + horizontal-alignment: left; + vertical-alignment: center; } + Text { - text: "Trading Platform"; - color: Theme.text-secondary; + text: "Active Status"; + color: Palette.control-foreground; font-size: 12px; + opacity: 0.7; + horizontal-alignment: left; + vertical-alignment: center; } } } + } + + // User Profile Section - Icon only mode + if sidebar-state == "icon-only": Rectangle { + height: 48px; + width: 48px; + background: Palette.control-background; + border-radius: 24px; + border-width: 1px; + border-color: Palette.border; - // Navigation Menu + Text { + text: user-initials; + color: Palette.accent-foreground; + font-size: 18px; + font-weight: 600; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + // Navigation Items + ScrollView { VerticalLayout { - spacing: 4px; + spacing: 8px; // Dashboard Rectangle { - height: 44px; + height: 48px; + background: current-page == "Dashboard" ? Palette.selection-background : transparent; border-radius: 8px; - background: current-page == "Dashboard" ? Theme.nav-active : transparent; + border-width: current-page == "Dashboard" ? 1px : 0px; + border-color: current-page == "Dashboard" ? Palette.border : transparent; - HorizontalLayout { - padding: 12px; + states [ + pressed when dashboard-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when dashboard-touch-area.has-hover && current-page != "Dashboard": { + background: Palette.control-background; + in { + animate background { duration: 150ms; } + } + } + ] + + if sidebar-state == "full": HorizontalLayout { spacing: 12px; + padding: 12px; alignment: start; Text { text: "📊"; - font-size: 16px; + font-size: 20px; vertical-alignment: center; } Text { text: "Dashboard"; - color: current-page == "Dashboard" ? white : Theme.text-primary; + color: current-page == "Dashboard" ? Palette.selection-foreground : Palette.alternate-foreground; font-size: 14px; font-weight: current-page == "Dashboard" ? 600 : 400; vertical-alignment: center; } } - TouchArea { + if sidebar-state == "icon-only": Text { + text: "📊"; + font-size: 20px; + horizontal-alignment: center; + vertical-alignment: center; + } + + dashboard-touch-area := TouchArea { clicked => { root.current-page = "Dashboard"; root.navigation-changed("Dashboard"); @@ -99,33 +212,115 @@ export component NavigationWidget { } } - // Trading + // Hunting Ground Rectangle { - height: 44px; + height: 48px; + background: current-page == "Hunting Ground" ? Palette.selection-background : transparent; border-radius: 8px; - background: current-page == "Trading" ? Theme.nav-active : transparent; + border-width: current-page == "Hunting Ground" ? 1px : 0px; + border-color: current-page == "Hunting Ground" ? Palette.border : transparent; - HorizontalLayout { - padding: 12px; + states [ + pressed when hunting-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when hunting-touch-area.has-hover && current-page != "Hunting Ground": { + background: Palette.control-background; + in { + animate background { duration: 150ms; } + } + } + ] + + if sidebar-state == "full": HorizontalLayout { spacing: 12px; + padding: 12px; alignment: start; Text { - text: "💹"; - font-size: 16px; + text: "🎯"; + font-size: 20px; + vertical-alignment: center; + } + + Text { + text: "Hunting Ground"; + color: current-page == "Hunting Ground" ? Palette.selection-foreground : Palette.alternate-foreground; + font-size: 14px; + font-weight: current-page == "Hunting Ground" ? 600 : 400; + vertical-alignment: center; + } + } + + if sidebar-state == "icon-only": Text { + text: "🎯"; + font-size: 20px; + horizontal-alignment: center; + vertical-alignment: center; + } + + hunting-touch-area := TouchArea { + clicked => { + root.current-page = "Hunting Ground"; + root.navigation-changed("Hunting Ground"); + } + } + } + + // Trading + Rectangle { + height: 48px; + background: current-page == "Trading" ? Palette.selection-background : transparent; + border-radius: 8px; + border-width: current-page == "Trading" ? 1px : 0px; + border-color: current-page == "Trading" ? Palette.border : transparent; + + states [ + pressed when trading-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when trading-touch-area.has-hover && current-page != "Trading": { + background: Palette.control-background; + in { + animate background { duration: 150ms; } + } + } + ] + + if sidebar-state == "full": HorizontalLayout { + spacing: 12px; + padding: 12px; + alignment: start; + + Text { + text: "⚡"; + font-size: 20px; vertical-alignment: center; } Text { text: "Trading"; - color: current-page == "Trading" ? white : Theme.text-primary; + color: current-page == "Trading" ? Palette.selection-foreground : Palette.alternate-foreground; font-size: 14px; font-weight: current-page == "Trading" ? 600 : 400; vertical-alignment: center; } } - TouchArea { + if sidebar-state == "icon-only": Text { + text: "⚡"; + font-size: 20px; + horizontal-alignment: center; + vertical-alignment: center; + } + + trading-touch-area := TouchArea { clicked => { root.current-page = "Trading"; root.navigation-changed("Trading"); @@ -135,31 +330,55 @@ export component NavigationWidget { // Portfolio Rectangle { - height: 44px; + height: 48px; + background: current-page == "Portfolio" ? Palette.selection-background : transparent; border-radius: 8px; - background: current-page == "Portfolio" ? Theme.nav-active : transparent; + border-width: current-page == "Portfolio" ? 1px : 0px; + border-color: current-page == "Portfolio" ? Palette.border : transparent; - HorizontalLayout { - padding: 12px; + states [ + pressed when portfolio-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when portfolio-touch-area.has-hover && current-page != "Portfolio": { + background: Palette.control-background; + in { + animate background { duration: 150ms; } + } + } + ] + + if sidebar-state == "full": HorizontalLayout { spacing: 12px; + padding: 12px; alignment: start; Text { text: "💼"; - font-size: 16px; + font-size: 20px; vertical-alignment: center; } Text { text: "Portfolio"; - color: current-page == "Portfolio" ? white : Theme.text-primary; + color: current-page == "Portfolio" ? Palette.selection-foreground : Palette.alternate-foreground; font-size: 14px; font-weight: current-page == "Portfolio" ? 600 : 400; vertical-alignment: center; } } - TouchArea { + if sidebar-state == "icon-only": Text { + text: "💼"; + font-size: 20px; + horizontal-alignment: center; + vertical-alignment: center; + } + + portfolio-touch-area := TouchArea { clicked => { root.current-page = "Portfolio"; root.navigation-changed("Portfolio"); @@ -169,121 +388,118 @@ export component NavigationWidget { // Markets Rectangle { - height: 44px; + height: 48px; + background: current-page == "Markets" ? Palette.selection-background : transparent; border-radius: 8px; - background: current-page == "Markets" ? Theme.nav-active : transparent; + border-width: current-page == "Markets" ? 1px : 0px; + border-color: current-page == "Markets" ? Palette.border : transparent; - HorizontalLayout { - padding: 12px; + states [ + pressed when markets-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when markets-touch-area.has-hover && current-page != "Markets": { + background: Palette.control-background; + in { + animate background { duration: 150ms; } + } + } + ] + + if sidebar-state == "full": HorizontalLayout { spacing: 12px; + padding: 12px; alignment: start; Text { text: "📈"; - font-size: 16px; + font-size: 20px; vertical-alignment: center; } Text { text: "Markets"; - color: current-page == "Markets" ? white : Theme.text-primary; + color: current-page == "Markets" ? Palette.selection-foreground : Palette.alternate-foreground; font-size: 14px; font-weight: current-page == "Markets" ? 600 : 400; vertical-alignment: center; } } - TouchArea { + if sidebar-state == "icon-only": Text { + text: "📈"; + font-size: 20px; + horizontal-alignment: center; + vertical-alignment: center; + } + + markets-touch-area := TouchArea { clicked => { root.current-page = "Markets"; root.navigation-changed("Markets"); } } } - - // Hunting Ground - Rectangle { - height: 44px; - border-radius: 8px; - background: current-page == "HuntingGround" ? Theme.nav-active : transparent; - - HorizontalLayout { - padding: 12px; - spacing: 12px; - alignment: start; - - Text { - text: "🎯"; - font-size: 16px; - vertical-alignment: center; - } - - Text { - text: "Hunting Ground"; - color: current-page == "HuntingGround" ? white : Theme.text-primary; - font-size: 14px; - font-weight: current-page == "HuntingGround" ? 600 : 400; - vertical-alignment: center; - } - } - - TouchArea { - clicked => { - root.current-page = "HuntingGround"; - root.navigation-changed("HuntingGround"); - } - } - } } } - // User Profile Section (bottom) + // Logout Section Rectangle { - height: 60px; + height: 48px; + background: transparent; border-radius: 8px; - background: Theme.surface; - HorizontalLayout { - padding: 12px; - spacing: 12px; - alignment: start; - - Rectangle { - width: 36px; - height: 36px; - border-radius: 18px; - background: Theme.primary; - - Text { - text: user-initials; - color: white; - font-size: 14px; - font-weight: 600; - horizontal-alignment: center; - vertical-alignment: center; + states [ + pressed when logout-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } } } + hover when logout-touch-area.has-hover: { + background: Palette.control-background; + in { + animate background { duration: 150ms; } + } + } + ] + + if sidebar-state == "full": HorizontalLayout { + spacing: 12px; + padding: 12px; + alignment: start; - VerticalLayout { - spacing: 2px; - - Text { - text: "User"; - color: Theme.text-primary; - font-size: 14px; - font-weight: 600; - } - - Text { - text: "Logout"; - color: Theme.text-secondary; - font-size: 12px; - } + Text { + text: "🚪"; + font-size: 20px; + vertical-alignment: center; + } + + Text { + text: "Logout"; + color: Palette.alternate-foreground; + font-size: 14px; + font-weight: 400; + vertical-alignment: center; + opacity: 0.8; } } - TouchArea { - clicked => { root.logout-clicked(); } + if sidebar-state == "icon-only": Text { + text: "🚪"; + font-size: 20px; + horizontal-alignment: center; + vertical-alignment: center; + opacity: 0.8; + } + + logout-touch-area := TouchArea { + clicked => { + root.logout-clicked(); + } } } } diff --git a/ui/widgets/navigation/ui/sidebar.slint b/ui/widgets/navigation/ui/sidebar.slint deleted file mode 100644 index 83cf43f..0000000 --- a/ui/widgets/navigation/ui/sidebar.slint +++ /dev/null @@ -1,178 +0,0 @@ -import { VerticalBox, HorizontalBox, ScrollView } from "std-widgets.slint"; - -// Navigation menu item data structure -export struct MenuItem { - name: string, - icon: string, - active: bool, -} - -// Professional sidebar navigation with direct styling -export component Sidebar { - in property <[MenuItem]> menu-items: [ - { name: "Dashboard", icon: "📊", active: true }, - { name: "Profile", icon: "👤", active: false }, - { name: "Trading", icon: "💹", active: false }, - { name: "Portfolio", icon: "💼", active: false }, - { name: "Markets", icon: "📈", active: false }, - { name: "Hunting Ground", icon: "🎯", active: false }, - { name: "Analytics", icon: "📊", active: false }, - ]; - - callback menu-item-clicked(string); - - width: 256px; - - Rectangle { - background: #f2f2f2; - border-width: 0px; - border-color: #e5e6e6; - - ScrollView { - viewport-height: root.height; - - VerticalBox { - padding: 16px; - spacing: 8px; - - // Sidebar Header - Rectangle { - height: 60px; - - VerticalBox { - alignment: center; - spacing: 8px; - - // Logo area - Rectangle { - height: 32px; - - HorizontalBox { - alignment: center; - spacing: 8px; - - // App icon - Rectangle { - width: 24px; - height: 24px; - background: #570df8; - border-radius: 8px; - } - - Text { - text: "Ziya"; - color: #1f2937; - font-size: 18px; - font-weight: 700; - } - } - } - - // Divider - Rectangle { - height: 1px; - background: #e5e6e6; - } - } - } - - // Menu Items - for item[index] in menu-items: Rectangle { - height: 44px; - border-radius: 8px; - - // Active/hover state styling - background: item.active ? #570df8 : transparent; - - states [ - active when item.active: { - background: #570df8; - } - hover when menu-area.has-hover && !item.active: { - background: #e5e6e6; - } - ] - - animate background { duration: 150ms; } - - HorizontalBox { - padding-left: 16px; - padding-right: 16px; - alignment: start; - spacing: 12px; - - // Icon - Text { - text: item.icon; - font-size: 16px; - vertical-alignment: center; - width: 20px; - } - - // Menu item text - Text { - text: item.name; - color: item.active ? #ffffff : #1f2937; - font-size: 14px; - font-weight: item.active ? 600 : 500; - vertical-alignment: center; - } - - // Active indicator - if item.active: Rectangle { - width: 4px; - height: 20px; - background: #ffffff; - border-radius: 2px; - } - } - - menu-area := TouchArea { - clicked => { menu-item-clicked(item.name); } - } - } - - // Spacer to push footer to bottom - Rectangle { - height: 1px; - } - - // Sidebar Footer - Rectangle { - height: 60px; - - VerticalBox { - alignment: center; - spacing: 8px; - - // Divider - Rectangle { - height: 1px; - background: #e5e6e6; - } - - // Status section - HorizontalBox { - alignment: center; - spacing: 8px; - - // Status indicator - Rectangle { - width: 8px; - height: 8px; - background: #36d399; - border-radius: 4px; - } - - Text { - text: "Connected"; - color: #6b7280; - font-size: 12px; - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/ui/widgets/token-column/ui/analysis-complete-column.slint b/ui/widgets/token-column/ui/analysis-complete-column.slint new file mode 100644 index 0000000..24ba6a8 --- /dev/null +++ b/ui/widgets/token-column/ui/analysis-complete-column.slint @@ -0,0 +1,193 @@ +import { Palette, ScrollView } from "std-widgets.slint"; +import { MaxDepthReachedUiData } from "../../../shared/types/token.slint"; + +export component AnalysisCompleteColumn { + width: 100%; // Will be constrained by parent + height: 100%; + + in property <[MaxDepthReachedUiData]> tokens: []; + callback clear-tokens(); + + Rectangle { + background: Palette.alternate-background; + border-radius: 12px; + border-width: 1px; + border-color: Palette.border; + width: 100%; + height: 100%; + + VerticalLayout { + spacing: 0px; + + // Column Header + Rectangle { + height: 48px; + background: Palette.control-background; + border-radius: 12px; + border-width: 1px; + border-color: Palette.border; + + HorizontalLayout { + alignment: space-between; + padding: 12px; + + Text { + text: "✅ Analysis Complete (" + tokens.length + ")"; + color: Palette.control-foreground; + font-size: 14px; + font-weight: 600; + vertical-alignment: center; + } + + Rectangle { + width: 20px; + height: 20px; + background: #f56565; + border-radius: 10px; + + Text { + text: "×"; + color: white; + font-size: 12px; + font-weight: 600; + horizontal-alignment: center; + vertical-alignment: center; + } + + TouchArea { + clicked => { root.clear-tokens(); } + } + } + } + } + + // Scrollable content area + ScrollView { + height: parent.height - 48px; + + VerticalLayout { + spacing: 8px; + padding: 8px; + padding-bottom: 16px; + + // Analysis token cards + for token[i] in tokens: Rectangle { + min-height: 80px; + background: Palette.background; + border-width: 1px; + border-color: Palette.border; + border-radius: 8px; + + states [ + pressed when touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when touch-area.has-hover: { + background: Palette.control-background; + border-color: #10b981; + in { + animate background { duration: 150ms; } + animate border-color { duration: 150ms; } + } + } + ] + + // Left border accent (green for complete) + Rectangle { + width: 3px; + height: 100%; + background: #10b981; + x: 0px; + border-radius: 3px; + } + + HorizontalLayout { + padding: 12px; + spacing: 12px; + + // Token avatar + Rectangle { + width: 40px; + height: 40px; + border-radius: 10px; + background: #10b981; + + Text { + text: "✓"; + color: white; + font-size: 12px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + // Token info + VerticalLayout { + spacing: 6px; + alignment: start; + + // Name and completion status + HorizontalLayout { + spacing: 8px; + alignment: start; + + Text { + text: token.name; + color: Palette.foreground; + font-size: 14px; + font-weight: 600; + vertical-alignment: center; + } + + Rectangle { + height: 18px; + border-radius: 9px; + background: #10b981; + padding-left: 8px; + padding-right: 8px; + + Text { + text: "Complete"; + color: white; + font-size: 10px; + font-weight: 600; + horizontal-alignment: center; + vertical-alignment: center; + } + } + } + + // Mint address + Text { + text: token.mint; + color: Palette.foreground; + font-size: 11px; + wrap: word-wrap; + opacity: 0.7; + vertical-alignment: center; + } + + // Icons and timestamp + Text { + text: ""; + color: Palette.foreground; + font-size: 11px; + opacity: 0.7; + vertical-alignment: center; + } + } + } + + touch-area := TouchArea { + // Add click functionality here if needed + } + } + } + } + } + } +} \ No newline at end of file diff --git a/ui/widgets/token-column/ui/cex-tokens-column.slint b/ui/widgets/token-column/ui/cex-tokens-column.slint new file mode 100644 index 0000000..e757d16 --- /dev/null +++ b/ui/widgets/token-column/ui/cex-tokens-column.slint @@ -0,0 +1,193 @@ +import { Palette, ScrollView } from "std-widgets.slint"; +import { CexUpdatedUiData } from "../../../shared/types/token.slint"; + +export component CexTokensColumn { + width: 100%; // Will be constrained by parent + height: 100%; + + in property <[CexUpdatedUiData]> tokens: []; + callback clear-tokens(); + + Rectangle { + background: Palette.alternate-background; + border-radius: 12px; + border-width: 1px; + border-color: Palette.border; + width: 100%; + height: 100%; + + VerticalLayout { + spacing: 0px; + + // Column Header + Rectangle { + height: 48px; + background: Palette.control-background; + border-radius: 12px; + border-width: 1px; + border-color: Palette.border; + + HorizontalLayout { + alignment: space-between; + padding: 12px; + + Text { + text: "📊 CEX Analyst (" + tokens.length + ")"; + color: Palette.control-foreground; + font-size: 14px; + font-weight: 600; + vertical-alignment: center; + } + + Rectangle { + width: 20px; + height: 20px; + background: #f56565; + border-radius: 10px; + + Text { + text: "×"; + color: white; + font-size: 12px; + font-weight: 600; + horizontal-alignment: center; + vertical-alignment: center; + } + + TouchArea { + clicked => { root.clear-tokens(); } + } + } + } + } + + // Scrollable content area + ScrollView { + height: parent.height - 48px; + + VerticalLayout { + spacing: 8px; + padding: 8px; + padding-bottom: 16px; + + // CEX token cards + for token[i] in tokens: Rectangle { + min-height: 80px; + background: Palette.background; + border-width: 1px; + border-color: Palette.border; + border-radius: 8px; + + states [ + pressed when touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when touch-area.has-hover: { + background: Palette.control-background; + border-color: #f59e0b; + in { + animate background { duration: 150ms; } + animate border-color { duration: 150ms; } + } + } + ] + + // Left border accent (orange for CEX) + Rectangle { + width: 3px; + height: 100%; + background: #f59e0b; + x: 0px; + border-radius: 3px; + } + + HorizontalLayout { + padding: 12px; + spacing: 12px; + + // Token avatar + Rectangle { + width: 40px; + height: 40px; + border-radius: 10px; + background: #f59e0b; + + Text { + text: "C"; + color: white; + font-size: 12px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + // Token info + VerticalLayout { + spacing: 6px; + alignment: start; + + // Name and CEX info + HorizontalLayout { + spacing: 8px; + alignment: start; + + Text { + text: token.name; + color: Palette.foreground; + font-size: 14px; + font-weight: 600; + vertical-alignment: center; + } + + Rectangle { + height: 18px; + border-radius: 9px; + background: #f59e0b; + padding-left: 8px; + padding-right: 8px; + + Text { + text: token.cex-name; + color: white; + font-size: 10px; + font-weight: 600; + horizontal-alignment: center; + vertical-alignment: center; + } + } + } + + // Mint address + Text { + text: token.mint; + color: Palette.foreground; + font-size: 11px; + wrap: word-wrap; + opacity: 0.7; + vertical-alignment: center; + } + + // Icons and timestamp + Text { + text: ""; + color: Palette.foreground; + font-size: 11px; + opacity: 0.7; + vertical-alignment: center; + } + } + } + + touch-area := TouchArea { + // Add click functionality here if needed + } + } + } + } + } + } +} \ No newline at end of file diff --git a/ui/widgets/token-column/ui/mod.slint b/ui/widgets/token-column/ui/mod.slint new file mode 100644 index 0000000..c7fd7d0 --- /dev/null +++ b/ui/widgets/token-column/ui/mod.slint @@ -0,0 +1,6 @@ +// Token Column Widgets Module +// Following Feature-Sliced Design - widgets layer + +export { NewTokensColumn } from "new-tokens-column.slint"; +export { CexTokensColumn } from "cex-tokens-column.slint"; +export { AnalysisCompleteColumn } from "analysis-complete-column.slint"; \ No newline at end of file diff --git a/ui/widgets/token-column/ui/new-tokens-column.slint b/ui/widgets/token-column/ui/new-tokens-column.slint new file mode 100644 index 0000000..c862daa --- /dev/null +++ b/ui/widgets/token-column/ui/new-tokens-column.slint @@ -0,0 +1,193 @@ +import { Palette, ScrollView } from "std-widgets.slint"; +import { NewTokenUiData } from "../../../shared/types/token.slint"; + +export component NewTokensColumn { + width: 100%; // Will be constrained by parent + height: 100%; + + in property <[NewTokenUiData]> tokens: []; + callback clear-tokens(); + + Rectangle { + background: Palette.alternate-background; + border-radius: 12px; + border-width: 1px; + border-color: Palette.border; + width: 100%; + height: 100%; + + VerticalLayout { + spacing: 0px; + + // Column Header + Rectangle { + height: 48px; + background: Palette.control-background; + border-radius: 12px; + border-width: 1px; + border-color: Palette.border; + + HorizontalLayout { + alignment: space-between; + padding: 12px; + + Text { + text: "🆕 New Tokens (" + tokens.length + ")"; + color: Palette.control-foreground; + font-size: 14px; + font-weight: 600; + vertical-alignment: center; + } + + Rectangle { + width: 20px; + height: 20px; + background: #f56565; + border-radius: 10px; + + Text { + text: "×"; + color: white; + font-size: 12px; + font-weight: 600; + horizontal-alignment: center; + vertical-alignment: center; + } + + TouchArea { + clicked => { root.clear-tokens(); } + } + } + } + } + + // Scrollable content area + ScrollView { + height: parent.height - 48px; + + VerticalLayout { + spacing: 8px; + padding: 8px; + padding-bottom: 16px; + + // New token cards + for token[i] in tokens: Rectangle { + min-height: 80px; + background: Palette.background; + border-width: 1px; + border-color: Palette.border; + border-radius: 8px; + + states [ + pressed when touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when touch-area.has-hover: { + background: Palette.control-background; + border-color: Palette.accent-background; + in { + animate background { duration: 150ms; } + animate border-color { duration: 150ms; } + } + } + ] + + // Left border accent + Rectangle { + width: 3px; + height: 100%; + background: Palette.accent-background; + x: 0px; + border-radius: 3px; + } + + HorizontalLayout { + padding: 12px; + spacing: 12px; + + // Token avatar + Rectangle { + width: 40px; + height: 40px; + border-radius: 10px; + background: Palette.accent-background; + + Text { + text: token.symbol != "" ? token.symbol : "?"; + color: Palette.accent-foreground; + font-size: 12px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + // Token info + VerticalLayout { + spacing: 6px; + alignment: start; + + // Name and symbol + HorizontalLayout { + spacing: 8px; + alignment: start; + + Text { + text: token.name; + color: Palette.foreground; + font-size: 14px; + font-weight: 600; + vertical-alignment: center; + } + + if token.symbol != "": Rectangle { + height: 18px; + border-radius: 9px; + background: Palette.accent-background; + padding-left: 8px; + padding-right: 8px; + + Text { + text: token.symbol; + color: Palette.accent-foreground; + font-size: 10px; + font-weight: 600; + horizontal-alignment: center; + vertical-alignment: center; + } + } + } + + // Mint address + Text { + text: token.mint; + color: Palette.foreground; + font-size: 11px; + wrap: word-wrap; + opacity: 0.7; + vertical-alignment: center; + } + + // Icons and timestamp + Text { + text: ""; + color: Palette.foreground; + font-size: 11px; + opacity: 0.7; + vertical-alignment: center; + } + } + } + + touch-area := TouchArea { + // Add click functionality here if needed + } + } + } + } + } + } +} \ No newline at end of file diff --git a/ui/widgets/window-controls/index.slint b/ui/widgets/window-controls/index.slint index 07ac388..8116901 100644 --- a/ui/widgets/window-controls/index.slint +++ b/ui/widgets/window-controls/index.slint @@ -1,2 +1,2 @@ // Window Controls Widget Public API -export { WindowControlsWidget } from "ui/window-controls-widget.slint"; \ No newline at end of file +export { TitleBar } from "ui/title-bar.slint"; \ No newline at end of file diff --git a/ui/widgets/window-controls/ui/title-bar.slint b/ui/widgets/window-controls/ui/title-bar.slint index b4e0757..4e247ff 100644 --- a/ui/widgets/window-controls/ui/title-bar.slint +++ b/ui/widgets/window-controls/ui/title-bar.slint @@ -1,108 +1,106 @@ -import { Button, HorizontalBox } from "std-widgets.slint"; -import { Theme } from "../../../shared/design-system/tokens/theme.slint"; +import { Palette } from "std-widgets.slint"; // Professional title bar component with theme support export component TitleBar { // Window state in-out property is-maximized: false; + in-out property is-dark-theme: true; // Callbacks for window controls callback minimize-window(); callback maximize-window(); callback close-window(); callback toggle-theme(); - callback start-drag-window(); - height: 32px; - + height: 40px; + Rectangle { - background: Theme.surface; - border-width: 0px; - border-color: Theme.border; + background: Palette.alternate-background; - // Drag area for the title bar - drag-area := TouchArea { + // Bottom border using a separate rectangle + Rectangle { width: 100%; - height: 100%; - - moved => { - if (self.pressed) { - start-drag-window(); - } - } + height: 1px; + y: parent.height - 1px; + background: Palette.border; } - HorizontalBox { - padding: 16px; - alignment: stretch; + HorizontalLayout { + spacing: 0px; + alignment: space-between; + padding-left: 16px; + padding-right: 8px; + padding-top: 8px; + padding-bottom: 8px; - // Left side - App branding - HorizontalBox { + // Left side - App title and drag area + HorizontalLayout { + spacing: 12px; alignment: start; - spacing: 8px; // App icon Rectangle { - width: 20px; - height: 20px; + width: 24px; + height: 24px; + border-radius: 6px; + background: Palette.accent-background; - // Ziya logo (layered rectangles) - Rectangle { - width: 16px; - height: 16px; - background: #570df8; - border-radius: 3px; - - Rectangle { - x: 2px; - y: 2px; - width: 12px; - height: 12px; - background: #4506cb; - border-radius: 2px; - } + Text { + text: "Z"; + color: Palette.accent-foreground; + font-size: 14px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; } } - // App name + // App title Text { - text: "Ziya"; - color: Theme.text-primary; + text: "Ziya - Trading Platform"; + color: Palette.alternate-foreground; font-size: 14px; - font-weight: 600; + font-weight: 500; vertical-alignment: center; } } - // Right side - Controls - HorizontalBox { - alignment: end; + // Right side - Theme toggle and window controls + HorizontalLayout { spacing: 4px; + alignment: end; - // Theme switcher + // Theme toggle button Rectangle { width: 32px; - height: 32px; - border-radius: 4px; + height: 24px; + border-radius: 6px; states [ - hover when theme-area.has-hover: { - background: Theme.nav-hover; + pressed when theme-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when theme-touch-area.has-hover: { + background: Palette.control-background; + in { + animate background { duration: 150ms; } + } } ] - // Theme icon - shows current theme state Text { - text: Theme.is-dark-mode ? "🌙" : "☀️"; + text: is-dark-theme ? "☀️" : "🌙"; font-size: 14px; horizontal-alignment: center; vertical-alignment: center; } - theme-area := TouchArea { - clicked => { - Theme.toggle-theme(); - toggle-theme(); + theme-touch-area := TouchArea { + clicked => { + root.toggle-theme(); } } } @@ -110,78 +108,110 @@ export component TitleBar { // Minimize button Rectangle { width: 32px; - height: 32px; - border-radius: 4px; + height: 24px; + border-radius: 6px; states [ - hover when min-area.has-hover: { - background: Theme.nav-hover; + pressed when minimize-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when minimize-touch-area.has-hover: { + background: Palette.control-background; + in { + animate background { duration: 150ms; } + } } ] - // Minimize icon Text { text: "−"; - color: Theme.text-secondary; + color: Palette.alternate-foreground; + font-size: 16px; + font-weight: 300; + horizontal-alignment: center; + vertical-alignment: center; + } + + minimize-touch-area := TouchArea { + clicked => { + root.minimize-window(); + } + } + } + + // Maximize button + Rectangle { + width: 32px; + height: 24px; + border-radius: 6px; + + states [ + pressed when maximize-touch-area.pressed: { + background: Palette.selection-background; + in { + animate background { duration: 100ms; } + } + } + hover when maximize-touch-area.has-hover: { + background: Palette.control-background; + in { + animate background { duration: 150ms; } + } + } + ] + + Text { + text: "□"; + color: Palette.alternate-foreground; font-size: 14px; horizontal-alignment: center; vertical-alignment: center; } - min-area := TouchArea { - clicked => { minimize-window(); } - } - } - - // Maximize/Restore button - Rectangle { - width: 32px; - height: 32px; - border-radius: 4px; - - states [ - hover when max-area.has-hover: { - background: Theme.nav-hover; + maximize-touch-area := TouchArea { + clicked => { + root.maximize-window(); } - ] - - // Maximize icon - Text { - text: is-maximized ? "❐" : "□"; - color: Theme.text-secondary; - font-size: 12px; - horizontal-alignment: center; - vertical-alignment: center; - } - - max-area := TouchArea { - clicked => { maximize-window(); } } } // Close button Rectangle { width: 32px; - height: 32px; - border-radius: 4px; + height: 24px; + border-radius: 6px; states [ - hover when close-area.has-hover: { - background: #ef4444; + pressed when close-touch-area.pressed: { + background: #d53f3f; + in { + animate background { duration: 100ms; } + } + } + hover when close-touch-area.has-hover: { + background: #f56565; + in { + animate background { duration: 150ms; } + } } ] - // Close icon Text { - text: "✕"; - color: close-area.has-hover ? #ffffff : Theme.text-secondary; - font-size: 10px; + text: "×"; + color: close-touch-area.has-hover ? white : Palette.alternate-foreground; + font-size: 16px; + font-weight: 400; horizontal-alignment: center; vertical-alignment: center; } - close-area := TouchArea { - clicked => { close-window(); } + close-touch-area := TouchArea { + clicked => { + root.close-window(); + } } } } diff --git a/ui/widgets/window-controls/ui/window-controls-widget.slint b/ui/widgets/window-controls/ui/window-controls-widget.slint deleted file mode 100644 index 7b53fa0..0000000 --- a/ui/widgets/window-controls/ui/window-controls-widget.slint +++ /dev/null @@ -1,18 +0,0 @@ -import { TitleBar } from "title-bar.slint"; - -// Window Controls Widget -export component WindowControlsWidget { - callback theme-toggle-clicked(); - callback minimize-clicked(); - callback maximize-clicked(); - callback close-clicked(); - callback start-drag-window(); - - TitleBar { - toggle-theme => { root.theme-toggle-clicked(); } - minimize-window => { root.minimize-clicked(); } - maximize-window => { root.maximize-clicked(); } - close-window => { root.close-clicked(); } - start-drag-window => { root.start-drag-window(); } - } -} \ No newline at end of file