diff --git a/.cursor/rules/roadmap.mdc b/.cursor/rules/roadmap.mdc new file mode 100644 index 0000000..f488709 --- /dev/null +++ b/.cursor/rules/roadmap.mdc @@ -0,0 +1,6 @@ +--- +description: +globs: +alwaysApply: true +--- +You must update your current roadmap file before/after EVERY Change. \ No newline at end of file diff --git a/PlugSnatcher Frontend Refactoring Plan.md b/PlugSnatcher Frontend Refactoring Plan.md new file mode 100644 index 0000000..20473c0 --- /dev/null +++ b/PlugSnatcher Frontend Refactoring Plan.md @@ -0,0 +1,211 @@ +# PlugSnatcher Frontend Refactoring Plan + +## Overview + +This document outlines a comprehensive plan to refactor the PlugSnatcher frontend from a monolithic structure into a well-organized, modular React application. The refactoring will focus on component separation, state management optimization, and establishing a scalable architecture. + +## Current Issues + +- All React components are in a single 1300+ line file (`App.tsx`) +- State management is centralized and lacks separation of concerns +- No clear component hierarchy or organization +- Complex UI logic mixed with event handling and API calls +- Poor testability due to tightly coupled components +- Difficulty maintaining and extending the codebase + +## Refactoring Goals + +- Separate components into individual files with clear responsibilities +- Implement a logical folder structure +- Improve state management using React context or other solutions +- Extract common functionality into hooks and utilities +- Make components more reusable +- Improve code maintainability and readability +- Establish patterns for future development + +## Project Structure + +``` +src/ +├── assets/ # Images, icons, and other static files +├── components/ # UI components +│ ├── common/ # Reusable UI components +│ │ ├── Button/ +│ │ ├── Modal/ +│ │ ├── ProgressBar/ +│ │ └── ... +│ ├── layout/ # Layout components +│ │ ├── Header/ +│ │ ├── Footer/ +│ │ └── ... +│ ├── plugins/ # Plugin-related components +│ │ ├── PluginList/ +│ │ ├── PluginItem/ +│ │ ├── PluginDetails/ +│ │ └── ... +│ ├── server/ # Server-related components +│ │ ├── ServerSelector/ +│ │ ├── ServerInfo/ +│ │ └── ... +│ └── updates/ # Update-related components +│ ├── UpdateControls/ +│ ├── CompatibilityCheck/ +│ └── ... +├── context/ # React context providers +│ ├── ServerContext/ +│ ├── PluginContext/ +│ └── ... +├── hooks/ # Custom React hooks +│ ├── usePluginActions.ts +│ ├── useServerActions.ts +│ ├── useEventListeners.ts +│ └── ... +├── services/ # API and service functions +│ ├── tauriInvoke.ts +│ ├── eventListeners.ts +│ └── ... +├── types/ # TypeScript type definitions +│ ├── plugin.types.ts +│ ├── server.types.ts +│ └── ... +├── utils/ # Utility functions +│ ├── formatters.ts +│ ├── validators.ts +│ └── ... +├── App.tsx # Main App component (refactored) +└── main.tsx # Entry point +``` + +## Implementation Checklist + +### Phase 1: Setup Project Structure and Types ✅ + +- [x] Create folder structure as outlined above +- [x] Extract TypeScript interfaces to separate files + - [x] Create `types/plugin.types.ts` for Plugin interfaces + - [x] Create `types/server.types.ts` for Server interfaces + - [x] Create `types/events.types.ts` for Event payloads + +### Phase 2: Extract Utility Functions ✅ + +- [x] Create utility files + - [x] `utils/serverUtils.ts` - Server type icons and helpers + - [x] `utils/formatters.ts` - Text formatting functions + - [x] `utils/validators.ts` - Data validation functions + +### Phase 3: Extract Common UI Components ✅ + +- [x] Create common UI components + - [x] `components/common/Modal/Modal.tsx` - Base modal component + - [x] `components/common/ProgressBar/ProgressBar.tsx` - Progress indicator + - [x] `components/common/Button/Button.tsx` - Styled button component + - [x] `components/common/Badge/Badge.tsx` - For platform compatibility badges + +### Phase 4: Implement Context for State Management ✅ + +- [x] Create server context + - [x] `context/ServerContext/ServerContext.tsx` - Server state provider + - [x] `context/ServerContext/useServerContext.ts` - Server context hook +- [x] Create plugin context + - [x] `context/PluginContext/PluginContext.tsx` - Plugin state provider + - [x] `context/PluginContext/usePluginContext.ts` - Plugin context hook +- [x] Create UI context + - [x] `context/UIContext/UIContext.tsx` - UI state provider (modals, errors) + - [x] `context/UIContext/useUIContext.ts` - UI context hook + +### Phase 5: Create Custom Hooks ✅ + +- [x] Create event listener hooks + - [x] `hooks/useEventListeners.ts` - Tauri event listener setup +- [x] Create server action hooks + - [x] `hooks/useServerActions.ts` - Server-related actions +- [x] Create plugin action hooks + - [x] `hooks/usePluginActions.ts` - Plugin-related actions + - [x] `hooks/useUpdateActions.ts` - Update-related actions + +### Phase 6: Implement UI Components + +#### Layout Components: +- ✅ Footer - Display application version and GitHub link +- ✅ MainContent - Main application content container +- ✅ ServerSelector - For selecting and managing server paths +- ✅ ServerInfo - Display information about the selected server +- ✅ ScanProgress - Show progress during server scanning + +#### Plugin Components: +- ✅ PluginList - Container for listing all plugins +- ✅ PluginItem - Individual plugin display with actions +- ✅ PluginDetails - Detailed view of a plugin +- ✅ NoPluginsMessage - Message shown when no plugins are found + +#### Update Components: +- ✅ UpdateControls - Controls for managing plugin updates +- ✅ BulkUpdateProgress - Shows progress during bulk updates +- ✅ CompatibilityCheckDialog - Confirms compatibility before update +- ✅ PremiumPluginModal - Info for premium plugins +- ✅ DownloadProgressIndicator - Shows download progress +- ✅ PluginMatchSelector - For selecting from potential plugin matches +- ✅ WarningModal - For displaying important warnings to users + +#### Additional Steps: +- ✅ Fixed linter errors related to Tauri API imports +- ✅ Added TypeScript declarations for Tauri API modules + +### Phase 7: Refactor Main App Component ✅ + +- ✅ Streamline App.tsx to use new components and contexts +- ✅ Remove direct state management from App.tsx +- ✅ Implement provider wrapping in the component tree +- ✅ Verify all functionality works as before + +### Phase 8: Performance Optimizations + +- [x] Add React.memo() to prevent unnecessary renders +- [ ] Optimize context usage to prevent unnecessary re-renders +- [x] Use callback and memoization for expensive operations +- [ ] Review and optimize state update patterns + +### Phase 9: Testing and Documentation + +- [ ] Add component documentation +- [ ] Add JSDoc comments for functions and hooks +- [ ] Set up unit testing framework +- [ ] Write tests for critical components + +### Phase 10: Final Review and Cleanup + +- [ ] Remove unused code and comments +- [ ] Ensure consistent naming conventions +- [ ] Verify all features work correctly +- [ ] Review for any performance issues +- [ ] Finalize documentation + +## Implementation Strategy + +### Approach + +1. **Incremental Migration**: Refactor incrementally, keeping the app functional at each step +2. **Component by Component**: Start with smaller, leaf components and work upward +3. **Test Frequently**: Verify functionality after each significant change +4. **State Migration**: Move state management to context providers gradually + +### Best Practices + +- Keep components focused on a single responsibility +- Use TypeScript interfaces for all component props +- Maintain clear separation between UI components and business logic +- Follow consistent naming conventions +- Use React's composition pattern for component reuse +- Document complex logic with comments + +## Future Enhancements + +- Consider adding a state management library if complexity increases (Redux, Zustand, etc.) +- Implement React Router for potential multi-page navigation +- Add i18n for internationalization +- Implement error boundary components for better error handling +- Set up automated testing workflows + +## Conclusion + +This refactoring plan provides a comprehensive roadmap for transforming the PlugSnatcher frontend into a maintainable, modular React application. By following this plan, we will address the current issues and establish patterns that support future development and scaling of the application. \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md index ba39ba2..367ffc1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -57,7 +57,9 @@ - [x] Improve matching algorithm to reduce false positives - [x] Complete GitHub integration - [ ] Add GitHub API authentication for higher rate limits (environment variable support exists) - - [ ] Fix command parameter naming issues for update checks + - [x] Fix command parameter naming issues for update checks + - [x] Fix plugin update checking functionality after UI refactor + - [x] Fix serverPath not being passed correctly to PluginContext - [ ] Optimize duplicate plugin search results (e.g., ViaVersion plugin) - [x] Implement changelog extraction - [x] Create plugin backup functionality @@ -72,14 +74,14 @@ - [x] Display up-to-date version information for all plugins - [x] Handle premium plugins with user guidance for manual downloads - [ ] Present multiple potential matches for ambiguous plugins - - [ ] Make version numbers clickable links to repository sources + - [x] Make version numbers clickable links to repository sources - [ ] Allow user selection of correct plugin match when multiple are found -- [ ] Server platform compatibility matching (High Priority) - - [ ] Detect server platform and version accurately (Paper, Spigot, Forge, NeoForge, Fabric, etc.) - - [ ] Filter plugin updates to match the server platform - - [ ] Prevent incompatible version updates - - [ ] Add platform indicators in the UI for available updates - - [ ] Allow manual selection of target platform when multiple are available +- [x] Server platform compatibility matching + - [x] Detect server platform and version accurately (Paper, Spigot, Forge, NeoForge, Fabric, etc.) + - [x] Filter plugin updates to match the server platform + - [x] Prevent incompatible version updates + - [x] Add compatibility indicators in the code for available updates + - [x] Add platform indicators in the UI for available updates ## UI Development (In Progress) - [x] Design and implement main dashboard @@ -95,9 +97,11 @@ - [ ] Create settings panel - [x] Implement dark mode - [ ] Implement plugin matching disambiguation UI -- [ ] Add clickable version links to repository pages +- [x] Add clickable version links to repository pages - [ ] Add error recovery UI for failed updates - [ ] Add detailed progress logging in UI for debugging +- [x] Implement application update checking functionality +- [x] Display application update notifications ## Security Features (Upcoming) - [ ] Implement sandboxing for network requests diff --git a/UI_UX_Improvement_Task_List.md b/UI_UX_Improvement_Task_List.md new file mode 100644 index 0000000..d8b9e5a --- /dev/null +++ b/UI_UX_Improvement_Task_List.md @@ -0,0 +1,24 @@ +# UI/UX Improvement Task List + +## Visual Consistency and Aesthetics +- [ ] Ensure consistent use of colors for buttons, backgrounds, and text. +- [x] Use a consistent font size and weight for headings, subheadings, and body text. +- [x] Ensure consistent padding and margins for elements. + +## User Interaction +- [x] Add subtle hover effects to buttons and interactive elements. +- [x] Ensure the layout adapts well to different screen sizes. +- [x] Use spinners or progress bars to indicate loading states. + +## Accessibility +- [x] Ensure sufficient contrast between text and background. +- [x] Ensure all interactive elements are accessible via keyboard. +- [x] Use ARIA attributes to improve screen reader support. + +## Feedback and Notifications +- [x] Clearly display messages for successful actions or errors. +- [ ] Provide tooltips for buttons and icons. + +## Performance Optimization +- [ ] Implement lazy loading for components not immediately visible. +- [x] Use React.memo and useCallback to prevent unnecessary re-renders. \ No newline at end of file diff --git a/code-styling.mdc b/code-styling.mdc new file mode 100644 index 0000000..5634a59 --- /dev/null +++ b/code-styling.mdc @@ -0,0 +1,6 @@ +--- +description: +globs: +alwaysApply: false +--- + \ No newline at end of file diff --git a/index.html b/index.html index ff93803..57720d9 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,10 @@ - + - Tauri + React + Typescript + + PlugSnatcher - Minecraft Server Plugin Manager diff --git a/package-lock.json b/package-lock.json index 3d00051..14981c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,15 @@ "name": "plugsnatcher", "version": "0.1.0", "dependencies": { - "@tauri-apps/api": "^2.4.0", - "@tauri-apps/plugin-dialog": "^2.2.0", - "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.0.0", + "@tauri-apps/plugin-fs": "^2.0.0", + "@tauri-apps/plugin-shell": "^2.0.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { - "@tauri-apps/cli": "^2", + "@tauri-apps/cli": "^2.0.0", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", @@ -1303,10 +1304,19 @@ "@tauri-apps/api": "^2.0.0" } }, - "node_modules/@tauri-apps/plugin-opener": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.6.tgz", - "integrity": "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==", + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.2.0.tgz", + "integrity": "sha512-+08mApuONKI8/sCNEZ6AR8vf5vI9DXD4YfrQ9NQmhRxYKMLVhRW164vdW5BSLmMpuevftpQ2FVoL9EFkfG9Z+g==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.0.0" + } + }, + "node_modules/@tauri-apps/plugin-shell": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.2.0.tgz", + "integrity": "sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==", "license": "MIT OR Apache-2.0", "dependencies": { "@tauri-apps/api": "^2.0.0" diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index b21bd68..502406b 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -1,7 +1,4 @@ # Generated by Cargo # will have compiled files and executables /target/ - -# Generated by Tauri -# will have schema files for capabilities auto-completion /gen/schemas diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 88024ac..9baaa9f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -30,14 +30,13 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "cfg-if", + "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy 0.7.35", ] [[package]] @@ -64,18 +63,29 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f39be698127218cca460cb624878c9aa4e2b47dba3b277963d2bf00bad263b" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -91,6 +101,40 @@ version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +[[package]] +name = "app" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.21.7", + "cached", + "futures", + "log", + "regex", + "reqwest 0.11.27", + "semver", + "serde", + "serde_json", + "sha2", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-log", + "tauri-plugin-shell", + "tokio", + "urlencoding", + "walkdir", + "yaml-rust", + "zip", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ashpd" version = "0.11.0" @@ -115,106 +159,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener 5.4.0", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] -[[package]] -name = "async-channel" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" -dependencies = [ - "async-lock", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 0.38.44", - "slab", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener 5.4.0", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-mutex" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-process" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener 5.4.0", - "futures-lite", - "rustix 0.38.44", - "tracing", -] - [[package]] name = "async-recursion" version = "1.1.1" @@ -226,30 +176,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "async-signal" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 0.38.44", - "signal-hook-registry", - "slab", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.88" @@ -284,12 +210,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.4.0" @@ -344,6 +264,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -372,16 +304,26 @@ dependencies = [ ] [[package]] -name = "blocking" -version = "1.6.1" +name = "borsh" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] @@ -411,6 +353,39 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byte-unit" +version = "5.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.22.0" @@ -454,16 +429,15 @@ dependencies = [ [[package]] name = "cached" -version = "0.52.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8466736fe5dbcaf8b8ee24f9bbefe43c884dc3e9ff7178da70f55bffca1133c" +checksum = "b195e4fbc4b6862bbd065b991a34750399c119797efff72492f28a5864de8700" dependencies = [ - "ahash", "async-trait", "cached_proc_macro", "cached_proc_macro_types", "futures", - "hashbrown 0.14.5", + "hashbrown 0.13.2", "instant", "once_cell", "thiserror 1.0.69", @@ -472,14 +446,15 @@ dependencies = [ [[package]] name = "cached_proc_macro" -version = "0.22.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f32e012222055211b70f5b0601f951f84523410a0e65c81f2744a6042450d" +checksum = "b48814962d2fd604c50d2b9433c2a41a0ab567779ee2c02f7fba6eca1221f082" dependencies = [ - "darling", + "cached_proc_macro_types", + "darling 0.14.4", "proc-macro2", "quote", - "syn 2.0.100", + "syn 1.0.109", ] [[package]] @@ -669,6 +644,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -692,9 +677,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.0", - "core-foundation", + "core-foundation 0.10.0", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -705,7 +690,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.9.0", - "core-foundation", + "core-foundation 0.10.0", "libc", ] @@ -789,14 +774,38 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + [[package]] name = "darling" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", ] [[package]] @@ -809,17 +818,28 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.100", ] +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", "quote", "syn 2.0.100", ] @@ -978,7 +998,7 @@ dependencies = [ "rustc_version", "toml", "vswhom", - "winreg", + "winreg 0.52.0", ] [[package]] @@ -987,6 +1007,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[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" @@ -1014,6 +1043,16 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1040,12 +1079,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "5.4.0" @@ -1063,7 +1096,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.0", + "event-listener", "pin-project-lite", ] @@ -1082,6 +1115,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -1108,6 +1150,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1115,7 +1166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1129,6 +1180,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1144,6 +1201,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -1392,10 +1455,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -1405,11 +1466,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", - "wasm-bindgen", ] [[package]] @@ -1566,21 +1625,39 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.8.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" [[package]] name = "hashbrown" @@ -1600,12 +1677,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - [[package]] name = "hex" version = "0.4.3" @@ -1635,6 +1706,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.15", +] + [[package]] name = "http" version = "1.3.1" @@ -1646,6 +1728,17 @@ dependencies = [ "itoa 1.0.15", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1653,7 +1746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.3.1", ] [[package]] @@ -1664,8 +1757,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -1675,6 +1768,36 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa 1.0.15", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.6.0" @@ -1684,8 +1807,8 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "httparse", "itoa 1.0.15", "pin-project-lite", @@ -1695,35 +1818,31 @@ dependencies = [ ] [[package]] -name = "hyper-rustls" -version = "0.27.5" +name = "hyper-tls" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "futures-util", - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", + "bytes", + "hyper 0.14.32", + "native-tls", "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", + "tokio-native-tls", ] [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", - "hyper", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", + "libc", "pin-project-lite", "socket2", "tokio", @@ -1733,9 +1852,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.62" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1743,7 +1862,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.61.0", ] [[package]] @@ -2168,12 +2287,6 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.9.3" @@ -2201,6 +2314,9 @@ name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "value-bag", +] [[package]] name = "mac" @@ -2291,6 +2407,23 @@ 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" @@ -2382,6 +2515,15 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -2621,6 +2763,50 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl" +version = "0.10.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2637,6 +2823,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "pango" version = "0.18.3" @@ -2872,17 +3068,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -2902,33 +3087,6 @@ dependencies = [ "time", ] -[[package]] -name = "plugsnatcher" -version = "0.1.0" -dependencies = [ - "async-mutex", - "async-trait", - "base64 0.21.7", - "cached", - "futures", - "regex", - "reqwest", - "semver", - "serde", - "serde_json", - "sha2", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-opener", - "tokio", - "url", - "urlencoding", - "walkdir", - "yaml-rust", - "zip", -] - [[package]] name = "png" version = "0.17.16" @@ -2942,21 +3100,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "polling" -version = "3.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 0.38.44", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -2969,7 +3112,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.24", + "zerocopy", ] [[package]] @@ -3045,6 +3188,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quick-xml" version = "0.32.0" @@ -3054,60 +3217,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.12", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" -dependencies = [ - "bytes", - "getrandom 0.3.2", - "rand 0.9.0", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.12", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" version = "1.0.40" @@ -3123,6 +3232,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -3156,7 +3271,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.24", + "zerocopy", ] [[package]] @@ -3289,6 +3404,55 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.50.0", +] + [[package]] name = "reqwest" version = "0.12.15" @@ -3299,11 +3463,10 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.6.0", "hyper-util", "ipnet", "js-sys", @@ -3312,16 +3475,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", - "rustls-pemfile", - "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", - "tokio-rustls", "tokio-util", "tower", "tower-service", @@ -3330,7 +3488,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", "windows-registry", ] @@ -3360,17 +3517,48 @@ dependencies = [ ] [[package]] -name = "ring" -version = "0.17.14" +name = "rkyv" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.15", - "libc", - "untrusted", - "windows-sys 0.52.0", + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", ] [[package]] @@ -3379,12 +3567,6 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -3394,19 +3576,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.9.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.0.3" @@ -3416,51 +3585,17 @@ dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys 0.9.3", + "linux-raw-sys", "windows-sys 0.59.0", ] -[[package]] -name = "rustls" -version = "0.23.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - [[package]] name = "rustls-pemfile" -version = "2.2.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" -dependencies = [ - "web-time", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", + "base64 0.21.7", ] [[package]] @@ -3484,6 +3619,15 @@ 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 = "schemars" version = "0.8.22" @@ -3517,6 +3661,35 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "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 = "selectors" version = "0.22.0" @@ -3656,7 +3829,7 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.100", @@ -3716,6 +3889,16 @@ dependencies = [ "digest", ] +[[package]] +name = "shared_child" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3737,6 +3920,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "0.3.11" @@ -3766,9 +3955,9 @@ checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3783,7 +3972,7 @@ dependencies = [ "bytemuck", "cfg_aliases", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "js-sys", "log", "objc2 0.5.2", @@ -3859,6 +4048,12 @@ dependencies = [ "quote", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -3904,6 +4099,12 @@ 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 = "sync_wrapper" version = "1.0.2" @@ -3924,6 +4125,27 @@ dependencies = [ "syn 2.0.100", ] +[[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" @@ -3944,7 +4166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63c8b1020610b9138dd7b1e06cf259ae91aa05c30f3bd0d6b42a03997b92dec1" dependencies = [ "bitflags 2.9.0", - "core-foundation", + "core-foundation 0.10.0", "core-graphics", "crossbeam-channel", "dispatch", @@ -3987,6 +4209,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.12.16" @@ -4009,7 +4237,7 @@ dependencies = [ "glob", "gtk", "heck 0.5.0", - "http", + "http 1.3.1", "jni", "libc", "log", @@ -4021,7 +4249,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.12.15", "serde", "serde_json", "serde_repr", @@ -4125,9 +4353,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.2.0" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b59fd750551b1066744ab956a1cd6b1ea3e1b3763b0b9153ac27a044d596426" +checksum = "4307310e1d2c09ab110235834722e7c2b85099b683e1eb7342ab351b0be5ada3" dependencies = [ "log", "raw-window-handle", @@ -4137,15 +4365,15 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.12", + "thiserror 1.0.69", "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.2.0" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1edf18000f02903a7c2e5997fb89aca455ecbc0acc15c6535afbb883be223" +checksum = "96ba7d46e86db8c830d143ef90ab5a453328365b0cc834c24edea4267b16aba0" dependencies = [ "anyhow", "dunce", @@ -4157,33 +4385,52 @@ dependencies = [ "serde_repr", "tauri", "tauri-plugin", - "tauri-utils", - "thiserror 2.0.12", - "toml", + "thiserror 1.0.69", "url", "uuid", ] [[package]] -name = "tauri-plugin-opener" -version = "2.2.6" +name = "tauri-plugin-log" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fdc6cb608e04b7d2b6d1f21e9444ad49245f6d03465ba53323d692d1ceb1a30" +checksum = "2341d5b9bc5318c8e34f35a569140c78337241aa9c14091550b424c49f0314e0" dependencies = [ - "dunce", - "glob", - "objc2-app-kit", + "android_logger", + "byte-unit", + "fern", + "log", + "objc2 0.6.0", "objc2-foundation 0.3.0", - "open", - "schemars", "serde", "serde_json", + "serde_repr", + "swift-rs", "tauri", "tauri-plugin", "thiserror 2.0.12", - "url", - "windows", - "zbus", + "time", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad7880c5586b6b2104be451e3d7fc0f3800c84bda69e9ba81c828f87cb34267" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 1.0.69", + "tokio", ] [[package]] @@ -4195,7 +4442,7 @@ dependencies = [ "cookie", "dpi", "gtk", - "http", + "http 1.3.1", "jni", "raw-window-handle", "serde", @@ -4213,7 +4460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "087188020fd6facb8578fe9b38e81fa0fe5fb85744c73da51a299f94a530a1e3" dependencies = [ "gtk", - "http", + "http 1.3.1", "jni", "log", "objc2 0.6.0", @@ -4246,7 +4493,7 @@ dependencies = [ "dunce", "glob", "html5ever", - "http", + "http 1.3.1", "infer", "json-patch", "kuchikiki", @@ -4290,7 +4537,7 @@ dependencies = [ "fastrand", "getrandom 0.3.2", "once_cell", - "rustix 1.0.3", + "rustix", "windows-sys 0.59.0", ] @@ -4359,7 +4606,9 @@ checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa 1.0.15", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -4438,12 +4687,12 @@ dependencies = [ ] [[package]] -name = "tokio-rustls" -version = "0.26.2" +name = "tokio-native-tls" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ - "rustls", + "native-tls", "tokio", ] @@ -4525,7 +4774,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", @@ -4678,12 +4927,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.4" @@ -4726,6 +4969,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4742,6 +4991,18 @@ dependencies = [ "serde", ] +[[package]] +name = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" @@ -4908,16 +5169,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webkit2gtk" version = "2.0.1" @@ -4962,15 +5213,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "webpki-roots" -version = "0.26.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "webview2-com" version = "0.36.0" @@ -4981,7 +5223,7 @@ dependencies = [ "webview2-com-sys", "windows", "windows-core 0.60.1", - "windows-implement", + "windows-implement 0.59.0", "windows-interface", ] @@ -5075,26 +5317,30 @@ dependencies = [ "windows-core 0.60.1", ] -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" dependencies = [ - "windows-implement", + "windows-implement 0.59.0", "windows-interface", "windows-link", "windows-result", - "windows-strings", + "windows-strings 0.3.1", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.0", ] [[package]] @@ -5118,6 +5364,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "windows-interface" version = "0.59.1" @@ -5152,7 +5409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", - "windows-strings", + "windows-strings 0.3.1", "windows-targets 0.53.0", ] @@ -5174,6 +5431,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -5479,6 +5745,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.52.0" @@ -5525,7 +5801,7 @@ dependencies = [ "gdkx11", "gtk", "html5ever", - "http", + "http 1.3.1", "javascriptcore-rs", "jni", "kuchikiki", @@ -5554,6 +5830,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" @@ -5625,17 +5910,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" dependencies = [ "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-process", "async-recursion", - "async-task", "async-trait", - "blocking", "enumflags2", - "event-listener 5.4.0", + "event-listener", "futures-core", "futures-lite", "hex", @@ -5682,33 +5960,13 @@ dependencies = [ "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive 0.7.35", -] - [[package]] name = "zerocopy" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ - "zerocopy-derive 0.8.24", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "zerocopy-derive", ] [[package]] @@ -5743,12 +6001,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - [[package]] name = "zerovec" version = "0.10.4" diff --git a/src-tauri/build.rs b/src-tauri/build.rs index d860e1e..795b9b7 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build() } diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 6be5e50..77e7d23 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index e81bece..0f7976f 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index a437dd5..98fda06 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 0ca4f27..f35d84f 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index b81f820..1823bb2 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index 624c7bf..dc2b22c 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index c021d2b..0ed3984 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index 6219700..60bf0ea 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index f9bc048..c8ca0ad 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index d5fbfb2..8756459 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index 63440d7..2c8023c 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index f3f705a..2c5e603 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index 4556388..17d142c 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 12a5bce..a2993ad 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index b3636e4..06c23c8 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index e1cd261..d1756ce 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/schema.json b/src-tauri/schema.json new file mode 100644 index 0000000..e69de29 diff --git a/src/App.css b/src/App.css index ff427fa..6fab12c 100644 --- a/src/App.css +++ b/src/App.css @@ -11,18 +11,29 @@ --background-color: #202124; --surface-color: #292a2d; --text-color: #e8eaed; - --text-secondary-color: #9aa0a6; + --text-secondary-color: #a8adb4; --border-color: #3c4043; --error-color: #f44336; --warning-color: #ff9800; --success-color: #4caf50; + + /* Hover/Active Colors */ + --primary-hover: color-mix(in srgb, var(--primary-color) 90%, black); + --secondary-hover: color-mix(in srgb, var(--secondary-color) 90%, black); + --danger-hover: color-mix(in srgb, var(--error-color) 90%, black); + --success-hover: color-mix(in srgb, var(--success-color) 90%, black); + --warning-hover: color-mix(in srgb, var(--warning-color) 90%, black); + --hover-bg: rgba(255, 255, 255, 0.05); /* For outline/text hover */ + --text-hover-bg: rgba(26, 115, 232, 0.1); /* Primary text hover */ + + --spacing-unit: 8px; font-family: Inter, Avenir, Helvetica, Arial, sans-serif; font-size: 16px; - line-height: 24px; + line-height: 1.5; font-weight: 400; - color: #0f0f0f; - background-color: #f6f6f6; + color: var(--text-color); + background-color: var(--background-color); font-synthesis: none; text-rendering: optimizeLegibility; @@ -38,7 +49,7 @@ } body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; background-color: var(--background-color); color: var(--text-color); } @@ -51,45 +62,58 @@ body { .app-header { background-color: var(--surface-color); - padding: 1rem; + padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 3); text-align: center; border-bottom: 1px solid var(--border-color); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .app-header h1 { - font-size: 2rem; - margin-bottom: 0.5rem; + font-size: 1.8rem; + margin-bottom: var(--spacing-unit); + font-weight: 700; +} + +.app-subtitle { + color: var(--text-secondary-color); + font-size: 1rem; } .app-content { flex: 1; - padding: 1rem; + padding: calc(var(--spacing-unit) * 3); max-width: 1200px; margin: 0 auto; width: 100%; } +.server-section { + margin-bottom: calc(var(--spacing-unit) * 3); +} + .server-selector { background-color: var(--surface-color); - padding: 1.5rem; + padding: calc(var(--spacing-unit) * 3); border-radius: 8px; - margin-bottom: 1.5rem; + margin-bottom: calc(var(--spacing-unit) * 3); border: 1px solid var(--border-color); } .server-selector h2 { - margin-bottom: 1rem; + margin-bottom: calc(var(--spacing-unit) * 2); font-size: 1.5rem; + font-weight: 600; } .input-group { display: flex; - margin-bottom: 1rem; + margin-bottom: var(--spacing-unit); + gap: 0; } .input-group input { flex: 1; - padding: 0.75rem; + padding: calc(var(--spacing-unit) * 1.5); border: 1px solid var(--border-color); background-color: rgba(255, 255, 255, 0.05); color: var(--text-color); @@ -98,39 +122,145 @@ body { } .input-group button { - padding: 0.75rem 1.5rem; - background-color: var(--primary-color); - color: white; - border: none; - border-radius: 0 4px 4px 0; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.3s; -} - -.input-group button:hover { - background-color: #1967d2; -} - -.scan-button { - padding: 0.75rem 1.5rem; - background-color: var(--secondary-color); + padding: calc(var(--spacing-unit) * 1.5); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; + font-weight: 600; + transition: background-color 0.3s, opacity 0.3s; + text-align: center; + line-height: 1.2; + background-color: var(--primary-color); +} + +.input-group button:hover { + opacity: 0.9; + background-color: color-mix(in srgb, var(--primary-color) 90%, black); +} + +.scan-button, +.update-button, +.info-button, +.check-button, +.continue-button, +.cancel-button, +.close-modal-button, +.plugin-page-button { + padding: calc(var(--spacing-unit) * 1.5); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + font-weight: 600; + transition: background-color 0.3s, opacity 0.3s; + text-align: center; + line-height: 1.2; +} + +.scan-button:hover, +.update-button:hover, +.info-button:hover, +.check-button:hover, +.continue-button:hover, +.cancel-button:hover, +.close-modal-button:hover, +.plugin-page-button:hover { + opacity: 0.9; +} + +.scan-button { + background-color: var(--secondary-color); width: 100%; - transition: background-color 0.3s; } .scan-button:hover { - background-color: #43a047; + background-color: color-mix(in srgb, var(--secondary-color) 90%, black); } .scan-button:disabled { - background-color: #666; + background-color: var(--text-secondary-color); cursor: not-allowed; + opacity: 0.7; +} + +.plugins-section { + background-color: var(--surface-color); + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--border-color); + margin-bottom: calc(var(--spacing-unit) * 3); +} + +.plugins-header-container { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 3); + border-bottom: 1px solid var(--border-color); + background-color: rgba(255, 255, 255, 0.03); +} + +.plugins-header-container h2 { + margin: 0; + font-size: 1.5rem; + font-weight: 600; + margin-right: 1rem; +} + +.plugins-header-container > *:not(h2) { + margin-top: 0.5rem; +} + +.plugin-grid-header { + display: grid; + grid-template-columns: 3fr 1fr 1.5fr 1.5fr; + padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 3); + font-weight: 600; + font-size: 0.9rem; + color: var(--text-secondary-color); + border-bottom: 1px solid var(--border-color); + background-color: rgba(255, 255, 255, 0.02); +} + +.plugin-grid-header > div:nth-child(4) { + text-align: right; + justify-self: end; +} + +.plugin-grid-header > div { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.plugin-list-container { + padding: 0; + border: none; + border-radius: 0; + background-color: transparent; +} + +.plugin-list-container .search-container { + padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 2.5); + border-bottom: 1px solid var(--border-color); + background-color: rgba(255, 255, 255, 0.01); +} + +.plugin-list-controls { + display: flex; + justify-content: space-between; + align-items: center; + padding: calc(var(--spacing-unit)) calc(var(--spacing-unit) * 2.5); + border-bottom: 1px solid var(--border-color); + font-size: 0.9rem; +} + +.plugin-list { + padding: 0; } .plugins-list { @@ -157,34 +287,33 @@ body { align-items: center; } +.plugin-info { + flex: 1; + overflow: hidden; +} + .plugin-version { color: var(--text-secondary-color); font-size: 0.9rem; + display: flex; + align-items: center; + gap: 0.4rem; } .update-available { - background-color: #4caf50; + background-color: var(--success-color); color: white; - padding: 4px 8px; + padding: calc(var(--spacing-unit) * 0.5) var(--spacing-unit); border-radius: 4px; font-size: 0.75rem; font-weight: bold; + margin-left: var(--spacing-unit); } .plugins-count { margin-top: 0.5rem; } -.plugins-header { - display: grid; - grid-template-columns: 2fr 1fr 1fr 1fr; - padding: 0.75rem 1rem; - background-color: rgba(255, 255, 255, 0.05); - border-radius: 4px 4px 0 0; - font-weight: bold; - border-bottom: 1px solid var(--border-color); -} - .plugin-item { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; @@ -202,45 +331,42 @@ body { } .plugin-actions { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 0.5rem; +} + +.plugin-actions > div { display: flex; gap: 0.5rem; } .update-button { - padding: 0.5rem 1rem; background-color: var(--warning-color); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.3s; } .update-button:hover { - background-color: #f57c00; + background-color: color-mix(in srgb, var(--warning-color) 90%, black); } .info-button { - padding: 0.5rem 1rem; background-color: var(--primary-color); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.3s; } .info-button:hover { - background-color: #1967d2; + background-color: color-mix(in srgb, var(--primary-color) 90%, black); } .app-footer { background-color: var(--surface-color); - padding: 1rem; + padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 3); text-align: center; border-top: 1px solid var(--border-color); - margin-top: auto; + margin-top: calc(var(--spacing-unit) * 4); color: var(--text-secondary-color); + font-size: 0.9rem; } .container { @@ -437,9 +563,8 @@ button { .server-info { background-color: var(--surface-color); - padding: 1.5rem; + padding: calc(var(--spacing-unit) * 2.5); border-radius: 8px; - margin-bottom: 1.5rem; border: 1px solid var(--border-color); } @@ -558,7 +683,7 @@ button { } .update-available-badge { - background-color: var(--secondary-color); + background-color: var(--warning-color); color: white; padding: 0.25rem 0.5rem; border-radius: 4px; @@ -566,22 +691,58 @@ button { margin-left: 0.5rem; } -.up-to-date-badge { - color: var(--success-color, #4caf50); - font-weight: bold; - margin-left: 0.5rem; +.up-to-date-text { + color: var(--success-color); + display: inline-flex; + align-items: center; + font-weight: 500; } -.up-to-date-text { - color: var(--success-color, #4caf50); - font-size: 0.85em; - margin-left: 0.4rem; +.up-to-date-badge a, +.update-available-badge a { + text-decoration: underline; + color: inherit; +} + +.version-link { + color: var(--link-color, #3498db); + text-decoration: underline; + transition: color 0.2s; + font-weight: 500; + position: relative; + display: inline-flex; + align-items: center; +} + +.version-link.up-to-date-text { + color: var(--success-color); +} + +.version-link:hover { + color: var(--highlight-color, #2980b9); + text-decoration: underline; +} + +.version-link:after { + content: '↗'; + font-size: 0.8em; + margin-left: 0.3em; + opacity: 0.7; +} + +/* Improve platform badges layout */ +.platform-badges { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + margin: 0.4rem 0; } .bulk-update-progress { - margin-top: 0.5rem; - font-size: 0.9rem; - color: var(--text-secondary-color); + margin-top: 1rem; + background-color: var(--surface-color); + padding: 1rem; + border-radius: 4px; } .bulk-update-progress progress { @@ -716,3 +877,448 @@ button { .close-modal-button:hover { background-color: var(--surface-hover-color, #616161); } + +/* Platform compatibility styles */ +.plugin-platform-compatibility { + margin-bottom: 1.5rem; +} + +.platform-badges { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.platform-badge { + display: inline-flex; + align-items: center; + justify-content: center; + color: white; + padding: 0.3rem 0.7rem; + border-radius: 12px; + font-size: 0.85rem; + font-weight: 600; + margin: 0.15rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); + text-shadow: 0 1px 1px rgba(0,0,0,0.2); +} + +.platform-compatibility-indicator { + display: inline-flex; + margin-left: 0.5rem; + vertical-align: middle; +} + +.compatible-badge { + display: inline-flex; + align-items: center; + justify-content: center; + background-color: var(--success-color); + color: white; + width: 18px; + height: 18px; + border-radius: 50%; + font-size: 0.7rem; + font-weight: bold; +} + +.caution-badge { + display: inline-flex; + align-items: center; + justify-content: center; + background-color: var(--warning-color); + color: white; + width: 18px; + height: 18px; + border-radius: 50%; + font-size: 0.7rem; + font-weight: bold; +} + +/* Improve warning modal for update notifications */ +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.warning-modal { + background-color: var(--surface-color); + border-radius: 8px; + padding: 1.5rem; + max-width: 500px; + width: 90%; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + border: 1px solid var(--warning-color); +} + +.warning-modal h3 { + color: var(--warning-color); + margin-bottom: 1rem; + display: flex; + align-items: center; +} + +.warning-modal h3::before { + content: '⚠️'; + margin-right: 0.5rem; +} + +.warning-modal p { + margin-bottom: 1.5rem; + line-height: 1.5; +} + +.warning-modal button { + padding: 0.5rem 1rem; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; + float: right; +} + +.warning-modal button:hover { + background-color: #1967d2; +} + +/* Download Progress Styles */ +.download-progress-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; +} + +.download-progress-modal { + background-color: var(--surface-color); + border-radius: 8px; + padding: 1.5rem; + width: 90%; + max-width: 450px; + text-align: center; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.download-progress-modal h3 { + margin-bottom: 1rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; +} + +.download-progress-modal p { + margin-bottom: 1.2rem; + color: var(--text-color); +} + +.progress-bar-container { + width: 100%; + height: 8px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 4px; + overflow: hidden; + margin-bottom: 0.5rem; +} + +.progress-bar { + height: 100%; + background-color: var(--primary-color); + border-radius: 4px; + transition: width 0.3s ease; +} + +.progress-percentage { + font-size: 0.9rem; + color: var(--text-secondary-color); + margin-bottom: 0.5rem; +} + +/* Custom animation for when download is completed */ +@keyframes fadeOut { + 0% { opacity: 1; } + 90% { opacity: 1; } + 100% { opacity: 0; } +} + +.download-progress-modal.completed { + animation: fadeOut 2s forwards; +} + +/* Platform badge styles with improved colors */ +.platform-badge { + display: inline-flex; + align-items: center; + justify-content: center; + color: white; + padding: 0.3rem 0.7rem; + border-radius: 12px; + font-size: 0.85rem; + font-weight: 600; + margin: 0.15rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); + text-shadow: 0 1px 1px rgba(0,0,0,0.2); +} + +/* Enhanced server-specific colors */ +.platform-badge[data-server-type="Paper"] { + background-color: #3498db; +} + +.platform-badge[data-server-type="Spigot"] { + background-color: #e67e22; +} + +.platform-badge[data-server-type="Bukkit"] { + background-color: #2ecc71; +} + +.platform-badge[data-server-type="Forge"] { + background-color: #9b59b6; +} + +.platform-badge[data-server-type="NeoForge"] { + background-color: #8e44ad; +} + +.platform-badge[data-server-type="Fabric"] { + background-color: #1abc9c; +} + +.platform-badge[data-server-type="Velocity"] { + background-color: #f1c40f; +} + +.platform-badge[data-server-type="BungeeCord"] { + background-color: #e74c3c; +} + +.platform-badge[data-server-type="Waterfall"] { + background-color: #3498db; +} + +/* Compatibility Check Dialog */ +.compatibility-dialog { + background-color: var(--surface-color); + border-radius: 8px; + padding: 1.5rem; + width: 90%; + max-width: 500px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.compatibility-dialog h3 { + margin-bottom: 1rem; + text-align: center; + font-size: 1.3rem; + color: var(--text-color); +} + +.plugin-info-section { + background-color: rgba(255, 255, 255, 0.05); + padding: 0.8rem; + border-radius: 4px; + margin-bottom: 1.2rem; + text-align: center; +} + +.compatibility-info { + margin-bottom: 1.5rem; +} + +.compatibility-status { + display: inline-block; + padding: 0.3rem 0.6rem; + border-radius: 4px; + margin-right: 0.5rem; + font-weight: bold; +} + +.compatibility-status.compatible { + background-color: var(--success-color); + color: white; +} + +.compatibility-status.caution { + background-color: var(--warning-color); + color: white; +} + +.compatibility-status.unknown { + background-color: var(--text-secondary-color); + color: white; +} + +.compatibility-warning { + margin-top: 0.8rem; + padding: 0.8rem; + background-color: rgba(255, 152, 0, 0.1); + border-left: 3px solid var(--warning-color); + border-radius: 0 4px 4px 0; +} + +.compatibility-actions { + display: flex; + justify-content: flex-end; + gap: 1rem; + margin-top: 1.5rem; +} + +.continue-button { + padding: 0.6rem 1.2rem; + background-color: var(--success-color); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.continue-button:hover { + background-color: #3d9140; +} + +.continue-button.caution-button { + background-color: var(--warning-color); +} + +.continue-button.caution-button:hover { + background-color: #e68a00; +} + +.cancel-button { + padding: 0.6rem 1.2rem; + background-color: var(--surface-alt-color, #424242); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.cancel-button:hover { + background-color: var(--surface-hover-color, #616161); +} + +/* Flexible match warning in plugin details */ +.flexible-match-warning { + background-color: rgba(255, 152, 0, 0.1); + border: 1px solid var(--warning-color); + border-radius: 4px; + padding: 0.75rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + font-size: 0.9rem; + line-height: 1.4; +} + +.warning-icon { + margin-right: 0.5rem; + font-size: 1.1rem; +} + +/* Flexible match indicator in plugin list */ +.flexible-match-indicator { + color: var(--warning-color); + font-weight: bold; +} + +.check-button { + padding: 0.5rem 1rem; + background-color: transparent; + border: 1px solid var(--primary-color); + color: var(--primary-color); + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s; +} + +.check-button:hover { + background-color: rgba(26, 115, 232, 0.1); +} + +.check-button:disabled { + background-color: #666; + cursor: not-allowed; +} + +/* Make platform badges more visible */ +.platform-badges { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + margin: 0.4rem 0; +} + +.platform-badge { + display: inline-flex; + align-items: center; + justify-content: center; + color: white; + padding: 0.3rem 0.7rem; + border-radius: 12px; + font-size: 0.85rem; + font-weight: 600; + margin: 0.15rem 0; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); + text-shadow: 0 1px 1px rgba(0,0,0,0.2); +} + +/* Make sure plugin actions has the right display */ +.plugin-actions { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 0.5rem; +} + +@media (max-width: 900px) { + .app-header { + padding: var(--spacing-unit) calc(var(--spacing-unit) * 2); + } + .app-header h1 { + font-size: 1.6rem; + } + .app-subtitle { + font-size: 0.9rem; + } + .app-content { + padding: calc(var(--spacing-unit) * 2); + } + .server-section { + margin-bottom: calc(var(--spacing-unit) * 2); + } + .plugin-grid-header { + display: none; + } + .plugin-item { + grid-template-columns: 1fr; + gap: var(--spacing-unit); + padding: var(--spacing-unit) * 2; + } + .plugin-version, .plugin-compatibility, .plugin-actions { + justify-content: flex-start; + } + .plugin-details-content { + padding: calc(var(--spacing-unit) * 2); + } +} diff --git a/src/App.tsx b/src/App.tsx index 5509a73..30f7f09 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,969 +1,209 @@ -import { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; -import { open } from "@tauri-apps/plugin-dialog"; -import { listen, UnlistenFn } from "@tauri-apps/api/event"; -import { appDataDir } from '@tauri-apps/api/path'; // Import for data directory (if needed frontend side) -import "./App.css"; +import React, { useEffect } from 'react'; +import './App.css'; -type ServerType = - | 'Paper' - | 'Spigot' - | 'Bukkit' - | 'Vanilla' - | 'Forge' - | 'Fabric' - | 'Velocity' - | 'BungeeCord' - | 'Waterfall' - | 'Unknown'; +// Import context providers +import { ServerProvider } from './context/ServerContext/ServerContext'; +import { PluginProvider } from './context/PluginContext/PluginContext'; +import UIProvider from './context/UIContext/UIContext'; +import { useUIContext } from './context/UIContext/useUIContext'; -interface ServerInfo { - server_type: ServerType; - minecraft_version?: string; - plugins_directory: string; - plugins_count: number; -} +// Import layout components +import Footer from './components/layout/Footer/Footer'; +import MainContent from './components/layout/MainContent/MainContent'; -interface Plugin { - name: string; - version: string; - latest_version?: string; - description?: string; - authors: string[]; - has_update: boolean; - api_version?: string; - main_class?: string; - depend?: string[] | null; - soft_depend?: string[] | null; - load_before?: string[] | null; - commands?: any; - permissions?: any; - file_path: string; - file_hash: string; - website?: string; - changelog?: string; - repository_source?: string; // Add repository source (string for simplicity) - repository_id?: string; // Add repository ID - repository_url?: string; // URL to the plugin's repository page -} +// Import server components +import ServerSelector from './components/server/ServerSelector/ServerSelector'; +import ServerInfo from './components/server/ServerInfo/ServerInfo'; +import ScanProgress from './components/server/ScanProgress/ScanProgress'; -interface ScanResult { - server_info: ServerInfo; - plugins: Plugin[]; -} +// Import plugin components +import PluginList from './components/plugins/PluginList/PluginList'; +import PluginDetails from './components/plugins/PluginDetails/PluginDetails'; +import NoPluginsMessage from './components/plugins/NoPluginsMessage/NoPluginsMessage'; -interface ScanProgress { - processed: number; - total: number; - current_file: string; -} +// Import update components +import UpdateControls from './components/updates/UpdateControls/UpdateControls'; +import BulkUpdateProgress from './components/updates/BulkUpdateProgress/BulkUpdateProgress'; +import CompatibilityCheckDialog from './components/updates/CompatibilityCheckDialog/CompatibilityCheckDialog'; +import PremiumPluginModal from './components/updates/PremiumPluginModal/PremiumPluginModal'; +import DownloadProgressIndicator from './components/updates/DownloadProgressIndicator/DownloadProgressIndicator'; +import PluginMatchSelector from './components/updates/PluginMatchSelector/PluginMatchSelector'; +import WarningModal from './components/updates/WarningModal/WarningModal'; -// --- New Interfaces for Update Events --- -interface BulkUpdateProgressPayload { - processed: number; - total: number; - current_plugin_name: string; -} +// Import common components +import NotificationDisplay from './components/common/NotificationDisplay/NotificationDisplay'; -interface SingleUpdateResultPayload { - original_file_path: string; - plugin: Plugin | null; // Updated plugin state or null if check failed but wasn't a panic - error: string | null; // Error message if any -} - -// Interface for potential plugin matches for ambiguous plugins -interface PotentialPluginMatch { - name: string; - version: string; - repository: string; - repository_id: string; - page_url: string; - description?: string; - minecraft_versions: string[]; - download_count?: number; -} - -// --- End New Interfaces --- - -interface PluginDetailsProps { - plugin: Plugin; - onClose: () => void; -} - -// Get server type icon -function getServerTypeIcon(serverType: ServerType): string { - switch (serverType) { - case 'Paper': - return '📄'; - case 'Spigot': - return '🔌'; - case 'Bukkit': - return '🪣'; - case 'Vanilla': - return '🧊'; - case 'Forge': - return '🔨'; - case 'Fabric': - return '🧵'; - case 'Velocity': - return '⚡'; - case 'BungeeCord': - return '🔗'; - case 'Waterfall': - return '🌊'; - default: - return '❓'; - } -} - -// Get a formatted server type name for display -function getServerTypeName(serverType: ServerType): string { - return serverType === 'Unknown' ? 'Unknown Server' : serverType; -} - -function PluginDetails({ plugin, onClose }: PluginDetailsProps) { - return ( -
-
- -

{plugin.name}

-
Version: {plugin.version} {plugin.latest_version && ( - plugin.has_update - ? (Update available: {plugin.latest_version}) - : (Up to Date: {plugin.latest_version}) - )} -
- - {plugin.description && ( -
{plugin.description}
- )} - - {plugin.website && ( -
-
Website:
- - {plugin.website} - -
- )} - - {plugin.authors && plugin.authors.length > 0 && ( -
-
Authors:
-
{plugin.authors.join(", ")}
-
- )} - - {plugin.depend && plugin.depend.length > 0 && ( -
-
Dependencies:
-
{plugin.depend.join(", ")}
-
- )} - - {plugin.soft_depend && plugin.soft_depend.length > 0 && ( -
-
Soft Dependencies:
-
{plugin.soft_depend.join(", ")}
-
- )} - - {plugin.changelog && plugin.has_update && ( -
-
Changelog:
-
{plugin.changelog}
-
- )} - -
-
File Path:
-
{plugin.file_path}
- -
File Hash (SHA-256):
-
{plugin.file_hash}
-
- - {plugin.has_update && plugin.latest_version && ( -
- -
- )} -
-
- ); -} - -// Add this component after the PluginDetails component and before the App component -function ServerInfoDisplay({ serverInfo }: { serverInfo: ServerInfo | null }) { - if (!serverInfo) return null; - - return ( -
-

Server Information

-
- {getServerTypeIcon(serverInfo.server_type)} - {getServerTypeName(serverInfo.server_type)} -
-
- Minecraft Version - {serverInfo.minecraft_version || "Unknown"} -
-
- Plugins Directory - {serverInfo.plugins_directory || "Unknown"} -
-
- {serverInfo.plugins_count} plugins found -
-
- ); -} - -// Add this new component after PluginDetails -function WarningModal({ message, onClose }: { message: string, onClose: () => void }) { - return ( -
-
-

⚠️ Warning

-

{message}

- -
-
- ); -} - -// Component for selecting the correct plugin from potential matches -function PluginMatchSelector({ - plugin, - potentialMatches, - onSelect, - onCancel -}: { - plugin: Plugin, - potentialMatches: PotentialPluginMatch[], - onSelect: (match: PotentialPluginMatch) => void, - onCancel: () => void -}) { - return ( -
-
-

Multiple Matches Found

-

We found several potential matches for {plugin.name}. Please select the correct one:

- -
- {potentialMatches.map((match, index) => ( -
-
-

{match.name}

-

Version: {match.version}

- {match.description &&

{match.description}

} -
- Source: {match.repository} - {match.download_count && Downloads: {match.download_count.toLocaleString()}} - MC: {match.minecraft_versions.join(', ')} -
-
-
- - - View Page - -
-
- ))} -
- -
- -
-
-
- ); -} - -// After the WarningModal component, add this new component -function PremiumPluginModal({ - pluginName, - pluginVersion, - resourceUrl, - onClose -}: { - pluginName: string; - pluginVersion: string; - resourceUrl: string; - onClose: () => void; -}) { - return ( -
-
-

Premium Plugin Detected

-

- {pluginName} {pluginVersion} appears to be a premium or protected plugin - that requires manual download. -

-
-

To update this plugin:

-
    -
  1. Visit the plugin page on SpigotMC
  2. -
  3. Log in to your SpigotMC account
  4. -
  5. Download the latest version manually
  6. -
  7. Replace the current plugin file in your server's plugins folder
  8. -
-
-
- - Open Plugin Page - - -
-
-
- ); -} +// Import hooks +import { useServerContext } from './context/ServerContext/useServerContext'; +import { usePluginContext } from './context/PluginContext/usePluginContext'; +import { usePluginActions } from './hooks/usePluginActions'; +/** + * The main application component that serves as the entry point. + * This component is responsible for setting up the context providers + * and rendering the main application structure. + */ function App() { - const [serverPath, setServerPath] = useState(""); - const [serverInfo, setServerInfo] = useState(null); - const [plugins, setPlugins] = useState([]); - const [isScanning, setIsScanning] = useState(false); - const [scanComplete, setScanComplete] = useState(false); - const [error, setError] = useState(null); - const [selectedPlugin, setSelectedPlugin] = useState(null); - const [scanProgress, setScanProgress] = useState(null); - const [isCheckingUpdates, setIsCheckingUpdates] = useState(false); - const [updateError, setUpdateError] = useState(null); - // --- New State Variables --- - const [pluginLoadingStates, setPluginLoadingStates] = useState>({}); - const [bulkUpdateProgress, setBulkUpdateProgress] = useState(null); - const [warningMessage, setWarningMessage] = useState(null); - const [potentialMatches, setPotentialMatches] = useState([]); - // --- New state for match selector --- - const [showMatchSelector, setShowMatchSelector] = useState(false); - const [pluginToDisambiguate, setPluginToDisambiguate] = useState(null); - // --- End New state for match selector --- - // --- End New State Variables --- - const [serverType, setServerType] = useState('Unknown'); - const [premiumPluginInfo, setPremiumPluginInfo] = useState<{name: string; version: string; url: string} | null>(null); + console.log("App Component Initialized"); + + return ( + + + + ); +} + +/** + * Wrapper to ensure PluginProvider has access to ServerContext + */ +function PluginContextWrapper() { + const { serverPath, serverInfo } = useServerContext(); useEffect(() => { - let unlistenScanStarted: UnlistenFn | undefined; - let unlistenScanProgress: UnlistenFn | undefined; - let unlistenScanCompleted: UnlistenFn | undefined; - let unlistenScanError: UnlistenFn | undefined; - let unlistenBulkUpdateStart: UnlistenFn | undefined; - let unlistenUpdateCheckProgress: UnlistenFn | undefined; - let unlistenSingleUpdateCheckStarted: UnlistenFn | undefined; - let unlistenSingleUpdateCheckCompleted: UnlistenFn | undefined; + console.log("PluginContextWrapper: serverPath =", serverPath); + }, [serverPath]); - const setupListeners = async () => { - unlistenScanStarted = await listen("scan_started", () => { - console.log("Scan started event received"); - setIsScanning(true); - setScanProgress(null); - setError(null); - }); + return ( + + + + + + ); +} - unlistenScanProgress = await listen("scan_progress", (event) => { - console.log("Scan progress event received:", event.payload); - setScanProgress(event.payload); - }); +/** + * The main application content that uses context hooks. + * This is separate from App to ensure the contexts are available. + */ +function AppContent() { + const { + serverInfo, + serverPath, + isScanning, + scanProgress, + scanComplete + } = useServerContext(); - unlistenScanCompleted = await listen("scan_completed", (event) => { - console.log("Scan completed event received with payload:", event.payload); - try { - console.log("Server info:", event.payload.server_info); - console.log("Plugins count:", event.payload.plugins.length); + const { + plugins, + selectedPlugin, + showPluginDetails, + closePluginDetails, + isCheckingUpdates, + bulkUpdateProgress + } = usePluginContext(); - // Update state in a specific order to ensure UI updates properly - setIsScanning(false); - setScanComplete(true); - setServerInfo(event.payload.server_info); - setPlugins(event.payload.plugins); - setServerType(event.payload.server_info.server_type); + const { + warningMessage, + clearWarningMessage, + downloadProgress, + premiumPluginInfo, + clearPremiumPluginInfo + } = useUIContext(); - // Add a slight delay and verify the state was updated - setTimeout(() => { - console.log("Verifying state updates after scan:"); - console.log("- scanComplete:", scanComplete); - console.log("- serverInfo:", serverInfo); - console.log("- plugins count:", plugins.length); - - // Force a state update if plugins length is still 0 but we got plugins - if (plugins.length === 0 && event.payload.plugins.length > 0) { - console.log("Forcing state update because plugins array is empty"); - setPlugins([...event.payload.plugins]); - } - }, 100); - - console.log("State updated after scan completion"); - } catch (err) { - console.error("Error handling scan completion:", err); - setError(`Error handling scan completion: ${err}`); - setIsScanning(false); - } - }); - - unlistenScanError = await listen("scan_error", (event) => { - console.log("Scan error event received:", event.payload); - setIsScanning(false); - setError(event.payload); - }); - - unlistenBulkUpdateStart = await listen("bulk_update_start", (event) => { - console.log("Bulk update start event received, total plugins:", event.payload); - setIsCheckingUpdates(true); - setUpdateError(null); - setBulkUpdateProgress({ - processed: 0, - total: event.payload, - current_plugin_name: "Starting update check..." - }); - }); - - unlistenUpdateCheckProgress = await listen("update_check_progress", (event) => { - console.log("Update check progress event received:", event.payload); - setBulkUpdateProgress(event.payload); - }); - - unlistenSingleUpdateCheckStarted = await listen("single_update_check_started", (event) => { - console.log("Single update check started for:", event.payload); - }); - - unlistenSingleUpdateCheckCompleted = await listen("single_update_check_completed", (event) => { - console.log("Single update check completed, result:", event.payload); - const { original_file_path, plugin, error } = event.payload; - - setPluginLoadingStates(prev => ({ ...prev, [original_file_path]: false })); - - if (error) { - setUpdateError(`Error checking for updates: ${error}`); - return; - } - - if (plugin) { - setPlugins(prevPlugins => prevPlugins.map(p => { - if (p.file_path === original_file_path) { - return plugin; - } - return p; - })); - - if (serverPath) { - invoke("save_plugin_data", { - plugins: plugins.map(p => p.file_path === original_file_path ? plugin : p), - serverPath - }).catch(err => { - console.error("Error saving plugin data after single update:", err); - }); - } - } - }); - - window.addEventListener('update-plugin', ((e: CustomEvent) => { - if (e.detail) { - updatePlugin(e.detail); - } - }) as EventListener); - }; - - setupListeners(); - - return () => { - unlistenScanStarted?.(); - unlistenScanProgress?.(); - unlistenScanCompleted?.(); - unlistenScanError?.(); - unlistenBulkUpdateStart?.(); - unlistenUpdateCheckProgress?.(); - unlistenSingleUpdateCheckStarted?.(); - unlistenSingleUpdateCheckCompleted?.(); - window.removeEventListener('update-plugin', (() => {}) as EventListener); - }; - }, []); - - async function selectDirectory() { - try { - const selected = await open({ - directory: true, - multiple: false, - title: "Select Minecraft Server Folder", - }); - - if (selected && typeof selected === "string") { - console.log(`Directory selected: ${selected}`); - setServerPath(selected); - setServerInfo(null); - setPlugins([]); - setIsScanning(false); - setScanComplete(false); - setError(null); - setScanProgress(null); - setIsCheckingUpdates(false); - setUpdateError(null); - setPluginLoadingStates({}); - setBulkUpdateProgress(null); - - try { - console.log(`Attempting to load persisted data for: ${selected}`); - const loadedPlugins: Plugin[] = await invoke("load_plugin_data", { serverPath: selected }); - if (loadedPlugins && loadedPlugins.length > 0) { - console.log(`Loaded ${loadedPlugins.length} plugins from persistence.`); - setPlugins(loadedPlugins); - setScanComplete(true); - setError(null); - } else { - console.log("No persisted plugin data found for this server."); - } - } catch (loadError) { - console.error("Error loading persisted plugin data:", loadError); - setError(`Failed to load previous plugin data: ${loadError}`); - } - } else { - console.log("Directory selection cancelled."); - } - } catch (err) { - console.error("Error selecting directory:", err); - setError(`Error selecting directory: ${err}`); - } - } - - async function scanForPlugins() { - if (!serverPath || isScanning) return; - - try { - console.log("Starting scan for plugins in:", serverPath); - setIsScanning(true); - setScanComplete(false); - setPlugins([]); - setServerInfo(null); - setScanProgress(null); - setError(null); - - await invoke("scan_server_dir", { path: serverPath }); - console.log("Scan server dir command invoked successfully"); - - } catch (err) { - console.error("Error invoking scan command:", err); - setError(`Failed to start scan: ${err as string}`); - setIsScanning(false); - } - } - - async function checkForUpdates() { - if (!plugins.length || isCheckingUpdates) return; - - setIsCheckingUpdates(true); - setUpdateError(null); - setBulkUpdateProgress(null); - console.log("Invoking bulk check_plugin_updates..."); - - try { - const repositoriesToCheck = ['SpigotMC', 'Modrinth', 'GitHub']; - - const pluginsToSend = plugins.map(p => ({ - name: p.name, - version: p.version, - authors: p.authors || [], - file_path: p.file_path, - file_hash: p.file_hash, - website: p.website, - description: p.description, - api_version: p.api_version, - main_class: p.main_class, - depend: p.depend, - soft_depend: p.soft_depend, - load_before: p.load_before, - commands: p.commands, - permissions: p.permissions, - has_update: p.has_update || false, - repository_source: p.repository_source, - repository_id: p.repository_id, - repository_url: p.repository_url, - })); - - console.log("Sending plugin data to backend, count:", pluginsToSend.length); - - const updatedPlugins = await invoke("check_plugin_updates", { - plugins: pluginsToSend, - repositories: repositoriesToCheck, - }); - - console.log("Bulk update check completed successfully, updating state."); - setPlugins(updatedPlugins); - - if (serverPath) { - try { - console.log("[checkForUpdates] Saving plugin data..."); - await invoke("save_plugin_data", { plugins: updatedPlugins, serverPath }); - console.log("[checkForUpdates] Plugin data saved successfully."); - } catch (saveError) { - console.error("Error saving plugin data after bulk update:", saveError); - setUpdateError(`Update check complete, but failed to save plugin data: ${saveError}`); - } - } - } catch (err) { - const errorMessage = `Error during bulk update check: ${err instanceof Error ? err.message : String(err)}`; - console.error(errorMessage); - setUpdateError(errorMessage); - } finally { - setIsCheckingUpdates(false); - setBulkUpdateProgress(null); - } - } - - async function checkSinglePlugin(plugin: Plugin) { - if (isScanning || isCheckingUpdates || pluginLoadingStates[plugin.file_path]) return; - - console.log(`Invoking single check for: ${plugin.name} (${plugin.file_path})`); - setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: true })); - setUpdateError(null); - - try { - const repositoriesToCheck = ['SpigotMC', 'Modrinth', 'GitHub']; - - const pluginToSend = { - name: plugin.name, - version: plugin.version, - authors: plugin.authors || [], - file_path: plugin.file_path, - file_hash: plugin.file_hash, - website: plugin.website, - description: plugin.description, - api_version: plugin.api_version, - main_class: plugin.main_class, - depend: plugin.depend, - soft_depend: plugin.soft_depend, - load_before: plugin.load_before, - commands: plugin.commands, - permissions: plugin.permissions, - has_update: plugin.has_update, - }; - - await invoke("check_single_plugin_update_command", { - plugin: pluginToSend, - repositories: repositoriesToCheck, - }); - } catch (err) { - const errorMessage = `Error invoking single update command for ${plugin.name}: ${err instanceof Error ? err.message : String(err)}`; - console.error(errorMessage); - setUpdateError(errorMessage); - setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: false })); - } - } - - async function updatePlugin(plugin: Plugin) { - if (!plugin.has_update || !plugin.latest_version || !plugin.repository_source || !plugin.repository_id) { - setUpdateError(`Cannot update ${plugin.name}: Missing required update information`); - return; - } - - setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: true })); - setUpdateError(null); - - try { - console.log(`Updating plugin: ${plugin.name} to version ${plugin.latest_version}`); - - // Try to update the plugin - try { - const newFilePath = await invoke("update_plugin", { - pluginId: plugin.repository_id, - version: plugin.latest_version, - repository: plugin.repository_source, - currentFilePath: plugin.file_path, - serverTypeStr: serverInfo?.server_type - }); - - console.log(`Update successful for ${plugin.name}, new file path: ${newFilePath}`); - - setPlugins(currentPlugins => currentPlugins.map(p => { - if (p.file_path === plugin.file_path) { - return { - ...p, - version: p.latest_version || p.version, - has_update: false, - latest_version: p.latest_version, - file_path: newFilePath - }; - } - return p; - })); - - if (serverPath) { - await invoke("save_plugin_data", { - plugins: plugins.map(p => { - if (p.file_path === plugin.file_path) { - return { - ...p, - version: p.latest_version || p.version, - has_update: false, - file_path: newFilePath - }; - } - return p; - }), - serverPath - }); - } - } catch (error: any) { - console.error(`Error updating plugin: ${error}`); - - // Check if this is a premium resource - const errorMsg = String(error); - if (errorMsg.startsWith("PREMIUM_RESOURCE:")) { - // Parse the premium resource error - const parts = errorMsg.split(":"); - if (parts.length >= 4) { - const resourceUrl = parts.slice(3).join(":"); // Rejoin in case URL contains colons - - // Show premium resource modal - setPremiumPluginInfo({ - name: plugin.name, - version: plugin.latest_version, - url: resourceUrl - }); - } else { - setUpdateError(`Premium plugin detected, but couldn't get resource URL: ${plugin.name}`); - } - } else { - // Standard error handling - setUpdateError(`Failed to update ${plugin.name}: ${error}`); - } - } - } catch (error) { - console.error(`Error in updatePlugin: ${error}`); - setUpdateError(`Update process error: ${error}`); - } finally { - setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: false })); - } - } - - const showPluginDetails = (plugin: Plugin) => { - setSelectedPlugin(plugin); - }; - - const closePluginDetails = () => { - setSelectedPlugin(null); - }; - - const handleSelectMatch = async (selectedMatch: PotentialPluginMatch) => { - if (!pluginToDisambiguate || !serverPath) return; - - console.log(`User selected match: ${selectedMatch.name} from ${selectedMatch.repository}`); - setShowMatchSelector(false); - setPluginLoadingStates(prev => ({ ...prev, [pluginToDisambiguate.file_path]: true })); - - try { - const updatedPlugin: Plugin = await invoke("set_plugin_repository", { - pluginFilePath: pluginToDisambiguate.file_path, - repository: selectedMatch.repository, - repositoryId: selectedMatch.repository_id, - pageUrl: selectedMatch.page_url, - serverPath: serverPath, - }); - - setPlugins(currentPlugins => - currentPlugins.map(p => - p.file_path === updatedPlugin.file_path ? updatedPlugin : p - ) - ); - console.log(`Successfully set repository source for ${updatedPlugin.name}`); - - } catch (err) { - console.error("Error setting plugin repository source:", err); - setUpdateError(`Failed to set repository source for ${pluginToDisambiguate.name}: ${err}`); - } finally { - setPluginLoadingStates(prev => ({ ...prev, [pluginToDisambiguate.file_path]: false })); - setPluginToDisambiguate(null); - setPotentialMatches([]); - } - }; - - const handleCancelMatchSelection = () => { - setShowMatchSelector(false); - setPluginToDisambiguate(null); - setPotentialMatches([]); - if (pluginToDisambiguate) { - setPluginLoadingStates(prev => ({ ...prev, [pluginToDisambiguate.file_path]: false })); - } - }; + const { isMatchSelectorOpen, potentialMatches, currentPluginForMatch, handleMatchSelection, closeMatchSelector } = usePluginActions(); return (
+ +
-

🔧 PlugSnatcher

-

Minecraft Plugin Manager

+

PlugSnatcher

+

Minecraft Server Plugin Manager

-
-
-

Select Server Directory

-
- setServerPath(e.target.value)} - placeholder="Enter server directory path..." - /> - -
- + + {/* Server section with selector and info */} +
+ + {!isScanning && serverInfo && } +
- {isScanning && scanProgress && ( -
-

Scanning: {scanProgress.current_file}

- - {scanProgress.processed} / {scanProgress.total} + {/* Scan progress - only shown when scanning is in progress */} + {/* Removed duplicate ScanProgress component since it's already in ServerSelector */} + + {/* Plugins section - only shown when scan is complete */} + {serverPath && scanComplete && plugins.length > 0 && ( +
+
+

Plugins ({plugins.length})

+ + {bulkUpdateProgress && }
- )} - {error && ( -
- {error} +
+
Name
+
Version
+
Compatibility
+
Actions
- )} -
- {serverInfo && ( - - )} - - {serverInfo && plugins.length > 0 && ( -
- - {isCheckingUpdates && bulkUpdateProgress && ( -
- Checking {bulkUpdateProgress.processed}/{bulkUpdateProgress.total}: {bulkUpdateProgress.current_plugin_name} - -
- )} - {updateError && ( -
{updateError}
- )} +
)} - {plugins.length > 0 && ( -
-

Installed Plugins ({plugins.length})

-
- Name - Current Version - Latest Version - Actions -
- {plugins.map((plugin) => ( -
-
{plugin.name}
-
{plugin.version}
-
- {plugin.repository_url && plugin.latest_version ? ( - - {plugin.latest_version} {!plugin.has_update && (Up to Date)} - - ) : ( - plugin.latest_version - ? <>{plugin.latest_version} {!plugin.has_update && (Up to Date)} - : 'N/A' - )} -
-
- - {plugin.has_update && ( - - )} - -
-
- ))} -
- )} - - {scanComplete && plugins.length === 0 && ( -
-

Installed Plugins (0)

-

No plugins found in this directory.

-
+ {/* No plugins message - only shown when scan complete but no plugins found */} + {serverPath && scanComplete && plugins.length === 0 && ( + )} + {/* Modals and Overlays */} {selectedPlugin && ( - + )} {warningMessage && ( setWarningMessage(null)} + isOpen={!!warningMessage} + onClose={clearWarningMessage} + title="Warning" + message={warningMessage.text} + variant={warningMessage.type === 'error' ? 'error' : + warningMessage.type === 'success' ? 'info' : 'warning'} /> )} - {showMatchSelector && pluginToDisambiguate && ( - 0 && ( + )} {premiumPluginInfo && ( setPremiumPluginInfo(null)} + isOpen={!!premiumPluginInfo} + onClose={clearPremiumPluginInfo} + pluginInfo={{ + name: premiumPluginInfo.name, + version: "Unknown", + url: "#" + }} /> )} -
+{isMatchSelectorOpen && currentPluginForMatch && ( + + )} + -
-

PlugSnatcher v0.1.0 - Developed with 💻 and ☕

-
+
); } -export default App; +export default App; \ No newline at end of file diff --git a/src/components/common/Badge/Badge.css b/src/components/common/Badge/Badge.css new file mode 100644 index 0000000..c9a4851 --- /dev/null +++ b/src/components/common/Badge/Badge.css @@ -0,0 +1,143 @@ +.app-badge { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 16px; + font-weight: 500; + transition: all 0.2s; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +/* Size variants */ +.app-badge-small { + padding: 0 8px; + font-size: 0.65rem; + height: 18px; + min-width: 18px; +} + +.app-badge-medium { + padding: 0 10px; + font-size: 0.75rem; + height: 22px; + min-width: 22px; +} + +.app-badge-large { + padding: 0 12px; + font-size: 0.85rem; + height: 26px; + min-width: 26px; +} + +/* Color variants - solid backgrounds */ +.app-badge-default { + background-color: var(--badge-default-bg, #e0e0e0); + color: var(--badge-default-text, #333333); +} + +.app-badge-primary { + background-color: var(--primary-color, #007bff); + color: white; +} + +.app-badge-success { + background-color: var(--success-color, #28a745); + color: white; +} + +.app-badge-warning { + background-color: var(--warning-color, #ffc107); + color: var(--warning-text, #212529); +} + +.app-badge-error { + background-color: var(--error-color, #dc3545); + color: white; +} + +.app-badge-info { + background-color: var(--info-color, #17a2b8); + color: white; +} + +/* Outlined variants */ +.app-badge-outlined { + background-color: transparent; + border: 1px solid; +} + +.app-badge-outlined.app-badge-default { + border-color: var(--badge-default-bg, #e0e0e0); + color: var(--badge-default-text, #555555); +} + +.app-badge-outlined.app-badge-primary { + border-color: var(--primary-color, #007bff); + color: var(--primary-color, #007bff); +} + +.app-badge-outlined.app-badge-success { + border-color: var(--success-color, #28a745); + color: var(--success-color, #28a745); +} + +.app-badge-outlined.app-badge-warning { + border-color: var(--warning-color, #ffc107); + color: var(--warning-color, #ffc107); +} + +.app-badge-outlined.app-badge-error { + border-color: var(--error-color, #dc3545); + color: var(--error-color, #dc3545); +} + +.app-badge-outlined.app-badge-info { + border-color: var(--info-color, #17a2b8); + color: var(--info-color, #17a2b8); +} + +/* Clickable badges */ +.app-badge-clickable { + cursor: pointer; +} + +.app-badge-clickable:hover { + opacity: 0.8; +} + +/* Badge with icon */ +.app-badge-icon { + display: flex; + align-items: center; + margin-right: 4px; +} + +/* Support for server type badges */ +.app-badge[data-server-type="Paper"] { + background-color: var(--paper-color, #4caf50); + color: white; +} + +.app-badge[data-server-type="Spigot"] { + background-color: var(--spigot-color, #ff9800); + color: white; +} + +.app-badge[data-server-type="Bukkit"] { + background-color: var(--bukkit-color, #9c27b0); + color: white; +} + +.app-badge[data-server-type="Forge"] { + background-color: var(--forge-color, #f44336); + color: white; +} + +.app-badge[data-server-type="Fabric"] { + background-color: var(--fabric-color, #2196f3); + color: white; +} \ No newline at end of file diff --git a/src/components/common/Badge/Badge.tsx b/src/components/common/Badge/Badge.tsx new file mode 100644 index 0000000..5a5f8ed --- /dev/null +++ b/src/components/common/Badge/Badge.tsx @@ -0,0 +1,109 @@ +import React, { memo, useCallback, KeyboardEvent } from 'react'; +import './Badge.css'; + +export interface BadgeProps { + /** + * Badge text content + */ + label: string; + + /** + * Badge visual variant/color scheme + * @default 'default' + */ + variant?: 'default' | 'primary' | 'success' | 'warning' | 'error' | 'info'; + + /** + * Badge size + * @default 'medium' + */ + size?: 'small' | 'medium' | 'large'; + + /** + * Optional icon to display before the label + */ + icon?: React.ReactNode; + + /** + * Whether to display badge with an outline style + * @default false + */ + outlined?: boolean; + + /** + * Additional CSS class + */ + className?: string; + + /** + * Optional tooltip text displayed on hover + */ + tooltip?: string; + + /** + * Optional badge data attribute used for custom styling + */ + dataAttribute?: { + name: string; + value: string; + }; + + /** + * Click handler for the badge + */ + onClick?: () => void; +} + +/** + * A reusable badge component for displaying labels, statuses, and categories + */ +const BadgeComponent: React.FC = ({ + label, + variant = 'default', + size = 'medium', + icon, + outlined = false, + className = '', + tooltip, + dataAttribute, + onClick +}) => { + const badgeClasses = [ + 'app-badge', + `app-badge-${variant}`, + `app-badge-${size}`, + outlined && 'app-badge-outlined', + onClick && 'app-badge-clickable', + className + ].filter(Boolean).join(' '); + + // Construct the data attributes object + const dataAttrs: { [key: string]: string } = {}; + if (dataAttribute) { + dataAttrs[`data-${dataAttribute.name}`] = dataAttribute.value; + } + + const handleKeyDown = useCallback((event: KeyboardEvent) => { + if (onClick && (event.key === 'Enter' || event.key === ' ')) { + event.preventDefault(); // Prevent default space bar scroll + onClick(); + } + }, [onClick]); + + return ( + + {icon && {icon}} + {label} + + ); +}; + +export default memo(BadgeComponent); \ No newline at end of file diff --git a/src/components/common/Button/Button.css b/src/components/common/Button/Button.css new file mode 100644 index 0000000..24c50e0 --- /dev/null +++ b/src/components/common/Button/Button.css @@ -0,0 +1,151 @@ +.app-button { + display: inline-flex; + align-items: center; + justify-content: center; + position: relative; + font-family: inherit; + font-weight: 500; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; + outline: none; + border: none; + gap: 8px; + text-align: center; + white-space: nowrap; +} + +/* Size variants */ +.app-button-small { + padding: 6px 12px; + font-size: 0.75rem; + min-height: 28px; +} + +.app-button-medium { + padding: 8px 16px; + font-size: 0.875rem; + min-height: 36px; +} + +.app-button-large { + padding: 10px 20px; + font-size: 1rem; + min-height: 44px; +} + +/* Style variants */ +.app-button-primary { + background-color: var(--primary-color, #007bff); + color: white; +} + +.app-button-primary:hover:not(:disabled) { + background-color: var(--primary-hover, #0069d9); +} + +.app-button-secondary { + background-color: var(--secondary-color, #6c757d); + color: white; +} + +.app-button-secondary:hover:not(:disabled) { + background-color: var(--secondary-hover, #5a6268); +} + +.app-button-outline { + background-color: transparent; + border: 1px solid var(--border-color, #d1d1d1); + color: var(--text-color, #333); +} + +.app-button-outline:hover:not(:disabled) { + background-color: var(--hover-bg); +} + +.app-button-danger { + background-color: var(--danger-color, #dc3545); + color: white; +} + +.app-button-danger:hover:not(:disabled) { + background-color: var(--danger-hover, #c82333); +} + +.app-button-success { + background-color: var(--success-color, #28a745); + color: #202124; +} + +.app-button-success:hover:not(:disabled) { + background-color: var(--success-hover, #218838); + color: #202124; +} + +.app-button-warning { + background-color: var(--warning-color, #ff9800); + color: #202124; +} + +.app-button-warning:hover:not(:disabled) { + background-color: var(--warning-hover); + color: #202124; +} + +.app-button-text { + background-color: transparent; + color: var(--text-color, #007bff); + padding-left: 4px; + padding-right: 4px; +} + +.app-button-text:hover:not(:disabled) { + background-color: var(--text-hover-bg); +} + +/* Disabled state */ +.app-button:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +/* Full width */ +.app-button-full-width { + width: 100%; +} + +/* Loading spinner */ +.app-button-loading { + cursor: wait; +} + +.app-button-spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 0.8s linear infinite; + margin-right: 4px; +} + +.app-button-outline .app-button-spinner, +.app-button-text .app-button-spinner { + border: 2px solid rgba(0, 0, 0, 0.1); + border-top-color: var(--primary-color, #007bff); +} + +/* Icon positioning */ +.app-button-start-icon, +.app-button-end-icon { + display: flex; + align-items: center; + justify-content: center; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/components/common/Button/Button.tsx b/src/components/common/Button/Button.tsx new file mode 100644 index 0000000..cf28ae4 --- /dev/null +++ b/src/components/common/Button/Button.tsx @@ -0,0 +1,91 @@ +import React, { memo } from 'react'; +import './Button.css'; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + /** + * Button visual variant + * @default 'primary' + */ + variant?: 'primary' | 'secondary' | 'outline' | 'danger' | 'success' | 'text' | 'warning'; + + /** + * Button size + * @default 'medium' + */ + size?: 'small' | 'medium' | 'large'; + + /** + * Whether the button is in a loading state + * @default false + */ + isLoading?: boolean; + + /** + * Icon to display before the button text + */ + startIcon?: React.ReactNode; + + /** + * Icon to display after the button text + */ + endIcon?: React.ReactNode; + + /** + * Full width button + * @default false + */ + fullWidth?: boolean; + + /** + * Button content + */ + children: React.ReactNode; + + /** + * Optional tooltip text displayed on hover + */ + tooltip?: string; +} + +/** + * A reusable button component with various styling options + */ +const ButtonComponent: React.FC = ({ + variant = 'primary', + size = 'medium', + isLoading = false, + disabled = false, + startIcon, + endIcon, + fullWidth = false, + className = '', + tooltip, + children, + ...restProps +}) => { + const buttonClasses = [ + 'app-button', + `app-button-${variant}`, + `app-button-${size}`, + isLoading && 'app-button-loading', + fullWidth && 'app-button-full-width', + className + ].filter(Boolean).join(' '); + + return ( + + ); +}; + +export default memo(ButtonComponent); \ No newline at end of file diff --git a/src/components/common/Modal/Modal.css b/src/components/common/Modal/Modal.css new file mode 100644 index 0000000..e31034c --- /dev/null +++ b/src/components/common/Modal/Modal.css @@ -0,0 +1,102 @@ +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(2px); +} + +.modal-container { + background-color: var(--surface-color, #292a2d); + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 600px; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; + animation: modal-fade-in 0.2s ease-out; + border: 1px solid var(--border-color, #3c4043); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid var(--border-color, #3c4043); + background-color: var(--surface-color, #292a2d); +} + +.modal-title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-color, #e8eaed); +} + +.modal-close-button { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--text-secondary-color, #9aa0a6); + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + padding: 0; + line-height: 1; +} + +.modal-close-button:hover { + color: var(--text-color, #e8eaed); + background-color: rgba(255, 255, 255, 0.1); +} + +.modal-content { + padding: 16px; + overflow-y: auto; + background-color: var(--surface-color, #292a2d); + color: var(--text-color, #e8eaed); +} + +@keyframes modal-fade-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@media (max-width: 768px) { + .modal-container { + width: 95%; + max-height: 95vh; + } + + .modal-header { + padding: 12px; + } + + .modal-content { + padding: 12px; + } + + .modal-title { + font-size: 1.1rem; + } +} \ No newline at end of file diff --git a/src/components/common/Modal/Modal.tsx b/src/components/common/Modal/Modal.tsx new file mode 100644 index 0000000..e9f7c9e --- /dev/null +++ b/src/components/common/Modal/Modal.tsx @@ -0,0 +1,114 @@ +import React, { ReactNode, useEffect, useRef } from 'react'; +import './Modal.css'; + +export interface ModalProps { + /** + * Title displayed at the top of the modal + */ + title?: string; + + /** + * Content of the modal + */ + children: ReactNode; + + /** + * Whether the modal is visible or not + */ + isOpen: boolean; + + /** + * Function to call when the modal close button is clicked + */ + onClose: () => void; + + /** + * Optional CSS class name to add to the modal for custom styling + */ + className?: string; + + /** + * Whether to show the close button in the header + * @default true + */ + showCloseButton?: boolean; + + /** + * Whether to close the modal when clicking outside of it + * @default true + */ + closeOnOutsideClick?: boolean; +} + +/** + * A reusable modal component that can be used for various purposes + */ +const Modal: React.FC = ({ + title, + children, + isOpen, + onClose, + className = '', + showCloseButton = true, + closeOnOutsideClick = true +}) => { + const modalRef = useRef(null); + + // Close modal when escape key is pressed + useEffect(() => { + const handleEscKey = (event: KeyboardEvent) => { + if (isOpen && event.key === 'Escape') { + onClose(); + } + }; + + document.addEventListener('keydown', handleEscKey); + return () => { + document.removeEventListener('keydown', handleEscKey); + }; + }, [isOpen, onClose]); + + // Handle outside click + const handleBackdropClick = (event: React.MouseEvent) => { + if (closeOnOutsideClick && modalRef.current && !modalRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + if (!isOpen) { + return null; + } + + return ( +
+
+ {(title || showCloseButton) && ( +
+ {title && } + {showCloseButton && ( + + )} +
+ )} + +
+
+ ); +}; + +export default Modal; \ No newline at end of file diff --git a/src/components/common/NotificationDisplay/NotificationDisplay.css b/src/components/common/NotificationDisplay/NotificationDisplay.css new file mode 100644 index 0000000..67653db --- /dev/null +++ b/src/components/common/NotificationDisplay/NotificationDisplay.css @@ -0,0 +1,51 @@ +.notification-display { + position: fixed; + top: 20px; + right: 20px; + padding: 1rem 1.5rem; + border-radius: 6px; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); + z-index: 1500; /* Above most content, below critical modals? */ + display: flex; + align-items: center; + justify-content: space-between; + min-width: 250px; + max-width: 400px; +} + +.notification-text { + flex-grow: 1; + margin-right: 1rem; +} + +.notification-close-button { + background: none; + border: none; + font-size: 1.2rem; + font-weight: bold; + color: inherit; /* Inherit color from parent */ + cursor: pointer; + padding: 0; + line-height: 1; +} + +/* Type-specific styles */ +.notification-success { + background-color: var(--success-color); + color: #202124; /* Dark text for contrast */ +} + +.notification-error { + background-color: var(--error-color); + color: white; +} + +.notification-warning { + background-color: var(--warning-color); + color: #202124; /* Dark text for contrast */ +} + +.notification-info { + background-color: var(--primary-color); + color: white; +} \ No newline at end of file diff --git a/src/components/common/NotificationDisplay/NotificationDisplay.tsx b/src/components/common/NotificationDisplay/NotificationDisplay.tsx new file mode 100644 index 0000000..6c0e778 --- /dev/null +++ b/src/components/common/NotificationDisplay/NotificationDisplay.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { useUIContext } from '../../../context/UIContext/useUIContext'; +import './NotificationDisplay.css'; + +/** + * Displays the current notification message from UIContext. + */ +export const NotificationDisplay: React.FC = () => { + const { warningMessage, clearWarningMessage } = useUIContext(); + + if (!warningMessage) { + return null; + } + + const { text, type, id } = warningMessage; + + return ( +
+ {text} + {/* Add a close button only for persistent errors? */} + {type === 'error' && ( + + )} +
+ ); +}; + +export default NotificationDisplay; \ No newline at end of file diff --git a/src/components/common/ProgressBar/ProgressBar.css b/src/components/common/ProgressBar/ProgressBar.css new file mode 100644 index 0000000..fe8aae4 --- /dev/null +++ b/src/components/common/ProgressBar/ProgressBar.css @@ -0,0 +1,69 @@ +.progress-container { + display: flex; + flex-direction: column; + width: 100%; + margin: 12px 0; +} + +.progress-label { + font-size: 0.875rem; + margin-bottom: 4px; + color: var(--text-secondary, #666); +} + +.progress-bar-wrapper { + width: 100%; + height: 10px; + background-color: var(--progress-bg, #f0f0f0); + border-radius: 6px; + overflow: hidden; + position: relative; +} + +.progress-bar { + height: 100%; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: width 0.3s ease; + position: relative; + min-width: 20px; +} + +.progress-percentage { + position: absolute; + font-size: 0.7rem; + color: var(--progress-text, #fff); + font-weight: 500; + text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); + right: 6px; +} + +.progress-values { + font-size: 0.75rem; + margin-top: 4px; + text-align: right; + color: var(--text-secondary, #666); +} + +/* Color variants */ +.progress-bar-primary { + background-color: var(--primary-color, #007bff); +} + +.progress-bar-success { + background-color: var(--success-color, #28a745); +} + +.progress-bar-warning { + background-color: var(--warning-color, #ffc107); +} + +.progress-bar-error { + background-color: var(--error-color, #dc3545); +} + +.progress-bar-info { + background-color: var(--info-color, #17a2b8); +} \ No newline at end of file diff --git a/src/components/common/ProgressBar/ProgressBar.tsx b/src/components/common/ProgressBar/ProgressBar.tsx new file mode 100644 index 0000000..07de988 --- /dev/null +++ b/src/components/common/ProgressBar/ProgressBar.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import './ProgressBar.css'; + +export interface ProgressBarProps { + /** + * Current value of the progress (should be between min and max) + */ + value: number; + + /** + * Maximum value for the progress bar + * @default 100 + */ + max?: number; + + /** + * Minimum value for the progress bar + * @default 0 + */ + min?: number; + + /** + * Label to display with the progress + */ + label?: string; + + /** + * Whether to show percentage text inside the progress bar + * @default true + */ + showPercentage?: boolean; + + /** + * Additional CSS class for custom styling + */ + className?: string; + + /** + * Progress bar color + */ + color?: 'primary' | 'success' | 'warning' | 'error' | 'info'; + + /** + * Whether to display the current value and max next to the bar + * @default false + */ + showValues?: boolean; + + /** + * Format for displaying values (when showValues is true) + * @default "{value}/{max}" + */ + valueFormat?: string; +} + +/** + * A reusable progress bar component that shows the progress of an operation + */ +const ProgressBar: React.FC = ({ + value, + max = 100, + min = 0, + label, + showPercentage = true, + className = '', + color = 'primary', + showValues = false, + valueFormat = "{value}/{max}" +}) => { + // Calculate percentage + const range = max - min; + const valueInRange = Math.max(min, Math.min(max, value)) - min; + const percentage = range > 0 ? Math.round((valueInRange / range) * 100) : 0; + + // Format value display + const formattedValue = valueFormat + .replace('{value}', value.toString()) + .replace('{max}', max.toString()) + .replace('{min}', min.toString()) + .replace('{percentage}', `${percentage}%`); + + return ( +
+ {label &&
{label}
} +
+
+ {showPercentage && ( + {percentage}% + )} +
+
+ {showValues && ( + + )} +
+ ); +}; + +export default ProgressBar; \ No newline at end of file diff --git a/src/components/layout/Footer/Footer.css b/src/components/layout/Footer/Footer.css new file mode 100644 index 0000000..c8afef2 --- /dev/null +++ b/src/components/layout/Footer/Footer.css @@ -0,0 +1,62 @@ +.footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 1.5rem; + background-color: var(--color-background-dark); + color: var(--color-text-muted); + border-top: 1px solid var(--color-border); + height: 40px; + font-size: 0.8rem; +} + +.footer-left, .footer-center, .footer-right { + display: flex; + align-items: center; +} + +.footer-left { + flex: 1; +} + +.footer-center { + flex: 2; + justify-content: center; +} + +.footer-right { + flex: 1; + justify-content: flex-end; +} + +.footer-version { + opacity: 0.7; +} + +.server-info { + display: flex; + align-items: center; + gap: 1rem; +} + +.server-type { + font-weight: 500; + color: var(--color-text-light); +} + +.plugin-count { + background-color: rgba(0, 0, 0, 0.2); + padding: 0.2rem 0.5rem; + border-radius: 4px; +} + +.github-link { + color: var(--color-primary); + text-decoration: none; + transition: color 0.2s; +} + +.github-link:hover { + color: var(--color-primary-light); + text-decoration: underline; +} \ No newline at end of file diff --git a/src/components/layout/Footer/Footer.tsx b/src/components/layout/Footer/Footer.tsx new file mode 100644 index 0000000..290b3aa --- /dev/null +++ b/src/components/layout/Footer/Footer.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { usePluginContext } from '../../../context/PluginContext/usePluginContext'; +import { useServerContext } from '../../../context/ServerContext/useServerContext'; +import './Footer.css'; + +interface FooterProps { + appVersion?: string; +} + +export const Footer: React.FC = ({ appVersion = '1.0.0' }) => { + const { plugins } = usePluginContext(); + const { serverInfo } = useServerContext(); + + const getServerTypeText = () => { + if (!serverInfo) return 'No server selected'; + + let text = `${serverInfo.server_type}`; + if (serverInfo.minecraft_version) { + text += ` (${serverInfo.minecraft_version})`; + } + return text; + }; + + return ( +
+
+ PlugSnatcher v{appVersion} +
+ + + +
+ ); +}; + +export default Footer; \ No newline at end of file diff --git a/src/components/layout/Header/Header.css b/src/components/layout/Header/Header.css new file mode 100644 index 0000000..2e30697 --- /dev/null +++ b/src/components/layout/Header/Header.css @@ -0,0 +1,112 @@ +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1.5rem; + background-color: var(--color-background-dark); + color: var(--color-text-light); + border-bottom: 1px solid var(--color-border); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + height: 60px; +} + +.header-left, .header-center, .header-right { + display: flex; + align-items: center; +} + +.header-left { + flex: 1; +} + +.header-center { + flex: 2; + justify-content: center; +} + +.header-right { + flex: 1; + justify-content: flex-end; +} + +.app-title { + font-size: 1.5rem; + font-weight: bold; + margin: 0; + margin-right: 0.5rem; + color: var(--color-primary); +} + +.app-version { + font-size: 0.8rem; + color: var(--color-text-muted); + margin-right: 0.75rem; +} + +.update-badge { + background-color: var(--color-accent); + color: white; + font-size: 0.7rem; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-weight: bold; + animation: pulse 2s infinite; +} + +.server-path-display { + display: flex; + align-items: center; + background-color: rgba(0, 0, 0, 0.1); + padding: 0.4rem 0.75rem; + border-radius: 4px; + max-width: 100%; + overflow: hidden; +} + +.server-label { + font-weight: bold; + margin-right: 0.5rem; + white-space: nowrap; + color: var(--color-text-muted); +} + +.server-path { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 300px; + color: var(--color-text-light); +} + +.update-check-button { + background-color: var(--color-primary); + color: white; + border: none; + border-radius: 4px; + padding: 0.5rem 1rem; + font-size: 0.9rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.update-check-button:hover:not(:disabled) { + background-color: var(--color-primary-dark); +} + +.update-check-button:disabled { + background-color: var(--color-disabled); + cursor: not-allowed; + opacity: 0.7; +} + +@keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.6; + } + 100% { + opacity: 1; + } +} \ No newline at end of file diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx new file mode 100644 index 0000000..3006f3f --- /dev/null +++ b/src/components/layout/Header/Header.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useAppUpdates } from '../../../hooks/useAppUpdates'; +import { useServerContext } from '../../../context/ServerContext/useServerContext'; +import { usePluginContext } from '../../../context/PluginContext/usePluginContext'; +import './Header.css'; + +interface HeaderProps { + appVersion?: string; +} + +export const Header: React.FC = ({ appVersion = '1.0.0' }) => { + const { isCheckingAppUpdate, appUpdateAvailable, checkForAppUpdate } = useAppUpdates(); + const { serverPath } = useServerContext(); + const { isCheckingUpdates } = usePluginContext(); + + const handleCheckForUpdates = async () => { + if (!isCheckingAppUpdate) { + await checkForAppUpdate(); + } + }; + + return ( +
+
+

PlugSnatcher

+ v{appVersion} + {appUpdateAvailable && ( + Update Available + )} +
+ +
+ {serverPath && ( +
+ Selected Server: + + {serverPath.length > 40 ? `...${serverPath.slice(-40)}` : serverPath} + +
+ )} +
+ +
+ +
+
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/src/components/layout/MainContent/MainContent.css b/src/components/layout/MainContent/MainContent.css new file mode 100644 index 0000000..dfd837b --- /dev/null +++ b/src/components/layout/MainContent/MainContent.css @@ -0,0 +1,78 @@ +.main-content { + flex: 1; + display: flex; + flex-direction: column; + overflow-y: auto; + padding: 0; + position: relative; + background-color: var(--background-color); +} + +.content-container { + flex: 1; + padding: 1.5rem; + display: flex; + flex-direction: column; + overflow-y: auto; + max-width: 1200px; + margin: 0 auto; + width: 100%; +} + +.warning-message { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1.5rem; + font-size: 0.9rem; + width: 100%; + animation: slide-down 0.3s ease-out; +} + +.warning-text { + flex: 1; +} + +.warning-info { + background-color: var(--primary-color); + color: white; +} + +.warning-success { + background-color: var(--success-color); + color: white; +} + +.warning-warning { + background-color: var(--warning-color); + color: white; +} + +.warning-error { + background-color: var(--error-color); + color: white; +} + +.close-warning { + background: none; + border: none; + color: inherit; + cursor: pointer; + font-size: 1.2rem; + margin-left: 1rem; + opacity: 0.7; + transition: opacity 0.2s; +} + +.close-warning:hover { + opacity: 1; +} + +@keyframes slide-down { + from { + transform: translateY(-100%); + } + to { + transform: translateY(0); + } +} \ No newline at end of file diff --git a/src/components/layout/MainContent/MainContent.tsx b/src/components/layout/MainContent/MainContent.tsx new file mode 100644 index 0000000..e9b4d7b --- /dev/null +++ b/src/components/layout/MainContent/MainContent.tsx @@ -0,0 +1,34 @@ +import React, { ReactNode } from 'react'; +import { useUIContext } from '../../../context/UIContext/useUIContext'; +import './MainContent.css'; + +interface MainContentProps { + children: ReactNode; +} + +export const MainContent: React.FC = ({ children }) => { + const { warningMessage, clearWarningMessage } = useUIContext(); + + return ( +
+ {warningMessage && ( +
+ {warningMessage.text} + +
+ )} + +
+ {children} +
+
+ ); +}; + +export default MainContent; \ No newline at end of file diff --git a/src/components/plugins/NoPluginsMessage/NoPluginsMessage.css b/src/components/plugins/NoPluginsMessage/NoPluginsMessage.css new file mode 100644 index 0000000..88579ae --- /dev/null +++ b/src/components/plugins/NoPluginsMessage/NoPluginsMessage.css @@ -0,0 +1,74 @@ +.no-plugins-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem 2rem; + text-align: center; + background-color: var(--color-background); + border-radius: 8px; + border: 1px dashed var(--color-border); + margin: 2rem auto; + max-width: 500px; +} + +.no-plugins-message h3 { + margin: 1rem 0 0.5rem; + font-size: 1.5rem; + color: var(--color-text); +} + +.no-plugins-message p { + margin: 0.25rem 0; + color: var(--color-text-muted); + font-size: 1rem; + max-width: 400px; +} + +.no-plugins-message .suggestion { + font-size: 0.9rem; + margin-top: 1rem; + padding: 0.75rem; + background-color: rgba(var(--color-info-rgb), 0.1); + border-radius: 4px; + color: var(--color-info); +} + +.no-plugins-message .icon { + font-size: 3rem; + margin-bottom: 0.5rem; +} + +.no-plugins-message.scanning { + background-color: rgba(var(--color-primary-rgb), 0.05); + border-color: var(--color-primary); +} + +.no-plugins-message.no-server { + background-color: rgba(var(--color-info-rgb), 0.05); + border-color: var(--color-info); +} + +.no-plugins-message.empty { + background-color: rgba(var(--color-warning-rgb), 0.05); + border-color: var(--color-warning); +} + +.spinner { + border: 4px solid rgba(var(--color-primary-rgb), 0.2); + border-left: 4px solid var(--color-primary); + border-radius: 50%; + width: 40px; + height: 40px; + margin-bottom: 1rem; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/components/plugins/NoPluginsMessage/NoPluginsMessage.tsx b/src/components/plugins/NoPluginsMessage/NoPluginsMessage.tsx new file mode 100644 index 0000000..b4f9b9b --- /dev/null +++ b/src/components/plugins/NoPluginsMessage/NoPluginsMessage.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { useServerActions } from '../../../hooks/useServerActions'; +import './NoPluginsMessage.css'; + +export const NoPluginsMessage: React.FC = () => { + const { serverPath, scanComplete, isScanning } = useServerActions(); + + if (isScanning) { + return ( +
+
+

Scanning for plugins...

+

This might take a moment, please wait.

+
+ ); + } + + if (!serverPath) { + return ( +
+
📁
+

No Server Selected

+

Please select a Minecraft server directory to get started.

+
+ ); + } + + if (scanComplete && !isScanning) { + return ( +
+
🔍
+

No Plugins Found

+

We couldn't find any plugins in the selected server directory.

+

Make sure you've selected a valid Minecraft server with plugins installed.

+
+ ); + } + + // Default message if we're in an intermediate state + return ( +
+
🧩
+

Ready to Scan

+

Click "Scan for Plugins" to discover plugins in your server.

+
+ ); +}; + +export default NoPluginsMessage; \ No newline at end of file diff --git a/src/components/plugins/PluginDetails/PluginDetails.css b/src/components/plugins/PluginDetails/PluginDetails.css new file mode 100644 index 0000000..2ca0d2f --- /dev/null +++ b/src/components/plugins/PluginDetails/PluginDetails.css @@ -0,0 +1,228 @@ +.plugin-details-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + padding: 20px; + animation: fadeIn 0.2s ease-out; +} + +.plugin-details-container { + background-color: var(--background-color); + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + width: 100%; + max-width: 1200px; + max-height: 90vh; + overflow-y: auto; + overflow-x: hidden; + position: relative; + animation: slideIn 0.2s ease-out; + display: flex; + flex-direction: column; +} + +.plugin-details-header { + position: sticky; + top: 0; + background-color: var(--background-color); + padding: 20px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: flex-start; + z-index: 1; + width: auto; + box-sizing: border-box; +} + +.plugin-details-content { + padding: 20px; + align-self: center; + box-sizing: border-box; + max-width: 100%; + width: 100%; +} + +.close-button { + background: none; + border: none; + font-size: 24px; + color: var(--text-color); + cursor: pointer; + padding: 8px; + border-radius: 4px; + transition: background-color 0.2s; + line-height: 1; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; +} + +.close-button:hover { + background-color: var(--hover-color); +} + +.plugin-details-columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-top: 20px; + width: 100%; +} + +.plugin-details-column { + width: 100%; +} + +@media (max-width: 768px) { + .plugin-details-columns { + grid-template-columns: 1fr; + } +} + +.plugin-details-section { + margin-bottom: 24px; +} + +.plugin-details-section h3 { + margin-bottom: 12px; + color: var(--heading-color); + font-size: 1.1em; +} + +.plugin-name { + margin: 0; + font-size: 1.5em; + color: var(--heading-color); +} + +.plugin-version-info { + margin-top: 8px; + display: flex; + align-items: center; + gap: 12px; +} + +.update-info { + display: flex; + align-items: center; + gap: 8px; +} + +.version-link { + color: var(--link-color); + text-decoration: none; +} + +.version-link:hover { + text-decoration: underline; +} + +.plugin-description { + line-height: 1.5; + color: var(--text-color); +} + +.plugin-authors { + margin: 0; + color: var(--text-color); +} + +.plugin-website { + display: inline-block; + margin-top: 8px; + color: var(--link-color); + text-decoration: none; +} + +.plugin-website:hover { + text-decoration: underline; +} + +.dependency-list { + list-style: none; + padding: 0; + margin: 0; +} + +.dependency-list li { + margin-bottom: 4px; +} + +.file-info-item { + margin-bottom: 8px; + display: flex; + gap: 8px; + width: 100%; +} + +.file-info-label { + color: var(--text-muted); + min-width: 100px; + flex-shrink: 0; +} + +.file-path { + word-break: break-all; +} + +.file-info-value { + word-break: break-all; + flex: 1; +} + +.platform-badges { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.platform-badge { + padding: 4px 8px; + border-radius: 4px; + font-size: 0.9em; + background-color: var(--badge-background); + color: var(--badge-text); +} + +.plugin-actions-section { + padding: 20px; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: 12px; + position: sticky; + bottom: 0; + background-color: var(--background-color); + width: auto; + box-sizing: border-box; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideIn { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} \ No newline at end of file diff --git a/src/components/plugins/PluginDetails/PluginDetails.tsx b/src/components/plugins/PluginDetails/PluginDetails.tsx new file mode 100644 index 0000000..66e4e17 --- /dev/null +++ b/src/components/plugins/PluginDetails/PluginDetails.tsx @@ -0,0 +1,291 @@ +import React, { useEffect, useCallback } from 'react'; +import { Plugin } from '../../../types/plugin.types'; +import { usePluginActions } from '../../../hooks/usePluginActions'; +import Badge from '../../common/Badge/Badge'; +import Button from '../../common/Button/Button'; +import './PluginDetails.css'; + +interface PluginDetailsProps { + plugin: Plugin; + onClose: () => void; +} + +// Add a function to create repository URL +const getRepositoryUrl = (plugin: Plugin): string | null => { + // Return existing repository URL if available + if (plugin.repository_url) { + return plugin.repository_url; + } + + // Try to construct URL based on repository source and ID + if (plugin.repository_source && plugin.repository_id) { + switch (plugin.repository_source) { + case 'SpigotMC': + return `https://www.spigotmc.org/resources/${plugin.repository_id}`; + case 'Modrinth': + return `https://modrinth.com/plugin/${plugin.repository_id}`; + case 'GitHub': + return `https://github.com/${plugin.repository_id}/releases`; + case 'HangarMC': + return `https://hangar.papermc.io/plugins/${plugin.repository_id}`; + default: + return null; + } + } + + return null; +}; + +export const PluginDetails: React.FC = ({ plugin, onClose }) => { + const { updatePlugin, checkSinglePlugin, pluginLoadingStates } = usePluginActions(); + + // Check if this specific plugin is being checked or updated + const isLoading = !!pluginLoadingStates[plugin.file_path]; + + // Handle escape key to close modal + const handleEscapeKey = useCallback((event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose(); + } + }, [onClose]); + + // Handle clicking outside the modal to close + const handleOverlayClick = useCallback((event: React.MouseEvent) => { + if (event.target === event.currentTarget) { + onClose(); + } + }, [onClose]); + + // Add event listener for escape key + useEffect(() => { + document.addEventListener('keydown', handleEscapeKey); + return () => { + document.removeEventListener('keydown', handleEscapeKey); + }; + }, [handleEscapeKey]); + + const handleCheckUpdate = async () => { + if (isLoading) return; // Prevent multiple clicks + + try { + console.log(`Checking for updates for plugin: ${plugin.name}`); + await checkSinglePlugin(plugin); + } catch (error) { + console.error(`Error checking updates for plugin ${plugin.name}:`, error); + } + }; + + const handleUpdate = async () => { + if (isLoading) return; // Prevent multiple clicks + + try { + console.log(`Updating plugin: ${plugin.name}`); + await updatePlugin(plugin); + // Note: Any success/error notifications will be handled by the usePluginActions hook + } catch (error) { + console.error(`Error updating plugin ${plugin.name}:`, error); + } + }; + + const formatDate = (timestamp: number | null): string => { + if (!timestamp) return 'Unknown'; + return new Date(timestamp).toLocaleDateString(); + }; + + const renderDependencies = () => { + const dependencies = [ + ...(plugin.depend || []), + ...(plugin.soft_depend || []) + ].filter(Boolean); + + if (dependencies.length === 0) { + return

No dependencies

; + } + + return ( +
    + {dependencies.map((dep, index) => ( +
  • + {dep} +
  • + ))} +
+ ); + }; + + const getSizeInMB = (bytes: number | undefined): string => { + if (!bytes) return 'Unknown'; + return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; + }; + + return ( +
+
+
+
+

{plugin.name}

+
+ + Version: { + plugin.repository_url || (plugin.repository_source && plugin.repository_id) ? ( + + {plugin.version} + + ) : plugin.version + } + + {plugin.has_update && plugin.latest_version && ( +
+ + + Latest: { + plugin.repository_url || (plugin.repository_source && plugin.repository_id) ? ( + + {plugin.latest_version} + + ) : plugin.latest_version + } + +
+ )} +
+
+ + +
+ +
+
+

Description

+

+ {plugin.description || 'No description available.'} +

+
+ +
+
+
+

Author Information

+

+ {plugin.authors && plugin.authors.length > 0 + ? plugin.authors.join(', ') + : 'Unknown author'} +

+ {plugin.website && ( + + Visit Website + + )} +
+ +
+

Dependencies

+ {renderDependencies()} +
+
+ +
+
+

File Information

+
+ Path: + {plugin.file_path} +
+
+ Hash: + {plugin.file_hash.substring(0, 10)}... +
+ {plugin.repository_source && ( +
+ Source: + + {plugin.repository_url || getRepositoryUrl(plugin) ? ( + + {plugin.repository_source} + + ) : plugin.repository_source} + +
+ )} + {plugin.platform_compatibility && Object.keys(plugin.platform_compatibility).length > 0 && ( +
+ Compatible with: +
+ {Object.entries(plugin.platform_compatibility).map(([platform, isCompatible]) => ( + + {platform} + + ))} +
+
+ )} +
+
+
+
+ +
+ {plugin.has_update ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export default PluginDetails; \ No newline at end of file diff --git a/src/components/plugins/PluginItem/PluginItem.css b/src/components/plugins/PluginItem/PluginItem.css new file mode 100644 index 0000000..e7f8400 --- /dev/null +++ b/src/components/plugins/PluginItem/PluginItem.css @@ -0,0 +1,172 @@ +.plugin-item { + display: grid; + grid-template-columns: 3fr 1fr 1.5fr 1.5fr; + align-items: center; + background-color: var(--surface-color); + border-bottom: 1px solid var(--border-color); + padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 3); + transition: background-color 0.2s; +} + +.plugin-item:last-child { + border-bottom: none; +} + +.plugin-item:hover { + background-color: rgba(255, 255, 255, 0.03); +} + +.plugin-item.has-update { + background-color: rgba(255, 152, 0, 0.08); +} + +.plugin-item.has-update:hover { + background-color: rgba(255, 152, 0, 0.12); +} + +.plugin-name-column { + padding-right: calc(var(--spacing-unit) * 2); + min-width: 0; +} + +.plugin-version-column { + padding: 0 calc(var(--spacing-unit)); +} + +.plugin-compatibility-column { + padding: 0 calc(var(--spacing-unit)); +} + +.plugin-actions-column { + justify-self: end; + text-align: right; +} + +.plugin-name { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + color: var(--text-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.plugin-version { + display: flex; + align-items: center; + gap: var(--spacing-unit); + font-size: 0.95rem; +} + +.current-version { + color: var(--text-secondary-color); +} + +.version-arrow { + color: var(--text-secondary-color); + font-size: 0.8rem; +} + +.latest-version { + color: var(--success-color); + font-weight: 500; +} + +.plugin-description { + margin: calc(var(--spacing-unit) * 0.75) 0; + font-size: 0.9rem; + color: var(--text-secondary-color); + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.plugin-authors { + font-size: 0.85rem; + color: var(--text-secondary-color); + margin-top: calc(var(--spacing-unit) * 0.5); +} + +.plugin-actions { + display: flex; + flex-direction: row; + align-items: center; + gap: calc(var(--spacing-unit)); +} + +.compatibility-unknown { + color: var(--text-secondary-color); + font-size: 0.9rem; + font-style: italic; +} + +.platform-badges { + display: flex; + flex-wrap: wrap; + gap: calc(var(--spacing-unit) * 0.5); +} + +.platform-badge { + display: inline-flex; + align-items: center; + justify-content: center; + color: white; + padding: calc(var(--spacing-unit) * 0.25) calc(var(--spacing-unit) * 0.75); + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); + text-shadow: 0 1px 1px rgba(0,0,0,0.2); +} + +.platform-badge[data-server-type="Paper"] { + background-color: #3498db; +} + +.platform-badge[data-server-type="Spigot"] { + background-color: #e67e22; +} + +.platform-badge[data-server-type="Bukkit"] { + background-color: #2ecc71; +} + +/* Custom styles for plugin action buttons */ +.info-button { + background-color: var(--primary-color); + color: white; +} + +.update-button { + background-color: var(--warning-color); + color: white; +} + +/* Responsive styles */ +@media (max-width: 900px) { + .plugin-item { + grid-template-columns: 1fr; + gap: calc(var(--spacing-unit) * 1.5); + padding: calc(var(--spacing-unit) * 2); + } + + .plugin-name-column, + .plugin-version-column, + .plugin-compatibility-column, + .plugin-actions-column { + padding: 0; + } + + .plugin-actions-column { + justify-self: start; + text-align: left; + } + + .plugin-actions { + padding-top: var(--spacing-unit); + } +} \ No newline at end of file diff --git a/src/components/plugins/PluginItem/PluginItem.tsx b/src/components/plugins/PluginItem/PluginItem.tsx new file mode 100644 index 0000000..8bbe5f0 --- /dev/null +++ b/src/components/plugins/PluginItem/PluginItem.tsx @@ -0,0 +1,159 @@ +import React, { useCallback, memo } from 'react'; +import { Plugin } from '../../../types/plugin.types'; +import { usePluginActions } from '../../../hooks/usePluginActions'; +import Badge from '../../common/Badge/Badge'; +import Button from '../../common/Button/Button'; +import './PluginItem.css'; + +interface PluginItemProps { + plugin: Plugin; + onSelect?: (plugin: Plugin) => void; +} + +const PluginItemComponent: React.FC = ({ plugin, onSelect }) => { + const { updatePlugin, checkSinglePlugin, pluginLoadingStates } = usePluginActions(); + + // Check if this specific plugin is being checked or updated + const isLoading = !!pluginLoadingStates[plugin.file_path]; + + const handlePluginClick = useCallback(() => { + if (onSelect) { + onSelect(plugin); + } + }, [onSelect, plugin]); + + const handleUpdateClick = useCallback(async (e: React.MouseEvent) => { + e.stopPropagation(); // Prevent triggering the parent click + if (isLoading) return; // Prevent multiple clicks while loading + + try { + console.log(`Updating plugin: ${plugin.name}`); + await updatePlugin(plugin); + } catch (error) { + console.error(`Error updating plugin ${plugin.name}:`, error); + } + }, [updatePlugin, plugin, isLoading]); + + const handleCheckUpdateClick = useCallback(async (e: React.MouseEvent) => { + e.stopPropagation(); // Prevent triggering the parent click + if (isLoading) return; // Prevent multiple clicks while loading + + try { + console.log(`Checking for updates for plugin: ${plugin.name}`); + await checkSinglePlugin(plugin); + } catch (error) { + console.error(`Error checking updates for plugin ${plugin.name}:`, error); + } + }, [checkSinglePlugin, plugin, isLoading]); + + const handleInfoClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); // Prevent triggering the parent click + if (onSelect) { + console.log(`Viewing details for plugin: ${plugin.name}`); + onSelect(plugin); + } + }, [onSelect, plugin]); + + // Get the plugin compatibility information for display, if available + const compatibility = plugin.platform_compatibility ? Object.keys(plugin.platform_compatibility).length > 0 : false; + + return ( +
+ {/* Plugin Name Column */} +
+

{plugin.name}

+ {plugin.authors && plugin.authors.length > 0 && ( +
+ By {plugin.authors.join(', ')} +
+ )} + {plugin.description && ( +

+ {plugin.description.length > 100 + ? `${plugin.description.substring(0, 100)}...` + : plugin.description} +

+ )} +
+ + {/* Version Column */} +
+
+ v{plugin.version} + {plugin.has_update && plugin.latest_version && ( + <> + + v{plugin.latest_version} + + )} +
+
+ + {/* Compatibility Column */} +
+ {compatibility ? ( +
+ {Object.entries(plugin.platform_compatibility || {}).map(([platform, isCompatible]) => ( + + {platform} + + ))} +
+ ) : ( + Unknown + )} +
+ + {/* Actions Column */} +
+
+ + + {plugin.has_update ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export const PluginItem = memo(PluginItemComponent); \ No newline at end of file diff --git a/src/components/plugins/PluginList/PluginList.css b/src/components/plugins/PluginList/PluginList.css new file mode 100644 index 0000000..cbb293e --- /dev/null +++ b/src/components/plugins/PluginList/PluginList.css @@ -0,0 +1,164 @@ +.plugin-list-container { + width: 100%; +} + +.plugin-list-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); + background-color: var(--surface-color); +} + +.plugin-list-title { + margin: 0; + font-size: 1.5rem; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-color); +} + +.plugin-count { + font-size: 1rem; + color: var(--text-secondary-color); + font-weight: normal; +} + +.outdated-badge { + background-color: var(--warning-color); + color: white; + font-size: 0.75rem; + padding: 0.2rem 0.6rem; + border-radius: 12px; + font-weight: bold; + margin-left: 0.5rem; +} + +.plugin-list-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.search-container { + position: relative; + width: 100%; + padding: 1rem 1.5rem; + background-color: transparent; +} + +.search-input { + width: 100%; + padding: 0.6rem 2rem 0.6rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 6px; + background-color: var(--background-color); + color: var(--text-color); + font-size: 0.9rem; + transition: all 0.2s; +} + +.search-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2); +} + +.clear-search { + position: absolute; + right: 2rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-secondary-color); + font-size: 1.2rem; + cursor: pointer; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.clear-search:hover { + color: var(--text-color); + background-color: rgba(255, 255, 255, 0.1); +} + +.plugin-list-controls { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1.5rem; + background-color: rgba(255, 255, 255, 0.02); + border-bottom: 1px solid var(--border-color); +} + +.sort-controls { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.75rem; +} + +.sort-label { + font-size: 0.85rem; + color: var(--text-secondary-color); + font-weight: 500; +} + +.sort-button { + background: none; + border: none; + padding: 0.3rem 0.6rem; + border-radius: 4px; + font-size: 0.85rem; + cursor: pointer; + color: var(--text-color); + transition: all 0.2s; +} + +.sort-button:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +.sort-button.active { + background-color: rgba(26, 115, 232, 0.2); + color: var(--primary-color); + font-weight: 500; +} + +.filter-info { + font-size: 0.85rem; + color: var(--text-secondary-color); +} + +.plugin-list { + overflow-y: auto; +} + +.no-results { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; + text-align: center; + color: var(--text-secondary-color); + background-color: var(--surface-color); + min-height: 200px; +} + +.no-results p { + margin-bottom: 1rem; + font-size: 1.1rem; +} + +.no-results button { + /* Button component will style this */ +} \ No newline at end of file diff --git a/src/components/plugins/PluginList/PluginList.tsx b/src/components/plugins/PluginList/PluginList.tsx new file mode 100644 index 0000000..c3f1917 --- /dev/null +++ b/src/components/plugins/PluginList/PluginList.tsx @@ -0,0 +1,134 @@ +import React, { useState } from 'react'; +import { usePluginActions } from '../../../hooks/usePluginActions'; +import { usePluginContext } from '../../../context/PluginContext/usePluginContext'; +import { Plugin } from '../../../types/plugin.types'; +import { PluginItem } from '../PluginItem/PluginItem'; +import Button from '../../common/Button/Button'; +import './PluginList.css'; + +export const PluginList: React.FC = () => { + const { plugins, isCheckingUpdates } = usePluginActions(); + const { showPluginDetails } = usePluginContext(); + const [searchTerm, setSearchTerm] = useState(''); + const [sortBy, setSortBy] = useState<'name' | 'version' | 'update'>('name'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); + + // Filter plugins based on search term + const filteredPlugins = plugins.filter(plugin => + plugin.name.toLowerCase().includes(searchTerm.toLowerCase()) || + plugin.version.toLowerCase().includes(searchTerm.toLowerCase()) || + (plugin.description && plugin.description.toLowerCase().includes(searchTerm.toLowerCase())) + ); + + // Sort plugins based on sort criteria + const sortedPlugins = [...filteredPlugins].sort((a, b) => { + let comparison = 0; + + switch (sortBy) { + case 'name': + comparison = a.name.localeCompare(b.name); + break; + case 'version': + comparison = a.version.localeCompare(b.version); + break; + case 'update': + // Sort by update status (plugins with updates first) + comparison = (b.has_update ? 1 : 0) - (a.has_update ? 1 : 0); + break; + } + + return sortOrder === 'asc' ? comparison : -comparison; + }); + + const handleSort = (criteria: 'name' | 'version' | 'update') => { + if (criteria === sortBy) { + // Toggle sort order if clicking the same criteria + setSortOrder(prevOrder => prevOrder === 'asc' ? 'desc' : 'asc'); + } else { + // Set new criteria and default to ascending + setSortBy(criteria); + setSortOrder('asc'); + } + }; + + const getSortIndicator = (criteria: 'name' | 'version' | 'update'): string => { + if (sortBy !== criteria) return ''; + return sortOrder === 'asc' ? ' ↑' : ' ↓'; + }; + + return ( +
+
+ setSearchTerm(e.target.value)} + className="search-input" + /> + {searchTerm && ( + + )} +
+ +
+
+ Sort by: + + + +
+ +
+ Showing {filteredPlugins.length} of {plugins.length} plugins +
+
+ +
+ {sortedPlugins.length > 0 ? ( + sortedPlugins.map(plugin => ( + + )) + ) : ( +
+

No plugins match your search.

+ +
+ )} +
+
+ ); +}; + +export default PluginList; \ No newline at end of file diff --git a/src/components/server/ScanProgress/ScanProgress.css b/src/components/server/ScanProgress/ScanProgress.css new file mode 100644 index 0000000..2e0c71e --- /dev/null +++ b/src/components/server/ScanProgress/ScanProgress.css @@ -0,0 +1,78 @@ +.scan-progress { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1.25rem; + background-color: var(--color-background); + border-radius: 6px; + border: 1px solid var(--color-border); + animation: fade-in 0.3s ease-out; +} + +.scan-progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.25rem; +} + +.scan-progress-title { + font-weight: 500; + color: var(--color-text); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.scan-progress-title::before { + content: ''; + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + background-color: var(--color-primary); + animation: pulse 1.5s infinite; +} + +.scan-progress-percentage { + font-weight: bold; + color: var(--color-primary); +} + +.scan-progress-details { + display: flex; + justify-content: space-between; + font-size: 0.85rem; + color: var(--color-text-muted); + margin-top: 0.25rem; +} + +.scan-progress-count { + white-space: nowrap; +} + +@keyframes pulse { + 0% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.6; + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/src/components/server/ScanProgress/ScanProgress.tsx b/src/components/server/ScanProgress/ScanProgress.tsx new file mode 100644 index 0000000..14a3b29 --- /dev/null +++ b/src/components/server/ScanProgress/ScanProgress.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import ProgressBar from '../../common/ProgressBar/ProgressBar'; +import './ScanProgress.css'; + +interface ScanProgressProps { + progress: { current: number; total: number } | null; +} + +export const ScanProgress: React.FC = ({ progress }) => { + // Add console logging for debugging + React.useEffect(() => { + console.log("ScanProgress component received progress:", progress); + }, [progress]); + + const calculatePercentage = (): number => { + if (!progress) return 0; + if (progress.total <= 0) return 0; // Handle case where total is 0 or negative + + // Make sure current is at least 0 to prevent NaN + const current = Math.max(0, progress.current); + return Math.min(Math.round((current / progress.total) * 100), 100); + }; + + const percentage = calculatePercentage(); + + return ( +
+
+ Scanning for plugins... + {percentage}% +
+ + + + {progress && ( +
+ + {progress.current} of {progress.total} files processed + +
+ )} +
+ ); +}; + +export default ScanProgress; \ No newline at end of file diff --git a/src/components/server/ServerInfo/ServerInfo.css b/src/components/server/ServerInfo/ServerInfo.css new file mode 100644 index 0000000..663b18e --- /dev/null +++ b/src/components/server/ServerInfo/ServerInfo.css @@ -0,0 +1,109 @@ +.server-info-container { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + background-color: var(--surface-color); + border-radius: 6px; + border: 1px solid var(--border-color); + margin-top: 1.5rem; +} + +.server-info-header { + display: flex; + align-items: center; + gap: 1rem; +} + +.server-icon { + font-size: 2rem; + background-color: var(--background-color); + width: 4rem; + height: 4rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + border: 1px solid var(--border-color); + flex-shrink: 0; +} + +.server-details { + flex: 1; +} + +.server-type-name { + font-size: 1.2rem; + margin: 0 0 0.5rem 0; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-color); +} + +.server-path { + margin: 0; + font-size: 0.9rem; + color: var(--text-secondary-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.server-path.clickable { + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + transition: color 0.2s; + width: fit-content; +} + +.server-path.clickable:hover { + color: var(--primary-color); + text-decoration: underline; +} + +.folder-icon { + font-size: 1rem; +} + +.server-stats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color); +} + +.stat-item { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.stat-label { + font-size: 0.8rem; + color: var(--text-secondary-color); + text-transform: uppercase; +} + +.stat-value { + font-size: 0.95rem; + font-weight: 500; + color: var(--text-color); +} + +.server-warning { + margin-top: 0.5rem; + padding: 0.75rem; + background-color: rgba(255, 152, 0, 0.1); + border-radius: 4px; + border-left: 4px solid var(--warning-color); +} + +.server-warning p { + margin: 0; + font-size: 0.9rem; + color: var(--warning-color); +} \ No newline at end of file diff --git a/src/components/server/ServerInfo/ServerInfo.tsx b/src/components/server/ServerInfo/ServerInfo.tsx new file mode 100644 index 0000000..fc4c663 --- /dev/null +++ b/src/components/server/ServerInfo/ServerInfo.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { ServerInfo as ServerInfoType } from '../../../types/server.types'; +import Badge from '../../common/Badge/Badge'; +import './ServerInfo.css'; +// Import only shell open as it's the primary method +import { open } from '@tauri-apps/plugin-shell'; +import { useServerContext } from '../../../context/ServerContext/useServerContext'; + +interface ServerInfoProps { + serverInfo: ServerInfoType; +} + +export const ServerInfo: React.FC = ({ serverInfo }) => { + const { serverPath } = useServerContext(); + + const getServerTypeIcon = () => { + switch (serverInfo.server_type.toLowerCase()) { + case 'paper': + return '📄'; + case 'spigot': + return '🔌'; + case 'bukkit': + return '🪣'; + case 'purpur': + return '🟣'; + case 'fabric': + return '🧵'; + case 'forge': + return '⚒️'; + default: + return '🖥️'; + } + }; + + const formatServerType = (type: string): string => { + return type.charAt(0).toUpperCase() + type.slice(1); + }; + + const getStatusColor = (): 'success' | 'warning' | 'error' => { + const version = serverInfo.minecraft_version; + if (!version) return 'warning'; + + // Parse version numbers like 1.19.2 + const parts = version.split('.'); + const major = parseInt(parts[0] || '0'); + const minor = parseInt(parts[1] || '0'); + + // Check if using recent version (arbitrary definition) + if (major >= 1 && minor >= 18) { + return 'success'; + } else if (major >= 1 && minor >= 16) { + return 'warning'; + } else { + return 'error'; + } + }; + + // Function to open the server directory in file explorer + const handleOpenDirectory = async () => { + try { + console.log('Attempting to open directory using shell.open:', serverPath); + // Use shell.open directly - it's generally more robust, especially for UNC + await open(serverPath); + console.log('Successfully opened directory via shell.open'); + } catch (error) { + console.error('Failed to open directory:', error); + // Optionally add user feedback here (e.g., toast notification) + } + }; + + return ( +
+
+
{getServerTypeIcon()}
+
+

+ {formatServerType(serverInfo.server_type)} + +

+

+ Server directory 📂 +

+
+
+ +
+
+ Plugins Directory + {serverInfo.plugins_directory} +
+ +
+ Plugins Found + {serverInfo.plugins_count} +
+
+
+ ); +}; + +export default ServerInfo; \ No newline at end of file diff --git a/src/components/server/ServerSelector/ServerSelector.css b/src/components/server/ServerSelector/ServerSelector.css new file mode 100644 index 0000000..42b1500 --- /dev/null +++ b/src/components/server/ServerSelector/ServerSelector.css @@ -0,0 +1,32 @@ +.server-selector { + display: flex; + flex-direction: column; + gap: 1.5rem; + width: 100%; + max-width: 800px; + margin: 0 auto; + padding: 1.5rem; + background-color: var(--surface-color); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border: 1px solid var(--border-color); +} + +.server-actions { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.server-selector-error { + padding: 0.75rem 1rem; + background-color: rgba(244, 67, 54, 0.1); + border-radius: 4px; + border-left: 4px solid var(--error-color); +} + +.error-message { + color: var(--error-color); + margin: 0; + font-size: 0.9rem; +} \ No newline at end of file diff --git a/src/components/server/ServerSelector/ServerSelector.tsx b/src/components/server/ServerSelector/ServerSelector.tsx new file mode 100644 index 0000000..a7cf141 --- /dev/null +++ b/src/components/server/ServerSelector/ServerSelector.tsx @@ -0,0 +1,98 @@ +import React, { useEffect } from 'react'; +import { useServerActions } from '../../../hooks/useServerActions'; +import { usePluginContext } from '../../../context/PluginContext/usePluginContext'; +import Button from '../../common/Button/Button'; +import ScanProgress from '../ScanProgress/ScanProgress'; +import './ServerSelector.css'; + +export const ServerSelector: React.FC = () => { + const { + serverPath, + isScanning, + scanComplete, + scanProgress, + error, + selectDirectory, + scanForPlugins, + lastScanResult + } = useServerActions(); + + const { setPlugins } = usePluginContext(); + + // Effect to handle scan results + useEffect(() => { + if (lastScanResult && lastScanResult.plugins) { + console.log("Setting plugins from lastScanResult in ServerSelector"); + setPlugins(lastScanResult.plugins); + } + }, [lastScanResult, setPlugins]); + + const handleSelectDirectory = async () => { + await selectDirectory(); + }; + + const handleScanForPlugins = async () => { + await scanForPlugins(); + }; + + const handleResetSelection = () => { + // Clear server path and plugins + setPlugins([]); + }; + + return ( +
+
+ + + {serverPath && !isScanning && ( + + )} + + {serverPath && scanComplete && ( + + )} +
+ + {isScanning && ( + <> + {/* Add debug info */} +
+
Debug - scanProgress: {JSON.stringify(scanProgress, null, 2)}
+
+ + + )} + + {error && ( +
+

Error: {error}

+
+ )} +
+ ); +}; + +export default ServerSelector; \ No newline at end of file diff --git a/src/components/updates/BulkUpdateProgress/BulkUpdateProgress.css b/src/components/updates/BulkUpdateProgress/BulkUpdateProgress.css new file mode 100644 index 0000000..a62f175 --- /dev/null +++ b/src/components/updates/BulkUpdateProgress/BulkUpdateProgress.css @@ -0,0 +1,87 @@ +.bulk-update-progress { + background-color: var(--color-background); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 0.75rem 1rem; + margin-top: 0.5rem; + width: 100%; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); + animation: slide-in 0.3s ease-out; +} + +.bulk-update-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.bulk-update-title { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--color-text); + display: flex; + align-items: center; +} + +.bulk-update-title::before { + content: ''; + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + background-color: var(--color-primary); + margin-right: 0.5rem; + animation: pulse 1.5s infinite; +} + +.bulk-update-count { + font-size: 0.9rem; + color: var(--color-text-muted); + font-weight: 500; +} + +.bulk-update-details { + margin-top: 0.75rem; + font-size: 0.9rem; + color: var(--color-text-muted); +} + +.current-plugin { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +.current-plugin strong { + color: var(--color-text); +} + +@keyframes pulse { + 0% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.6; + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes slide-in { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/src/components/updates/BulkUpdateProgress/BulkUpdateProgress.tsx b/src/components/updates/BulkUpdateProgress/BulkUpdateProgress.tsx new file mode 100644 index 0000000..fb8f8d9 --- /dev/null +++ b/src/components/updates/BulkUpdateProgress/BulkUpdateProgress.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { usePluginContext } from '../../../context/PluginContext/usePluginContext'; +import ProgressBar from '../../common/ProgressBar/ProgressBar'; +import './BulkUpdateProgress.css'; + +/** + * Component that displays the progress of bulk plugin updates + */ +export const BulkUpdateProgress: React.FC = () => { + const { bulkUpdateProgress } = usePluginContext(); + + // If no bulk update is in progress, don't render anything + if (!bulkUpdateProgress) { + return null; + } + + const progressPercentage = bulkUpdateProgress.total > 0 + ? Math.round((bulkUpdateProgress.processed / bulkUpdateProgress.total) * 100) + : 0; + + return ( +
+
+

+ Checking for Updates +

+ + {bulkUpdateProgress.processed} of {bulkUpdateProgress.total} + +
+ + + +
+ + Processing: {bulkUpdateProgress.current_plugin_name} + +
+
+ ); +}; + +export default BulkUpdateProgress; \ No newline at end of file diff --git a/src/components/updates/CompatibilityCheckDialog/CompatibilityCheckDialog.css b/src/components/updates/CompatibilityCheckDialog/CompatibilityCheckDialog.css new file mode 100644 index 0000000..fcde578 --- /dev/null +++ b/src/components/updates/CompatibilityCheckDialog/CompatibilityCheckDialog.css @@ -0,0 +1,81 @@ +.compatibility-check-content { + padding: 1rem 0; +} + +.compatibility-status { + margin-bottom: 1.5rem; +} + +.compatibility-supported, +.compatibility-warning { + display: flex; + align-items: flex-start; + padding: 1rem; + border-radius: 6px; + margin-bottom: 1rem; +} + +.compatibility-supported { + background-color: rgba(76, 175, 80, 0.1); + border: 1px solid rgba(76, 175, 80, 0.3); +} + +.compatibility-warning { + background-color: rgba(255, 152, 0, 0.1); + border: 1px solid rgba(255, 152, 0, 0.3); +} + +.status-icon { + font-size: 1.25rem; + margin-right: 0.75rem; + line-height: 1; +} + +.compatibility-status p { + margin: 0; + line-height: 1.5; +} + +.plugin-details { + margin-bottom: 1.5rem; +} + +.plugin-details h4 { + margin-top: 0; + margin-bottom: 0.75rem; + font-size: 1rem; +} + +.plugin-details ul { + margin: 0; + padding-left: 1.5rem; +} + +.plugin-details li { + margin-bottom: 0.5rem; +} + +.risk-acknowledgment { + margin-bottom: 1.5rem; +} + +.checkbox-container { + display: flex; + align-items: center; + cursor: pointer; +} + +.checkbox-container input[type="checkbox"] { + margin-right: 0.5rem; +} + +.checkbox-label { + font-weight: 500; +} + +.dialog-actions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1.5rem; +} \ No newline at end of file diff --git a/src/components/updates/CompatibilityCheckDialog/CompatibilityCheckDialog.tsx b/src/components/updates/CompatibilityCheckDialog/CompatibilityCheckDialog.tsx new file mode 100644 index 0000000..e140adc --- /dev/null +++ b/src/components/updates/CompatibilityCheckDialog/CompatibilityCheckDialog.tsx @@ -0,0 +1,112 @@ +import React, { useState } from 'react'; +import Modal from '../../common/Modal/Modal'; +import Button from '../../common/Button/Button'; +import './CompatibilityCheckDialog.css'; +import { useServerContext } from '../../../context/ServerContext/useServerContext'; +import { Plugin } from '../../../types/plugin.types'; + +interface CompatibilityCheckDialogProps { + isOpen: boolean; + onClose: () => void; + plugin: Plugin; + onConfirmUpdate: () => void; +} + +/** + * A dialog that shows compatibility warnings before updating a plugin + */ +export const CompatibilityCheckDialog: React.FC = ({ + isOpen, + onClose, + plugin, + onConfirmUpdate +}) => { + const { serverInfo } = useServerContext(); + const [acknowledgedRisk, setAcknowledgedRisk] = useState(false); + + // Determine if the plugin explicitly supports this server version + const serverVersion = serverInfo?.minecraft_version || ''; + const supportsVersion = plugin.platform_compatibility?.includes(serverVersion) || false; + + const handleConfirm = () => { + onConfirmUpdate(); + onClose(); + }; + + return ( + +
+
+ {supportsVersion ? ( +
+ +

+ This plugin update is marked as compatible with your server version ({serverVersion}). + It should work correctly after updating. +

+
+ ) : ( +
+ ⚠️ +

+ This plugin update is not explicitly marked as compatible with your server version ({serverVersion}). + Updating may cause compatibility issues or server errors. +

+
+ )} +
+ +
+

Plugin Details

+
    +
  • Name: {plugin.name}
  • +
  • Current Version: {plugin.version}
  • +
  • Latest Version: {plugin.latest_version}
  • + {plugin.platform_compatibility && ( +
  • + Supported Versions: {plugin.platform_compatibility.join(', ')} +
  • + )} +
+
+ + {!supportsVersion && ( +
+ +
+ )} + +
+ + +
+
+
+ ); +}; + +export default CompatibilityCheckDialog; \ No newline at end of file diff --git a/src/components/updates/DownloadProgressIndicator/DownloadProgressIndicator.css b/src/components/updates/DownloadProgressIndicator/DownloadProgressIndicator.css new file mode 100644 index 0000000..f676df0 --- /dev/null +++ b/src/components/updates/DownloadProgressIndicator/DownloadProgressIndicator.css @@ -0,0 +1,106 @@ +.download-progress-indicator { + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; + background-color: var(--color-background); + border: 1px solid var(--color-border); + transition: all 0.3s ease; +} + +.download-progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.download-progress-plugin-info { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.status-icon { + font-size: 1rem; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.plugin-name { + font-weight: 600; + color: var(--color-text); +} + +.plugin-version { + font-size: 0.85rem; + color: var(--color-text-muted); +} + +.download-status { + font-size: 0.85rem; + font-weight: 500; + padding: 0.25rem 0.5rem; + border-radius: 12px; + background-color: var(--color-background-light); +} + +.download-message { + margin-top: 0.75rem; + font-size: 0.85rem; + color: var(--color-text-muted); + padding: 0.5rem; + background-color: var(--color-background-light); + border-radius: 4px; + word-break: break-word; +} + +/* Status-specific styles */ +.download-status-downloading .status-icon { + color: var(--color-primary); + animation: pulse 1.5s infinite; +} + +.download-status-extracting .status-icon, +.download-status-installing .status-icon { + color: var(--color-primary); + animation: spin 2s linear infinite; +} + +.download-status-completed { + border-color: var(--color-success); +} + +.download-status-completed .status-icon { + color: var(--color-success); +} + +.download-status-error { + border-color: var(--color-error); +} + +.download-status-error .status-icon, +.download-status-error .download-message { + color: var(--color-error); +} + +@keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.6; + } + 100% { + opacity: 1; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/components/updates/DownloadProgressIndicator/DownloadProgressIndicator.tsx b/src/components/updates/DownloadProgressIndicator/DownloadProgressIndicator.tsx new file mode 100644 index 0000000..5766b1b --- /dev/null +++ b/src/components/updates/DownloadProgressIndicator/DownloadProgressIndicator.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import ProgressBar from '../../common/ProgressBar/ProgressBar'; +import { DownloadProgress } from '../../../types/events.types'; +import './DownloadProgressIndicator.css'; + +interface DownloadProgressIndicatorProps { + downloadProgress: DownloadProgress; +} + +/** + * Component that displays the progress of a plugin download + */ +export const DownloadProgressIndicator: React.FC = ({ + downloadProgress +}) => { + const { pluginName, version, percentage, status, message } = downloadProgress; + + const getStatusText = () => { + switch (status) { + case 'downloading': + return 'Downloading'; + case 'extracting': + return 'Extracting'; + case 'installing': + return 'Installing'; + case 'completed': + return 'Completed'; + case 'error': + return 'Error'; + default: + return 'Processing'; + } + }; + + const getStatusIcon = () => { + switch (status) { + case 'downloading': + return '↓'; + case 'extracting': + return '📦'; + case 'installing': + return '⚙️'; + case 'completed': + return '✓'; + case 'error': + return '⚠️'; + default: + return '•'; + } + }; + + const getStatusColor = () => { + switch (status) { + case 'completed': + return 'success'; + case 'error': + return 'error'; + default: + return 'primary'; + } + }; + + return ( +
+
+
+ {getStatusIcon()} + {pluginName} + v{version} +
+
+ {getStatusText()} +
+
+ + + + {message && ( +
+ {message} +
+ )} +
+ ); +}; + +export default DownloadProgressIndicator; \ No newline at end of file diff --git a/src/components/updates/PluginMatchSelector/PluginMatchSelector.css b/src/components/updates/PluginMatchSelector/PluginMatchSelector.css new file mode 100644 index 0000000..99a95bc --- /dev/null +++ b/src/components/updates/PluginMatchSelector/PluginMatchSelector.css @@ -0,0 +1,132 @@ +.plugin-match-selector { + padding: 0.5rem 0; +} + +.plugin-match-header { + margin-bottom: 1.25rem; +} + +.plugin-match-header p { + margin: 0; + line-height: 1.5; + font-size: 0.95rem; +} + +.plugin-matches-list { + max-height: 400px; + overflow-y: auto; + margin-bottom: 1.5rem; + border: 1px solid var(--color-border); + border-radius: 6px; +} + +.plugin-match-item { + display: flex; + padding: 1rem; + border-bottom: 1px solid var(--color-border); + cursor: pointer; + transition: background-color 0.2s; + position: relative; +} + +.plugin-match-item:last-child { + border-bottom: none; +} + +.plugin-match-item:hover { + background-color: var(--color-background-hover); +} + +.plugin-match-item.selected { + background-color: var(--color-background-active); + border-left: 3px solid var(--color-primary); +} + +.plugin-match-info { + flex: 0 0 25%; + padding-right: 1rem; +} + +.plugin-match-name { + margin: 0 0 0.25rem 0; + font-size: 1rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.plugin-match-version, +.plugin-match-source { + display: block; + font-size: 0.85rem; + color: var(--color-text-muted); +} + +.plugin-match-details { + flex: 1; + min-width: 0; +} + +.plugin-match-description { + margin: 0 0 0.5rem 0; + font-size: 0.9rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.plugin-match-metadata { + display: flex; + flex-wrap: wrap; + gap: 1rem; + font-size: 0.8rem; +} + +.plugin-match-mc-versions, +.plugin-match-downloads { + color: var(--color-text-muted); +} + +.plugin-match-selection { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 40px; +} + +.plugin-match-radio { + height: 20px; + width: 20px; +} + +.plugin-match-radio input { + width: 20px; + height: 20px; + cursor: pointer; +} + +.plugin-match-actions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; +} + +@media (max-width: 768px) { + .plugin-match-item { + flex-direction: column; + } + + .plugin-match-info { + flex: 1 1 auto; + padding-right: 0; + margin-bottom: 0.5rem; + } + + .plugin-match-selection { + position: absolute; + top: 1rem; + right: 1rem; + } +} \ No newline at end of file diff --git a/src/components/updates/PluginMatchSelector/PluginMatchSelector.tsx b/src/components/updates/PluginMatchSelector/PluginMatchSelector.tsx new file mode 100644 index 0000000..924e882 --- /dev/null +++ b/src/components/updates/PluginMatchSelector/PluginMatchSelector.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import Modal from '../../common/Modal/Modal'; +import Button from '../../common/Button/Button'; +import { PotentialPluginMatch } from '../../../types/plugin.types'; +import './PluginMatchSelector.css'; + +interface PluginMatchSelectorProps { + isOpen: boolean; + onClose: () => void; + pluginName: string; + potentialMatches: PotentialPluginMatch[]; + onSelectMatch: (match: PotentialPluginMatch) => void; +} + +/** + * Component that allows users to select from potential plugin matches when the exact match isn't found + */ +export const PluginMatchSelector: React.FC = ({ + isOpen, + onClose, + pluginName, + potentialMatches, + onSelectMatch +}) => { + const [selectedMatchIndex, setSelectedMatchIndex] = useState(null); + + const handleSelectMatch = () => { + if (selectedMatchIndex !== null) { + onSelectMatch(potentialMatches[selectedMatchIndex]); + onClose(); + } + }; + + return ( + +
+
+

+ We found multiple potential matches for {pluginName}. + Please select the correct plugin from the list below: +

+
+ +
+ {potentialMatches.map((match, index) => ( +
setSelectedMatchIndex(index)} + > +
+

{match.name}

+ v{match.version} + {match.repository} +
+ +
+ {match.description && ( +

{match.description}

+ )} +
+ + MC: {match.minecraft_versions.join(', ')} + + {match.download_count !== undefined && ( + + {new Intl.NumberFormat().format(match.download_count)} downloads + + )} +
+
+ +
+
+ setSelectedMatchIndex(index)} + /> +
+
+
+ ))} +
+ +
+ + +
+
+
+ ); +}; + +export default PluginMatchSelector; \ No newline at end of file diff --git a/src/components/updates/PremiumPluginModal/PremiumPluginModal.css b/src/components/updates/PremiumPluginModal/PremiumPluginModal.css new file mode 100644 index 0000000..7296140 --- /dev/null +++ b/src/components/updates/PremiumPluginModal/PremiumPluginModal.css @@ -0,0 +1,53 @@ +.premium-plugin-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 1.5rem 0.5rem; + text-align: center; +} + +.premium-plugin-icon { + color: #ffc107; + margin-bottom: 1.25rem; + animation: float 3s ease-in-out infinite; +} + +.premium-plugin-message { + margin-bottom: 1.5rem; +} + +.premium-plugin-message h3 { + margin-top: 0; + margin-bottom: 1rem; + font-size: 1.25rem; + color: var(--color-text); +} + +.premium-plugin-message p { + margin: 0.5rem 0; + line-height: 1.5; + color: var(--color-text-muted); +} + +.premium-plugin-message strong { + color: var(--color-text); +} + +.premium-plugin-actions { + display: flex; + gap: 0.75rem; + justify-content: center; + margin-top: 1rem; +} + +@keyframes float { + 0% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } + 100% { + transform: translateY(0px); + } +} \ No newline at end of file diff --git a/src/components/updates/PremiumPluginModal/PremiumPluginModal.tsx b/src/components/updates/PremiumPluginModal/PremiumPluginModal.tsx new file mode 100644 index 0000000..2748653 --- /dev/null +++ b/src/components/updates/PremiumPluginModal/PremiumPluginModal.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import Modal from '../../common/Modal/Modal'; +import Button from '../../common/Button/Button'; +import './PremiumPluginModal.css'; +import { PremiumPluginInfo } from '../../../types/events.types'; +import { invoke } from '@tauri-apps/api/core'; + +interface PremiumPluginModalProps { + isOpen: boolean; + onClose: () => void; + pluginInfo: PremiumPluginInfo; +} + +/** + * Modal that displays information about a premium plugin that requires manual download + */ +export const PremiumPluginModal: React.FC = ({ + isOpen, + onClose, + pluginInfo +}) => { + const handleOpenUrl = async () => { + try { + // Use Tauri invoke to call Rust command to open the URL + await invoke('plugin:shell|open', { url: pluginInfo.url }); + } catch (error) { + console.error('Failed to open URL:', error); + // Fallback to standard window.open if invoke fails + window.open(pluginInfo.url, '_blank'); + } + }; + + return ( + +
+
+ + + +
+ +
+

Premium Plugin Detected

+

+ {pluginInfo.name} (version {pluginInfo.version}) is a premium plugin + that requires manual download from the official source. +

+

+ Click the button below to visit the plugin's website where you can purchase or + download the update if you already own it. +

+
+ +
+ + +
+
+
+ ); +}; + +export default PremiumPluginModal; \ No newline at end of file diff --git a/src/components/updates/UpdateControls/UpdateControls.css b/src/components/updates/UpdateControls/UpdateControls.css new file mode 100644 index 0000000..00803ad --- /dev/null +++ b/src/components/updates/UpdateControls/UpdateControls.css @@ -0,0 +1,114 @@ +.update-controls { + display: flex; + justify-content: space-between; + align-items: center; + background-color: var(--surface-color); + border-radius: 8px; + border: 1px solid var(--border-color); + padding: 1rem 1.5rem; + margin-bottom: 1.5rem; + width: 100%; + box-sizing: border-box; +} + +.update-status { + display: flex; + flex-direction: column; + gap: 0.75rem; + min-width: 200px; +} + +.checking-status { + display: flex; + align-items: center; + gap: 0.75rem; + color: var(--text-secondary-color); + font-size: 0.9rem; +} + +.spinner.small { + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.updates-available { + color: var(--warning-color); + font-weight: 500; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.update-count { + display: inline-flex; + align-items: center; + justify-content: center; + background-color: var(--warning-color); + color: white; + font-weight: bold; + width: 24px; + height: 24px; + border-radius: 50%; + font-size: 0.8rem; +} + +.no-updates { + color: var(--success-color); + font-weight: 500; +} + +.no-updates-checked { + color: var(--text-secondary-color); + font-style: italic; + font-size: 0.9rem; +} + +.app-update-available { + display: flex; + align-items: center; + gap: 1rem; + font-size: 0.9rem; + padding: 0.5rem 0.75rem; + background-color: rgba(76, 175, 80, 0.1); + border-radius: 4px; + color: var(--success-color); +} + +.update-actions { + display: flex; + gap: 0.75rem; + flex-shrink: 0; +} + +.check-icon, .update-icon { + font-size: 1rem; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@media (max-width: 768px) { + .update-controls { + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + + .update-actions { + width: 100%; + } + + .update-actions button { + flex: 1; + } +} \ No newline at end of file diff --git a/src/components/updates/UpdateControls/UpdateControls.tsx b/src/components/updates/UpdateControls/UpdateControls.tsx new file mode 100644 index 0000000..dbff324 --- /dev/null +++ b/src/components/updates/UpdateControls/UpdateControls.tsx @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; +import { useUpdateActions } from '../../../hooks/useUpdateActions'; +import { usePluginActions } from '../../../hooks/usePluginActions'; +import Button from '../../common/Button/Button'; +import './UpdateControls.css'; + +export const UpdateControls: React.FC = () => { + const { + updateAllPlugins, + checkForAllUpdates, + bulkUpdateProgress + } = useUpdateActions(); + + const { + isCheckingUpdates, + getOutdatedPluginsCount, + plugins + } = usePluginActions(); + + // Local loading state for the "Update All" button + const [isUpdatingAll, setIsUpdatingAll] = useState(false); + + // Calculate how many plugins need updates + const outdatedCount = getOutdatedPluginsCount(); + const hasUpdates = outdatedCount > 0; + + // Track if updates have been checked (any plugin has has_update property set) + const updatesHaveBeenChecked = plugins.some(plugin => plugin.has_update !== undefined); + + // Determine overall loading state + const isLoading = isCheckingUpdates || isUpdatingAll; + + const handleCheckForUpdates = async () => { + if (isLoading) return; + + try { + console.log("Starting plugin update check"); + await checkForAllUpdates(); + } catch (error) { + console.error("Error checking for updates:", error); + } + }; + + const handleUpdateAll = async () => { + if (isLoading || !hasUpdates) return; + + try { + setIsUpdatingAll(true); + await updateAllPlugins(); + } catch (error) { + console.error("Error updating all plugins:", error); + } finally { + setIsUpdatingAll(false); + } + }; + + return ( +
+
+ {isCheckingUpdates ? ( +
+ + Checking for plugin updates{bulkUpdateProgress ? ` (${bulkUpdateProgress.processed}/${bulkUpdateProgress.total})` : '...'} +
+ ) : hasUpdates ? ( +
+ {outdatedCount} plugin update{outdatedCount !== 1 ? 's' : ''} available +
+ ) : updatesHaveBeenChecked ? ( +
All plugins are up to date
+ ) : ( +
Click "Check for Updates" to check plugin versions
+ )} +
+ +
+ + + {hasUpdates && ( + + )} +
+
+ ); +}; + +export default UpdateControls; \ No newline at end of file diff --git a/src/components/updates/WarningModal/WarningModal.css b/src/components/updates/WarningModal/WarningModal.css new file mode 100644 index 0000000..a6155c1 --- /dev/null +++ b/src/components/updates/WarningModal/WarningModal.css @@ -0,0 +1,62 @@ +.warning-modal { + display: flex; + flex-direction: column; + align-items: center; + padding: 1.5rem; + text-align: center; +} + +.warning-modal-icon { + margin-bottom: 1.5rem; +} + +.warning-modal-warning .warning-modal-icon { + color: #ff9800; /* warning color */ +} + +.warning-modal-error .warning-modal-icon { + color: #f44336; /* error color */ +} + +.warning-modal-info .warning-modal-icon { + color: #2196f3; /* info color */ +} + +.warning-modal-content { + margin-bottom: 1.5rem; + width: 100%; +} + +.warning-modal-message { + margin: 0; + line-height: 1.6; + font-size: 1rem; + white-space: pre-line; /* Respects line breaks in the message */ +} + +.warning-modal-actions { + display: flex; + gap: 0.75rem; + justify-content: center; + margin-top: 0.5rem; +} + +/* Add a subtle shake animation for error warnings */ +.warning-modal-error { + animation: shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; +} + +@keyframes shake { + 10%, 90% { + transform: translateX(-1px); + } + 20%, 80% { + transform: translateX(2px); + } + 30%, 50%, 70% { + transform: translateX(-4px); + } + 40%, 60% { + transform: translateX(4px); + } +} \ No newline at end of file diff --git a/src/components/updates/WarningModal/WarningModal.tsx b/src/components/updates/WarningModal/WarningModal.tsx new file mode 100644 index 0000000..31901f7 --- /dev/null +++ b/src/components/updates/WarningModal/WarningModal.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import Modal from '../../common/Modal/Modal'; +import Button from '../../common/Button/Button'; +import './WarningModal.css'; + +interface WarningModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + message: string; + confirmLabel?: string; + cancelLabel?: string; + onConfirm?: () => void; + variant?: 'warning' | 'error' | 'info'; +} + +/** + * A modal component for displaying important warnings to users + */ +export const WarningModal: React.FC = ({ + isOpen, + onClose, + title, + message, + confirmLabel = 'Confirm', + cancelLabel = 'Cancel', + onConfirm, + variant = 'warning' +}) => { + const handleConfirm = () => { + if (onConfirm) { + onConfirm(); + } + onClose(); + }; + + // Choose icon based on variant + const renderIcon = () => { + switch (variant) { + case 'error': + return ( + + + + ); + case 'info': + return ( + + + + ); + case 'warning': + default: + return ( + + + + ); + } + }; + + return ( + +
+
+ {renderIcon()} +
+ +
+

{message}

+
+ +
+ {onConfirm ? ( + <> + + + + ) : ( + + )} +
+
+
+ ); +}; + +export default WarningModal; \ No newline at end of file diff --git a/src/context/PluginContext/PluginContext.tsx b/src/context/PluginContext/PluginContext.tsx new file mode 100644 index 0000000..485508a --- /dev/null +++ b/src/context/PluginContext/PluginContext.tsx @@ -0,0 +1,504 @@ +import React, { createContext, useState, useCallback, ReactNode, useEffect } from 'react'; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { Plugin, PotentialPluginMatch } from '../../types/plugin.types'; +import { BulkUpdateProgressPayload, SingleUpdateResultPayload } from '../../types/events.types'; +import { ScanResult } from '../../types/server.types'; +import { canUpdatePlugin } from '../../utils/validators'; +import { createUpdateMessage } from '../../utils/formatters'; +import { ServerType } from '../../types/server.types'; +import { useServerContext } from '../ServerContext/useServerContext'; + +interface PluginContextProps { + /** + * List of plugins installed on the server + */ + plugins: Plugin[]; + + /** + * Currently selected plugin (for details view) + */ + selectedPlugin: Plugin | null; + + /** + * Whether plugin updates are being checked + */ + isCheckingUpdates: boolean; + + /** + * Error message specific to plugin operations + */ + updateError: string | null; + + /** + * Loading states for individual plugins (keyed by file_path) + */ + pluginLoadingStates: Record; + + /** + * Progress information for bulk update checks + */ + bulkUpdateProgress: BulkUpdateProgressPayload | null; + + /** + * Whether a single plugin update check is in progress + */ + isCheckingSinglePlugin: boolean; + + /** + * Function to check for updates for all plugins + */ + checkForUpdates: (serverType?: ServerType) => Promise; + + /** + * Function to check for updates for a single plugin + */ + checkSinglePlugin: (plugin: Plugin) => Promise; + + /** + * Function to update a plugin to the latest version + */ + updatePlugin: (plugin: Plugin) => Promise; + + /** + * Function to select a plugin for viewing details + */ + showPluginDetails: (plugin: Plugin) => void; + + /** + * Function to close the plugin details view + */ + closePluginDetails: () => void; + + /** + * Function to set the plugins array directly + */ + setPlugins: (plugins: Plugin[]) => void; + + /** + * Function to clear update errors + */ + clearUpdateError: () => void; +} + +// Create the context with default values +export const PluginContext = createContext({} as PluginContextProps); + +interface PluginProviderProps { + children: ReactNode; +} + +/** + * Provider component for managing plugin-related state + */ +export const PluginProvider: React.FC = ({ + children +}) => { + // Get server context directly + const { serverPath, serverInfo } = useServerContext(); + const serverType = serverInfo?.server_type; + + const [plugins, setPluginsState] = useState([]); + const [selectedPlugin, setSelectedPlugin] = useState(null); + const [isCheckingUpdates, setIsCheckingUpdates] = useState(false); + const [updateError, setUpdateError] = useState(null); + const [pluginLoadingStates, setPluginLoadingStates] = useState>({}); + const [bulkUpdateProgress, setBulkUpdateProgress] = useState(null); + const [isCheckingSinglePlugin, setIsCheckingSinglePlugin] = useState(false); + + // Setup event listeners + useEffect(() => { + const unlisteners: (() => void)[] = []; + + // Listen for scan-completed events to update plugins + listen('scan_completed', (event) => { + console.log("Received scan_completed event in PluginContext:", event.payload); + const result = event.payload as ScanResult; + + // Update the plugins state with the scanned plugins + setPluginsState(result.plugins); + console.log(`Updated plugins state with ${result.plugins.length} plugins`); + }).then(unlisten => unlisteners.push(unlisten)); + + // Listen for update check progress + listen("update_check_progress", (event) => { + console.log("Update check progress event received:", event.payload); + setBulkUpdateProgress(event.payload); + }).then(unlisten => unlisteners.push(unlisten)); + + // Listen for single update check completed + listen("single_update_check_completed", (eventData) => { + const { original_file_path, plugin: updatedPlugin, error } = eventData.payload; + console.log("Single update check completed event received:", original_file_path); + + if (error) { + console.error("Error checking plugin for updates:", error); + setUpdateError(error); + } + + if (updatedPlugin) { + console.log("Plugin update check result:", updatedPlugin); + + // Update the plugin in the list + setPluginsState(currentPlugins => { + return currentPlugins.map(p => { + if (p.file_path === original_file_path) { + return updatedPlugin; + } + return p; + }); + }); + } + + // Clear loading state for this plugin - IMPORTANT: always clear the loading state + console.log(`Clearing loading state for plugin path: ${original_file_path}`); + setPluginLoadingStates(prev => { + const newState = { ...prev }; + delete newState[original_file_path]; + return newState; + }); + + // Also notify the UI that the check is complete + const customEvent = new CustomEvent('single_plugin_check_completed', { + detail: { + plugin_path: original_file_path, + success: !error, + error: error + } + }); + window.dispatchEvent(customEvent); + }).then(unlisten => unlisteners.push(unlisten)); + + // Listen for update check complete + listen("update_check_complete", (event) => { + console.log("Update check complete event received:", event); + + // Optionally handle any additional logic needed when bulk update is complete + }).then(unlisten => unlisteners.push(unlisten)); + + // Cleanup function + return () => { + unlisteners.forEach(unlisten => unlisten()); + }; + }, []); + + // Set plugins directly + const setPlugins = useCallback((newPlugins: Plugin[]) => { + setPluginsState(newPlugins); + }, []); + + // Clear update error + const clearUpdateError = useCallback(() => { + setUpdateError(null); + }, []); + + // Log when serverPath changes + useEffect(() => { + console.log(`PluginContext: serverPath changed to ${serverPath}`); + }, [serverPath]); + + // Check for updates for all plugins + const checkForUpdates = useCallback(async (currentServerType?: ServerType) => { + const currentServerPath = serverPath; + console.log(`checkForUpdates called with serverPath: ${currentServerPath}`); + + if (!plugins.length) { + console.error('No plugins to check for updates'); + setUpdateError('No plugins to check for updates'); + return; + } + + if (isCheckingUpdates) { + console.warn('Update check already in progress'); + return; + } + + if (!currentServerPath) { + console.error('No server path available in PluginContext'); + setUpdateError('No server path available'); + return; + } + + console.log(`Starting update check with serverPath: ${currentServerPath}`); + console.log(`Total plugins to check: ${plugins.length}`); + console.log(`Server type: ${currentServerType || serverType || 'Unknown'}`); + + setIsCheckingUpdates(true); + setUpdateError(null); + setBulkUpdateProgress(null); + console.log("Invoking bulk check_plugin_updates..."); + + try { + // Include all repositories to check + const repositoriesToCheck = ['hangarmc', 'spigotmc', 'modrinth', 'github']; + + // Prepare plugins data with correct structure + const pluginsToSend = plugins.map(p => ({ + name: p.name, + version: p.version, + authors: p.authors || [], + file_path: p.file_path, + file_hash: p.file_hash, + website: p.website, + description: p.description, + api_version: p.api_version, + main_class: p.main_class, + depend: p.depend, + soft_depend: p.soft_depend, + load_before: p.load_before, + commands: p.commands, + permissions: p.permissions, + has_update: p.has_update || false, + repository_source: p.repository_source, + repository_id: p.repository_id, + repository_url: p.repository_url, + })); + + console.log("Sending plugin data to backend, count:", pluginsToSend.length); + console.log("Using repositories:", repositoriesToCheck); + console.log("Sample plugin data:", pluginsToSend[0]); + + const updatedPlugins = await invoke("check_plugin_updates", { + plugins: pluginsToSend, + repositories: repositoriesToCheck, + }); + + console.log("Bulk update check completed successfully, updating state."); + console.log(`Received ${updatedPlugins.length} updated plugins from backend`); + + // Ensure up-to-date plugins have a latest_version + const processedPlugins = updatedPlugins.map(plugin => { + if (!plugin.latest_version && !plugin.has_update) { + return { + ...plugin, + latest_version: plugin.version + }; + } + return plugin; + }); + + setPlugins(processedPlugins); + + // Emit an update check complete event if not emitted by backend + let updatedCount = processedPlugins.filter(p => p.has_update).length; + console.log(`Update check complete: ${updatedCount} plugins need updates`); + + // Send a custom update_check_complete event to ensure the UI is updated + const event = new CustomEvent('update_check_complete', { + detail: { + outdated_count: updatedCount, + total_checked: processedPlugins.length, + success: true + } + }); + window.dispatchEvent(event); + + if (currentServerPath) { + try { + console.log("[checkForUpdates] Saving plugin data..."); + await invoke("save_plugin_data", { plugins: processedPlugins, serverPath: currentServerPath }); + console.log("[checkForUpdates] Plugin data saved successfully."); + } catch (saveError) { + console.error("Error saving plugin data after bulk update:", saveError); + setUpdateError(`Update check complete, but failed to save plugin data: ${saveError}`); + } + } + } catch (error) { + console.error("Error checking for updates:", error); + setUpdateError(`Failed to check for updates: ${error}`); + } finally { + setIsCheckingUpdates(false); + setBulkUpdateProgress(null); + } + }, [plugins, isCheckingUpdates, serverPath, setPlugins, serverType]); + + // Check for updates for a single plugin + const checkSinglePlugin = useCallback(async (plugin: Plugin) => { + const currentServerPath = serverPath; + console.log(`checkSinglePlugin called with serverPath: ${currentServerPath}`); + + if (!currentServerPath) { + console.error('No server path available for checking single plugin'); + setUpdateError('No server path available'); + return; + } + + setIsCheckingSinglePlugin(true); + setUpdateError(null); + setPluginLoadingStates(prev => ({ + ...prev, + [plugin.file_path]: true + })); + + try { + console.log(`Checking for updates for plugin: ${plugin.name}`); + + // Use all lowercase repository names to match backend expectations + const repositories = ["hangarmc", "spigotmc", "modrinth", "github"]; + + console.log(`Repositories to check: ${repositories.join(', ')}`); + + await invoke("check_single_plugin_update_command", { + plugin: plugin, + repositories: repositories + }); + + console.log(`Single plugin update check initiated for ${plugin.name}`); + } catch (err) { + console.error(`Error checking plugin ${plugin.name} for updates:`, err); + setUpdateError(`Failed to check ${plugin.name} for updates: ${err}`); + + // Clear loading state for this plugin + setPluginLoadingStates(prev => { + const newState = { ...prev }; + delete newState[plugin.file_path]; + return newState; + }); + } finally { + setIsCheckingSinglePlugin(false); + } + }, [serverPath]); + + // Update a plugin to the latest version + const updatePlugin = useCallback(async (plugin: Plugin) => { + const currentServerPath = serverPath; + const currentServerType = serverType; + + console.log(`updatePlugin called with serverPath: ${currentServerPath}`); + + if (!canUpdatePlugin(plugin)) { + setUpdateError(`Cannot update ${plugin.name}: Missing required update information`); + return; + } + + if (!currentServerPath) { + console.error('No server path available for updating plugin'); + setUpdateError('No server path available'); + return; + } + + // Set loading state for this plugin + setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: true })); + setUpdateError(null); + + try { + console.log(`Updating plugin: ${plugin.name} to version ${plugin.latest_version}`); + + // Create update message + const updateMessage = createUpdateMessage( + plugin.name, + plugin.latest_version, + plugin.platform_compatibility + ); + + // This is simplified as the actual UI feedback would be handled by the UIContext + console.log(updateMessage); + + const newFilePath = await invoke("update_plugin", { + pluginId: plugin.repository_id, + version: plugin.latest_version, + repository: plugin.repository_source, + currentFilePath: plugin.file_path, + serverTypeStr: currentServerType + }); + + console.log(`Update successful for ${plugin.name}, new file path: ${newFilePath}`); + + // Update the plugins array with the updated plugin + const updatedPlugins = plugins.map(p => { + if (p.file_path === plugin.file_path) { + return { + ...p, + version: p.latest_version || p.version, + has_update: false, + latest_version: p.latest_version, + file_path: newFilePath + }; + } + return p; + }); + + setPluginsState(updatedPlugins); + + // Save updated plugins data + if (currentServerPath) { + try { + await invoke("save_plugin_data", { + plugins: updatedPlugins, + serverPath: currentServerPath + }); + console.log(`Plugin data saved successfully after updating ${plugin.name}`); + } catch (saveError) { + console.error(`Error saving plugin data after update: ${saveError}`); + setUpdateError(`Plugin updated, but failed to save plugin data: ${saveError}`); + } + } + + // Dispatch custom event for plugin update completed + const event = new CustomEvent('single_plugin_updated', { + detail: { + plugin_name: plugin.name, + old_path: plugin.file_path, + new_path: newFilePath, + success: true + } + }); + window.dispatchEvent(event); + } catch (error) { + console.error(`Error updating plugin: ${error}`); + setUpdateError(`Failed to update ${plugin.name}: ${error}`); + + // Dispatch custom event for plugin update failed + const event = new CustomEvent('single_plugin_update_failed', { + detail: { + plugin_name: plugin.name, + error: String(error) + } + }); + window.dispatchEvent(event); + } finally { + // Clear loading state for this plugin + setPluginLoadingStates(prev => { + const newState = { ...prev }; + delete newState[plugin.file_path]; + return newState; + }); + } + }, [plugins, serverPath, serverType]); + + // Show plugin details + const showPluginDetails = useCallback((plugin: Plugin) => { + setSelectedPlugin(plugin); + }, []); + + // Close plugin details + const closePluginDetails = useCallback(() => { + setSelectedPlugin(null); + }, []); + + // Define the context value + const contextValue: PluginContextProps = { + plugins, + selectedPlugin, + isCheckingUpdates, + updateError, + pluginLoadingStates, + bulkUpdateProgress, + isCheckingSinglePlugin, + checkForUpdates, + checkSinglePlugin, + updatePlugin, + showPluginDetails, + closePluginDetails, + setPlugins, + clearUpdateError + }; + + return ( + + {children} + + ); +}; + +export default PluginProvider; \ No newline at end of file diff --git a/src/context/PluginContext/usePluginContext.ts b/src/context/PluginContext/usePluginContext.ts new file mode 100644 index 0000000..f8d73cc --- /dev/null +++ b/src/context/PluginContext/usePluginContext.ts @@ -0,0 +1,20 @@ +import { useContext } from 'react'; +import { PluginContext } from './PluginContext'; + +/** + * Custom hook for accessing the PluginContext + * + * @returns The PluginContext values and methods + * @throws Error if used outside of a PluginProvider + */ +export const usePluginContext = () => { + const context = useContext(PluginContext); + + if (!context || Object.keys(context).length === 0) { + throw new Error('usePluginContext must be used within a PluginProvider'); + } + + return context; +}; + +export default usePluginContext; \ No newline at end of file diff --git a/src/context/ServerContext/ServerContext.tsx b/src/context/ServerContext/ServerContext.tsx new file mode 100644 index 0000000..3787cac --- /dev/null +++ b/src/context/ServerContext/ServerContext.tsx @@ -0,0 +1,246 @@ +import React, { createContext, useState, useCallback, ReactNode, useEffect } from 'react'; +import { open } from "@tauri-apps/plugin-dialog"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { ServerInfo, ScanProgress, ScanResult } from '../../types/server.types'; +import { Plugin } from '../../types/plugin.types'; +import { isValidPath } from '../../utils/validators'; + +interface ServerContextProps { + /** + * Current server directory path + */ + serverPath: string; + + /** + * Server information (type, version, etc.) + */ + serverInfo: ServerInfo | null; + + /** + * Whether a server scan is in progress + */ + isScanning: boolean; + + /** + * Whether a scan has been completed + */ + scanComplete: boolean; + + /** + * Error message if any operation fails + */ + error: string | null; + + /** + * Progress information during scanning + */ + scanProgress: ScanProgress | null; + + /** + * Function to select a server directory + */ + selectDirectory: () => Promise; + + /** + * Function to scan for plugins in the selected directory + */ + scanForPlugins: () => Promise; + + /** + * Function to set the server path directly + */ + setServerPath: (path: string) => void; + + /** + * Function to clear any errors + */ + clearError: () => void; + + /** + * Function to get the path to the plugins directory + */ + getPluginsPath: () => string | null; +} + +// Create the context with default values +export const ServerContext = createContext({} as ServerContextProps); + +interface ServerProviderProps { + children: ReactNode; + onScanComplete?: (result: ScanResult) => void; +} + +/** + * Provider component for managing server-related state + */ +export const ServerProvider: React.FC = ({ + children, + onScanComplete +}) => { + const [serverPath, setServerPathState] = useState(""); + const [serverInfo, setServerInfo] = useState(null); + const [isScanning, setIsScanning] = useState(false); + const [scanComplete, setScanComplete] = useState(false); + const [error, setError] = useState(null); + const [scanProgress, setScanProgress] = useState(null); + + // Setup event listeners for scan events + useEffect(() => { + const unlisteners: (() => void)[] = []; + + // Handle scan-started event + listen('scan_started', () => { + console.log("Received scan_started event in ServerContext"); + setIsScanning(true); + setScanComplete(false); + setScanProgress(null); + setError(null); + }).then(unlisten => unlisteners.push(unlisten)); + + // Handle scan-progress event + listen('scan_progress', (event) => { + console.log("Received scan_progress event in ServerContext:", event.payload); + // Map backend field names to frontend field names + const progressData = event.payload as { processed: number; total: number; current_file: string }; + setScanProgress({ + current: progressData.processed, + total: progressData.total, + current_file: progressData.current_file + }); + }).then(unlisten => unlisteners.push(unlisten)); + + // Handle scan-completed event + listen('scan_completed', (event) => { + console.log("Received scan_completed event in ServerContext:", event.payload); + const result = event.payload as ScanResult; + + setServerInfo(result.server_info); + setIsScanning(false); + setScanComplete(true); + + // Call the callback if provided + if (onScanComplete) { + onScanComplete(result); + } + }).then(unlisten => unlisteners.push(unlisten)); + + // Handle scan-error event + listen('scan_error', (event) => { + console.log("Received scan_error event in ServerContext:", event.payload); + setError(event.payload as string); + setIsScanning(false); + }).then(unlisten => unlisteners.push(unlisten)); + + // Cleanup function to remove event listeners + return () => { + unlisteners.forEach(unlisten => unlisten()); + }; + }, [onScanComplete]); + + // Set server path + const setServerPath = useCallback((path: string) => { + setServerPathState(path); + setServerInfo(null); + setScanComplete(false); + setError(null); + setScanProgress(null); + }, []); + + // Clear error state + const clearError = useCallback(() => { + setError(null); + }, []); + + // Function to select a server directory using Tauri dialog + const selectDirectory = useCallback(async () => { + try { + const selected = await open({ + directory: true, + multiple: false, + title: "Select Minecraft Server Folder", + }); + + if (selected && typeof selected === "string") { + console.log(`Directory selected: ${selected}`); + setServerPath(selected); + + try { + // Load persisted plugin data if available + console.log(`Attempting to load persisted data for: ${selected}`); + const loadedPlugins: Plugin[] = await invoke("load_plugin_data", { serverPath: selected }); + + if (loadedPlugins && loadedPlugins.length > 0) { + console.log(`Loaded ${loadedPlugins.length} plugins from persistence.`); + // We don't set plugins here as that's handled by the PluginContext + setScanComplete(true); + } else { + console.log("No persisted plugin data found for this server."); + } + } catch (loadError) { + console.error("Error loading persisted plugin data:", loadError); + setError(`Failed to load previous plugin data: ${loadError}`); + } + } else { + console.log("Directory selection cancelled."); + } + } catch (err) { + console.error("Error selecting directory:", err); + setError(`Error selecting directory: ${err}`); + } + }, [setServerPath]); + + // Function to scan for plugins in the selected directory + const scanForPlugins = useCallback(async () => { + if (!isValidPath(serverPath) || isScanning) return; + + try { + console.log("Starting scan for plugins in:", serverPath); + setIsScanning(true); + setScanComplete(false); + setScanProgress(null); + setError(null); + + await invoke("scan_server_dir", { path: serverPath }); + console.log("Scan server dir command invoked successfully"); + + // Note: The actual scan results will be received via event listeners + // which are now set up in the useEffect hook above + + } catch (err) { + console.error("Error invoking scan command:", err); + setError(`Failed to start scan: ${err as string}`); + setIsScanning(false); + } + }, [serverPath, isScanning]); + + // Function to get the plugins directory path + const getPluginsPath = useCallback((): string | null => { + if (!serverInfo || !serverPath) return null; + + return `${serverPath}/${serverInfo.plugins_directory}`; + }, [serverPath, serverInfo]); + + // Define the context value + const contextValue: ServerContextProps = { + serverPath, + serverInfo, + isScanning, + scanComplete, + error, + scanProgress, + selectDirectory, + scanForPlugins, + setServerPath: setServerPathState, + clearError, + getPluginsPath + }; + + return ( + + {children} + + ); +}; + +export default ServerProvider; \ No newline at end of file diff --git a/src/context/ServerContext/useServerContext.ts b/src/context/ServerContext/useServerContext.ts new file mode 100644 index 0000000..451ff4d --- /dev/null +++ b/src/context/ServerContext/useServerContext.ts @@ -0,0 +1,20 @@ +import { useContext } from 'react'; +import { ServerContext } from './ServerContext'; + +/** + * Custom hook for accessing the ServerContext + * + * @returns The ServerContext values and methods + * @throws Error if used outside of a ServerProvider + */ +export const useServerContext = () => { + const context = useContext(ServerContext); + + if (!context || Object.keys(context).length === 0) { + throw new Error('useServerContext must be used within a ServerProvider'); + } + + return context; +}; + +export default useServerContext; \ No newline at end of file diff --git a/src/context/UIContext/UIContext.tsx b/src/context/UIContext/UIContext.tsx new file mode 100644 index 0000000..3818c1c --- /dev/null +++ b/src/context/UIContext/UIContext.tsx @@ -0,0 +1,150 @@ +import React, { createContext, useState, useCallback, ReactNode } from 'react'; +import { Plugin } from '../../types/plugin.types'; + +export type MessageType = 'info' | 'warning' | 'error' | 'success'; + +export interface WarningMessage { + text: string; + type: MessageType; + id: number; +} + +export interface PremiumPluginInfo { + name: string; + errorMessage: string; +} + +export interface UIContextProps { + warningMessage: WarningMessage | null; + premiumPluginInfo: PremiumPluginInfo | null; + downloadProgress: number; + isCompatibilityDialogOpen: boolean; + currentPluginForCompatibility: Plugin | null; + isUpdateAvailable: boolean; + updateVersion: string | null; + showWarningMessage: (text: string, type: MessageType) => void; + clearWarningMessage: () => void; + handlePremiumPlugin: (errorMessage: string, pluginName: string) => void; + clearPremiumPluginInfo: () => void; + setDownloadProgress: (progress: number) => void; + showCompatibilityDialog: (plugin: Plugin) => void; + closeCompatibilityDialog: () => void; + showUpdateAvailableMessage: (version: string) => void; + clearUpdateAvailableMessage: () => void; +} + +export const UIContext = createContext(null); + +interface UIProviderProps { + children: ReactNode; +} + +let messageIdCounter = 0; + +export const UIProvider: React.FC = ({ children }) => { + // State for warning messages + const [warningMessage, setWarningMessage] = useState(null); + + // State for premium plugin info + const [premiumPluginInfo, setPremiumPluginInfo] = useState(null); + + // State for download progress + const [downloadProgress, setDownloadProgress] = useState(0); + + // State for compatibility dialog + const [isCompatibilityDialogOpen, setIsCompatibilityDialogOpen] = useState(false); + const [currentPluginForCompatibility, setCurrentPluginForCompatibility] = useState(null); + + // State for update available notification + const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); + const [updateVersion, setUpdateVersion] = useState(null); + + // Function to show a warning message + const showWarningMessage = useCallback((text: string, type: MessageType = 'warning') => { + setWarningMessage({ + text, + type, + id: messageIdCounter++ + }); + + // Auto-clear messages after 5 seconds except for errors + if (type !== 'error') { + setTimeout(() => { + setWarningMessage(prevMessage => + prevMessage && prevMessage.id === messageIdCounter - 1 ? null : prevMessage + ); + }, 5000); + } + }, []); + + // Function to clear the warning message + const clearWarningMessage = useCallback(() => { + setWarningMessage(null); + }, []); + + // Function to handle premium plugin errors + const handlePremiumPlugin = useCallback((errorMessage: string, pluginName: string) => { + setPremiumPluginInfo({ + name: pluginName, + errorMessage + }); + }, []); + + // Function to clear premium plugin info + const clearPremiumPluginInfo = useCallback(() => { + setPremiumPluginInfo(null); + }, []); + + // Function to show compatibility dialog + const showCompatibilityDialog = useCallback((plugin: Plugin) => { + setCurrentPluginForCompatibility(plugin); + setIsCompatibilityDialogOpen(true); + }, []); + + // Function to close compatibility dialog + const closeCompatibilityDialog = useCallback(() => { + setIsCompatibilityDialogOpen(false); + setCurrentPluginForCompatibility(null); + }, []); + + // Function to show update available message + const showUpdateAvailableMessage = useCallback((version: string) => { + setUpdateVersion(version); + setIsUpdateAvailable(true); + showWarningMessage(`Update available: version ${version}`, 'info'); + }, [showWarningMessage]); + + // Function to clear update available message + const clearUpdateAvailableMessage = useCallback(() => { + setIsUpdateAvailable(false); + setUpdateVersion(null); + }, []); + + // Context value + const value: UIContextProps = { + warningMessage, + premiumPluginInfo, + downloadProgress, + isCompatibilityDialogOpen, + currentPluginForCompatibility, + isUpdateAvailable, + updateVersion, + showWarningMessage, + clearWarningMessage, + handlePremiumPlugin, + clearPremiumPluginInfo, + setDownloadProgress, + showCompatibilityDialog, + closeCompatibilityDialog, + showUpdateAvailableMessage, + clearUpdateAvailableMessage + }; + + return ( + + {children} + + ); +}; + +export default UIProvider; \ No newline at end of file diff --git a/src/context/UIContext/useUIContext.ts b/src/context/UIContext/useUIContext.ts new file mode 100644 index 0000000..70022a3 --- /dev/null +++ b/src/context/UIContext/useUIContext.ts @@ -0,0 +1,20 @@ +import { useContext } from 'react'; +import { UIContext } from './UIContext'; + +/** + * Custom hook for accessing the UIContext + * + * @returns The UIContext values and methods + * @throws Error if used outside of a UIProvider + */ +export const useUIContext = () => { + const context = useContext(UIContext); + + if (!context || Object.keys(context).length === 0) { + throw new Error('useUIContext must be used within a UIProvider'); + } + + return context; +}; + +export default useUIContext; \ No newline at end of file diff --git a/src/hooks/useAppUpdates.ts b/src/hooks/useAppUpdates.ts new file mode 100644 index 0000000..8427c17 --- /dev/null +++ b/src/hooks/useAppUpdates.ts @@ -0,0 +1,59 @@ +import { useState, useCallback, useEffect } from 'react'; +import { useUIContext } from '../context/UIContext/useUIContext'; +import { checkForAppUpdate as checkForAppUpdateAPI, UpdateInfo } from '../utils/appUpdates'; + +/** + * Custom hook for managing application updates + */ +export const useAppUpdates = () => { + const [isCheckingAppUpdate, setIsCheckingAppUpdate] = useState(false); + const [appUpdateAvailable, setAppUpdateAvailable] = useState(false); + const [appUpdateVersion, setAppUpdateVersion] = useState(null); + + const { showUpdateAvailableMessage } = useUIContext(); + + // Check for application updates + const checkForAppUpdate = useCallback(async () => { + if (isCheckingAppUpdate) return; + + setIsCheckingAppUpdate(true); + + try { + console.log('Checking for application updates...'); + const updateInfo: UpdateInfo = await checkForAppUpdateAPI(); + + setAppUpdateAvailable(updateInfo.available); + setAppUpdateVersion(updateInfo.version || null); + + if (updateInfo.available && updateInfo.version) { + console.log(`Application update available: ${updateInfo.version}`); + showUpdateAvailableMessage(updateInfo.version); + } else { + console.log('No application updates available'); + } + } catch (error) { + console.error('Error checking for app updates:', error); + } finally { + setIsCheckingAppUpdate(false); + } + }, [isCheckingAppUpdate, showUpdateAvailableMessage]); + + // Auto-check for updates on initial load + useEffect(() => { + // Wait a moment after initial load before checking + const timer = setTimeout(() => { + checkForAppUpdate(); + }, 2000); + + return () => clearTimeout(timer); + }, [checkForAppUpdate]); + + return { + isCheckingAppUpdate, + appUpdateAvailable, + appUpdateVersion, + checkForAppUpdate + }; +}; + +export default useAppUpdates; \ No newline at end of file diff --git a/src/hooks/useEventListeners.ts b/src/hooks/useEventListeners.ts new file mode 100644 index 0000000..7675302 --- /dev/null +++ b/src/hooks/useEventListeners.ts @@ -0,0 +1,215 @@ +import { useEffect, useRef } from 'react'; +import { listen, UnlistenFn } from "@tauri-apps/api/event"; + +// Import types +import { ScanProgress, ScanResult } from '../types/server.types'; +import { BulkUpdateProgressPayload, SingleUpdateResultPayload, DownloadProgress, PremiumPluginInfo } from '../types/events.types'; +import { Plugin } from '../types/plugin.types'; + +// Event handler types +export type ScanStartedHandler = (serverPath: string) => void; +export type ScanProgressHandler = (progress: { current: number; total: number }) => void; +export type ScanCompletedHandler = (result: { + plugins: any[]; + server_info: any; +}) => void; +export type ScanErrorHandler = (error: string) => void; + +export type BulkUpdateStartHandler = (totalPlugins: number) => void; +export type UpdateCheckProgressHandler = (progress: { + current: number; + total: number; + plugin_name?: string; +}) => void; +export type SingleUpdateStartedHandler = (pluginName: string) => void; +export type SingleUpdateCompletedHandler = (result: { + success: boolean; + error?: string; + plugin_name?: string; + original_file_path?: string; +}) => void; + +export type DownloadProgressHandler = (progress: { + percentage: number; + pluginName?: string; + status: 'downloading' | 'completed' | 'error'; + error?: string; +}) => void; + +export type UpdateCheckCompleteHandler = (result: { + outdated_count: number; + total_checked: number; + success: boolean; +}) => void; + +export type AppUpdateAvailableHandler = (info: { + version: string; + body?: string; + date?: string; +}) => void; + +export interface EventListenerProps { + // Scan events + onScanStarted?: ScanStartedHandler; + onScanProgress?: ScanProgressHandler; + onScanCompleted?: ScanCompletedHandler; + onScanError?: ScanErrorHandler; + + // Update check events + onBulkUpdateStart?: BulkUpdateStartHandler; + onUpdateCheckProgress?: UpdateCheckProgressHandler; + onSingleUpdateStarted?: SingleUpdateStartedHandler; + onSingleUpdateCompleted?: SingleUpdateCompletedHandler; + onDownloadProgress?: DownloadProgressHandler; + onUpdateCheckComplete?: UpdateCheckCompleteHandler; + + // App update events + onAppUpdateAvailable?: AppUpdateAvailableHandler; +} + +/** + * Custom hook for setting up all the Tauri event listeners + * + * @param handlers Object containing event handler functions + */ +export const useEventListeners = ({ + onScanStarted, + onScanProgress, + onScanCompleted, + onScanError, + onBulkUpdateStart, + onUpdateCheckProgress, + onSingleUpdateStarted, + onSingleUpdateCompleted, + onDownloadProgress, + onUpdateCheckComplete, + onAppUpdateAvailable +}: EventListenerProps) => { + useEffect(() => { + const unlistenFunctions: UnlistenFn[] = []; + + // Set up scan event listeners + if (onScanStarted) { + listen('scan_started', (event) => { + onScanStarted(event.payload as string); + }).then(unlisten => unlistenFunctions.push(unlisten)); + } + + if (onScanProgress) { + listen('scan_progress', (event) => { + onScanProgress(event.payload as { current: number; total: number }); + }).then(unlisten => unlistenFunctions.push(unlisten)); + } + + if (onScanCompleted) { + listen('scan_completed', (event) => { + onScanCompleted(event.payload as { plugins: any[]; server_info: any }); + }).then(unlisten => unlistenFunctions.push(unlisten)); + } + + if (onScanError) { + listen('scan_error', (event) => { + onScanError(event.payload as string); + }).then(unlisten => unlistenFunctions.push(unlisten)); + } + + // Set up update check event listeners + if (onBulkUpdateStart) { + listen('bulk_update_start', (event) => { + onBulkUpdateStart(event.payload as number); + }).then(unlisten => unlistenFunctions.push(unlisten)); + } + + if (onUpdateCheckProgress) { + listen('update_check_progress', (event) => { + onUpdateCheckProgress(event.payload as { + current: number; + total: number; + plugin_name?: string; + }); + }).then(unlisten => unlistenFunctions.push(unlisten)); + } + + if (onSingleUpdateStarted) { + listen('single_update_started', (event) => { + onSingleUpdateStarted(event.payload as string); + }).then(unlisten => unlistenFunctions.push(unlisten)); + } + + if (onSingleUpdateCompleted) { + listen('single_update_completed', (event) => { + onSingleUpdateCompleted(event.payload as { + success: boolean; + error?: string; + plugin_name?: string; + original_file_path?: string; + }); + }).then(unlisten => unlistenFunctions.push(unlisten)); + } + + if (onDownloadProgress) { + listen('download_progress', (event) => { + onDownloadProgress(event.payload as { + percentage: number; + pluginName?: string; + status: 'downloading' | 'completed' | 'error'; + error?: string; + }); + }).then(unlisten => unlistenFunctions.push(unlisten)); + } + + if (onUpdateCheckComplete) { + // Listen for Tauri event + listen('update_check_complete', (event) => { + console.log("Received Tauri update_check_complete event:", event.payload); + onUpdateCheckComplete(event.payload as { + outdated_count: number; + total_checked: number; + success: boolean; + }); + }).then(unlisten => unlistenFunctions.push(unlisten)); + + // Also listen for custom browser event (fallback) + const handleCustomEvent = (event: any) => { + console.log("Received custom update_check_complete event:", event.detail); + onUpdateCheckComplete(event.detail); + }; + + window.addEventListener('update_check_complete', handleCustomEvent); + + // Add cleanup function for browser event + unlistenFunctions.push(() => { + window.removeEventListener('update_check_complete', handleCustomEvent); + }); + } + + if (onAppUpdateAvailable) { + listen('update_available', (event) => { + onAppUpdateAvailable(event.payload as { + version: string; + body?: string; + date?: string; + }); + }).then(unlisten => unlistenFunctions.push(unlisten)); + } + + // Clean up function to remove all listeners + return () => { + unlistenFunctions.forEach(unlisten => unlisten()); + }; + }, [ + onScanStarted, + onScanProgress, + onScanCompleted, + onScanError, + onBulkUpdateStart, + onUpdateCheckProgress, + onSingleUpdateStarted, + onSingleUpdateCompleted, + onDownloadProgress, + onUpdateCheckComplete, + onAppUpdateAvailable + ]); +}; + +export default useEventListeners; \ No newline at end of file diff --git a/src/hooks/usePluginActions.ts b/src/hooks/usePluginActions.ts new file mode 100644 index 0000000..6badd5c --- /dev/null +++ b/src/hooks/usePluginActions.ts @@ -0,0 +1,418 @@ +import { useCallback, useState, useEffect, useMemo } from 'react'; +import { usePluginContext } from '../context/PluginContext/usePluginContext'; +import { useUIContext } from '../context/UIContext/useUIContext'; +import { useEventListeners } from './useEventListeners'; +import { Plugin, PotentialPluginMatch } from '../types/plugin.types'; +import { ServerType } from '../types/server.types'; +import { isPremiumPluginError } from '../utils/validators'; +import { invoke } from '@tauri-apps/api/core'; +import { listen } from '@tauri-apps/api/event'; + +// Define the potential matches payload type +interface PotentialMatchesPayload { + plugin: { + file_path: string; + }; + matches: PotentialPluginMatch[]; +} + +/** + * Custom hook that extends PluginContext with enhanced plugin actions + * and automatic event handling + */ +export const usePluginActions = (serverType?: ServerType) => { + const { + plugins, + selectedPlugin, + isCheckingUpdates, + updateError, + pluginLoadingStates, + bulkUpdateProgress, + isCheckingSinglePlugin, + checkForUpdates: contextCheckForUpdates, + checkSinglePlugin: contextCheckSinglePlugin, + updatePlugin: contextUpdatePlugin, + showPluginDetails, + closePluginDetails, + setPlugins, + clearUpdateError + } = usePluginContext(); + + const { + showCompatibilityDialog, + handlePremiumPlugin, + showWarningMessage + } = useUIContext(); + + // Local state to track updates in progress + const [updatingPlugin, setUpdatingPlugin] = useState(null); + const [currentUpdateProgress, setCurrentUpdateProgress] = useState(0); + + // Add state for potential matches + const [potentialMatches, setPotentialMatches] = useState([]); + const [currentPluginForMatch, setCurrentPluginForMatch] = useState(null); + const [isMatchSelectorOpen, setIsMatchSelectorOpen] = useState(false); + + // Create a state object for plugin loading states that we manage locally + const [localPluginLoadingStates, setLocalPluginLoadingStates] = useState>({}); + + // Enhanced check for updates function + const checkForUpdates = useCallback(async () => { + await contextCheckForUpdates(serverType); + }, [contextCheckForUpdates, serverType]); + + // Enhanced function for managing loading states + const checkSinglePlugin = useCallback(async (plugin: Plugin) => { + // First, check if the plugin is already being checked + if (pluginLoadingStates[plugin.file_path] || localPluginLoadingStates[plugin.file_path]) { + console.log(`Plugin ${plugin.name} is already being checked for updates, skipping.`); + return; + } + + // Set loading state before checking update + setLocalPluginLoadingStates(prev => ({ + ...prev, + [plugin.file_path]: true + })); + + try { + console.log(`Checking plugin ${plugin.name} for updates`); + await contextCheckSinglePlugin(plugin); + // The loading state will be cleared by the event listener when update check completes + } catch (error) { + console.error(`Error checking plugin ${plugin.name} for updates:`, error); + showWarningMessage(`Failed to check ${plugin.name} for updates: ${error}`, 'error'); + + // Clear loading state on error + setLocalPluginLoadingStates(prev => { + const newState = { ...prev }; + delete newState[plugin.file_path]; + return newState; + }); + } + }, [contextCheckSinglePlugin, showWarningMessage, pluginLoadingStates, localPluginLoadingStates]); + + // Enhanced update plugin function with compatibility check + const updatePlugin = useCallback((plugin: Plugin) => { + // Set loading state + setLocalPluginLoadingStates(prev => ({ + ...prev, + [plugin.file_path]: true + })); + + // Instead of updating directly, show compatibility dialog first + showCompatibilityDialog(plugin); + + // Note: The actual update will happen when the user confirms in the dialog + // and the UIContext will call the updatePlugin function from PluginContext + // Loading state will be cleared by event listeners + }, [showCompatibilityDialog]); + + // Function to proceed with update after compatibility check + const proceedWithUpdate = useCallback(async (plugin: Plugin) => { + try { + await contextUpdatePlugin(plugin); + } catch (error: any) { + console.error('Error in proceedWithUpdate:', error); + + // Handle premium plugin error + if (isPremiumPluginError(error.toString())) { + handlePremiumPlugin(error.toString(), plugin.name); + } + } + }, [contextUpdatePlugin, handlePremiumPlugin]); + + // Listen for events + useEventListeners({ + onBulkUpdateStart: (totalPlugins) => { + console.log('Bulk update start handled in usePluginActions:', totalPlugins); + // Context already handles this, no additional action needed + }, + onUpdateCheckProgress: (progress) => { + console.log('Update check progress handled in usePluginActions:', progress); + // Context already handles this, no additional action needed + }, + onSingleUpdateStarted: (pluginName) => { + console.log('Single update started handled in usePluginActions:', pluginName); + // Context already handles this, no additional action needed + }, + onSingleUpdateCompleted: (result) => { + console.log('Single update completed handled in usePluginActions:', result); + const plugin = plugins.find(p => p.file_path === result.original_file_path); + const pluginName = plugin ? plugin.name : 'Plugin'; + + if (result.error) { + // If it's a premium plugin error, handle it specially + if (isPremiumPluginError(result.error)) { + if (plugin) { + handlePremiumPlugin(result.error, plugin.name); + // Optionally show a general warning too + showWarningMessage(`Could not update premium plugin: ${pluginName}. Requires manual download.`, 'warning'); + } + } else { + // Show general error notification + showWarningMessage(`Failed to update ${pluginName}: ${result.error}`, 'error'); + } + } else { + // Show success notification + showWarningMessage(`${pluginName} updated successfully!`, 'success'); + } + }, + onDownloadProgress: (progress) => { + console.log('Download progress handled in usePluginActions:', progress); + + if (progress.pluginName) { + setUpdatingPlugin(progress.pluginName); + setCurrentUpdateProgress(progress.percentage); + + if (progress.status === 'completed') { + // Reset after completion + setTimeout(() => { + setUpdatingPlugin(null); + setCurrentUpdateProgress(0); + }, 1000); + } + } + } + }); + + // Listen for potential matches in a useEffect + useEffect(() => { + const unlistenPotentialMatches = listen('potential_matches_found', (event: { payload: PotentialMatchesPayload }) => { + const payload = event.payload; + console.log('Potential matches found:', payload); + + if (payload.matches && payload.matches.length > 0) { + // Find the plugin this is for + const targetPlugin = plugins.find(p => p.file_path === payload.plugin.file_path); + if (targetPlugin) { + setPotentialMatches(payload.matches); + setCurrentPluginForMatch(targetPlugin); + setIsMatchSelectorOpen(true); + + // Clear loading state for this plugin + setLocalPluginLoadingStates(prev => { + const newState = { ...prev }; + delete newState[payload.plugin.file_path]; + return newState; + }); + } + } + }); + + return () => { + unlistenPotentialMatches.then((unlisten: () => void) => unlisten()); + }; + }, [plugins]); + + // Function to handle selecting a match + const handleMatchSelection = useCallback(async (match: PotentialPluginMatch) => { + if (currentPluginForMatch) { + try { + // Set loading state for this plugin + setLocalPluginLoadingStates(prev => ({ + ...prev, + [currentPluginForMatch.file_path]: true + })); + + // Invoke backend to update with selected match + await invoke('set_plugin_match', { + pluginPath: currentPluginForMatch.file_path, + match: { + name: match.name, + repository: match.repository, + repositoryId: match.repository_id, + version: match.version, + pageUrl: match.page_url + } + }); + + // Show success message + showWarningMessage(`Successfully matched ${currentPluginForMatch.name} with ${match.name} from ${match.repository}`, 'success'); + + // Refresh plugin data + checkSinglePlugin(currentPluginForMatch); + } catch (error) { + console.error('Error setting plugin match:', error); + showWarningMessage(`Failed to set plugin match: ${error}`, 'error'); + + // Clear loading state + setLocalPluginLoadingStates(prev => { + const newState = { ...prev }; + delete newState[currentPluginForMatch.file_path]; + return newState; + }); + } + } + + // Close the selector + setIsMatchSelectorOpen(false); + setPotentialMatches([]); + setCurrentPluginForMatch(null); + }, [currentPluginForMatch, showWarningMessage, checkSinglePlugin]); + + // Function to close match selector without selecting + const closeMatchSelector = useCallback(() => { + setIsMatchSelectorOpen(false); + setPotentialMatches([]); + setCurrentPluginForMatch(null); + + // Clear loading state if there's a current plugin + if (currentPluginForMatch) { + setLocalPluginLoadingStates(prev => { + const newState = { ...prev }; + delete newState[currentPluginForMatch.file_path]; + return newState; + }); + } + }, [currentPluginForMatch]); + + // Get outdated plugins count + const getOutdatedPluginsCount = useCallback((): number => { + return plugins.filter(plugin => plugin.has_update).length; + }, [plugins]); + + // Get plugin by name + const getPluginByName = useCallback((name: string): Plugin | undefined => { + return plugins.find(plugin => plugin.name.toLowerCase() === name.toLowerCase()); + }, [plugins]); + + // Get plugins filtered by criteria + const getFilteredPlugins = useCallback((criteria: Partial): Plugin[] => { + return plugins.filter(plugin => { + return Object.entries(criteria).every(([key, value]) => { + return plugin[key as keyof Plugin] === value; + }); + }); + }, [plugins]); + + // Combined loading states from context and local state + const combinedLoadingStates = useMemo(() => { + return { + ...localPluginLoadingStates, + ...pluginLoadingStates + }; + }, [localPluginLoadingStates, pluginLoadingStates]); + + // Set up event listeners for plugin-related events + useEffect(() => { + const unlisteners: (() => void)[] = []; + + // Listen for bulk updates complete + window.addEventListener('update_check_complete', (event: any) => { + console.log('Custom update_check_complete event received:', event.detail); + + if (event.detail?.success && event.detail?.outdated_count) { + const message = `Found ${event.detail.outdated_count} plugins with updates available.`; + showWarningMessage(message, 'info'); + } + }); + + // Listen for single plugin update completed + window.addEventListener('single_plugin_updated', (event: any) => { + console.log('Custom single_plugin_updated event received:', event.detail); + + if (event.detail?.success) { + const message = `Successfully updated ${event.detail.plugin_name}.`; + showWarningMessage(message, 'success'); + + // Clear loading state for this plugin + setLocalPluginLoadingStates(prev => { + const newState = { ...prev }; + if (event.detail.old_path) { + delete newState[event.detail.old_path]; + } + return newState; + }); + } + }); + + // Listen for single plugin update failed + window.addEventListener('single_plugin_update_failed', (event: any) => { + console.log('Custom single_plugin_update_failed event received:', event.detail); + + if (event.detail?.plugin_name) { + const message = `Failed to update ${event.detail.plugin_name}: ${event.detail.error || 'Unknown error'}`; + showWarningMessage(message, 'error'); + + // Clear loading state for this plugin + setLocalPluginLoadingStates(prev => { + const newState = { ...prev }; + if (event.detail.plugin_name) { + // Find the plugin by name and clear its loading state + const plugin = plugins.find(p => p.name === event.detail.plugin_name); + if (plugin?.file_path) { + delete newState[plugin.file_path]; + } + } + return newState; + }); + } + }); + + // Listen for single plugin check completed + window.addEventListener('single_plugin_check_completed', (event: any) => { + console.log('Custom single_plugin_check_completed event received:', event.detail); + + if (event.detail?.plugin_path) { + // Clear loading state for this plugin + setLocalPluginLoadingStates(prev => { + const newState = { ...prev }; + delete newState[event.detail.plugin_path]; + return newState; + }); + + // Show result notification + if (event.detail.error) { + showWarningMessage( + `Failed to check for updates: ${event.detail.error}`, + 'error' + ); + } + } + }); + + return () => { + // Remove event listeners + window.removeEventListener('update_check_complete', () => {}); + window.removeEventListener('single_plugin_updated', () => {}); + window.removeEventListener('single_plugin_update_failed', () => {}); + window.removeEventListener('single_plugin_check_completed', () => {}); + }; + }, [plugins, showWarningMessage]); + + return { + // Original context values + plugins, + selectedPlugin, + isCheckingUpdates, + updateError, + pluginLoadingStates: combinedLoadingStates, + bulkUpdateProgress, + isCheckingSinglePlugin, + showPluginDetails, + closePluginDetails, + setPlugins, + clearUpdateError, + + // Enhanced functionality + updatingPlugin, + currentUpdateProgress, + checkForUpdates, + checkSinglePlugin, + updatePlugin, + proceedWithUpdate, + getOutdatedPluginsCount, + getPluginByName, + getFilteredPlugins, + + // Add match selector state and handlers + potentialMatches, + currentPluginForMatch, + isMatchSelectorOpen, + handleMatchSelection, + closeMatchSelector + }; +}; + +export default usePluginActions; \ No newline at end of file diff --git a/src/hooks/useServerActions.ts b/src/hooks/useServerActions.ts new file mode 100644 index 0000000..dc86d6d --- /dev/null +++ b/src/hooks/useServerActions.ts @@ -0,0 +1,109 @@ +import { useState, useCallback } from 'react'; +import { useServerContext } from '../context/ServerContext/useServerContext'; +import { useEventListeners } from './useEventListeners'; +import { ScanResult, ServerInfo } from '../types/server.types'; +import { Plugin } from '../types/plugin.types'; + +/** + * Custom hook that extends ServerContext with enhanced scan functionality + * with automatic event handling + */ +export const useServerActions = (onPluginsLoaded?: (plugins: Plugin[]) => void) => { + const { + serverPath, + serverInfo, + isScanning, + scanComplete, + error, + scanProgress, + scanForPlugins: contextScanForPlugins, + selectDirectory: contextSelectDirectory, + setServerPath, + clearError, + getPluginsPath + } = useServerContext(); + + // Local state for enhanced functionality + const [lastScanResult, setLastScanResult] = useState(null); + + // Handle scan events + useEventListeners({ + onScanStarted: () => { + console.log('Scan started handled in useServerActions'); + // This is already handled by the context, no additional action needed + }, + onScanProgress: (progress) => { + console.log('Scan progress handled in useServerActions:', progress); + // This is already handled by the context, no additional action needed + }, + onScanCompleted: (result) => { + console.log('Scan completed handled in useServerActions:', result); + setLastScanResult(result); + + // If a callback was provided, call it with the plugins + if (onPluginsLoaded) { + onPluginsLoaded(result.plugins); + } + }, + onScanError: (errorMessage) => { + console.log('Scan error handled in useServerActions:', errorMessage); + // This is already handled by the context, no additional action needed + } + }); + + // Enhanced select directory with post-selection actions + const selectDirectory = useCallback(async () => { + await contextSelectDirectory(); + // The rest is handled by the context and event listeners + }, [contextSelectDirectory]); + + // Enhanced scan for plugins with additional options + const scanForPlugins = useCallback(async () => { + // Reset the last scan result if we're starting a new scan + setLastScanResult(null); + + // Call the context's scan function + await contextScanForPlugins(); + // The rest is handled by the context and event listeners + }, [contextScanForPlugins]); + + // Get server name based on server info + const getServerName = useCallback((): string => { + if (!serverInfo) return 'Unknown Server'; + + let name = serverInfo.server_type; + if (serverInfo.minecraft_version) { + name += ` (${serverInfo.minecraft_version})`; + } + + return name; + }, [serverInfo]); + + // Check if server is compatible with a specific server type + const isCompatibleWith = useCallback((serverTypes: string[]): boolean => { + if (!serverInfo) return false; + return serverTypes.includes(serverInfo.server_type); + }, [serverInfo]); + + return { + // Original context values + serverPath, + serverInfo, + isScanning, + scanComplete, + error, + scanProgress, + setServerPath, + clearError, + getPluginsPath, + + // Enhanced functionality + lastScanResult, + selectDirectory, + scanForPlugins, + getServerName, + isCompatibleWith + }; +}; + +export default useServerActions; \ No newline at end of file diff --git a/src/hooks/useUpdateActions.ts b/src/hooks/useUpdateActions.ts new file mode 100644 index 0000000..9c2ad86 --- /dev/null +++ b/src/hooks/useUpdateActions.ts @@ -0,0 +1,113 @@ +import { useCallback } from 'react'; +import { usePluginContext } from '../context/PluginContext/usePluginContext'; +import { useUIContext } from '../context/UIContext/useUIContext'; +import { useEventListeners } from './useEventListeners'; +import { useServerContext } from '../context/ServerContext/useServerContext'; +import { Plugin } from '../types/plugin.types'; + +/** + * Custom hook for managing plugin updates + */ +export const useUpdateActions = () => { + const { + plugins, + checkForUpdates: checkForPluginUpdates, + updatePlugin, + bulkUpdateProgress, + isCheckingUpdates + } = usePluginContext(); + + const { + showWarningMessage + } = useUIContext(); + + const { serverPath } = useServerContext(); + + // Listen for update-related events + useEventListeners({ + onUpdateCheckComplete: (result) => { + console.log('Update check complete handled in useUpdateActions:', result); + + if (result.outdated_count > 0) { + showWarningMessage( + `Found ${result.outdated_count} plugin(s) with updates available.`, + 'info' + ); + } else { + showWarningMessage('All plugins are up to date!', 'success'); + } + } + }); + + // Update all plugins with available updates + const updateAllPlugins = useCallback(async () => { + const pluginsToUpdate = plugins.filter(plugin => plugin.has_update); + + if (pluginsToUpdate.length === 0) { + showWarningMessage('No plugins with updates available.', 'info'); + return; + } + + // The actual update process is handled by the PluginContext + // This is just a convenience function to find and trigger updates + for (const plugin of pluginsToUpdate) { + try { + await updatePlugin(plugin); + } catch (error: any) { + console.error(`Error updating plugin ${plugin.name}:`, error); + // Continue with other plugins if one fails + } + } + }, [plugins, updatePlugin, showWarningMessage]); + + // Check for plugin updates with improved debugging + const checkForAllUpdates = useCallback(async () => { + console.log('Starting plugin update check in useUpdateActions'); + + // Get the latest server path directly from context + const currentServerPath = serverPath; + + console.log('Current state:', { + serverPath: currentServerPath, + pluginsCount: plugins.length, + isCheckingUpdates + }); + + if (!currentServerPath) { + console.error('No server path available. Please select a server first.'); + showWarningMessage('No server path available. Please select a server first.', 'error'); + return; + } + + if (!plugins.length) { + console.error('No plugins available to check for updates.'); + showWarningMessage('No plugins available to check for updates.', 'warning'); + return; + } + + if (isCheckingUpdates) { + console.log('Update check already in progress, ignoring request'); + return; + } + + try { + console.log('Calling checkForPluginUpdates with serverPath:', currentServerPath); + await checkForPluginUpdates(); + console.log('Plugin update check completed successfully'); + } catch (error) { + console.error('Error checking for plugin updates:', error); + showWarningMessage(`Failed to check for updates: ${String(error)}`, 'error'); + } + }, [checkForPluginUpdates, plugins.length, serverPath, isCheckingUpdates, showWarningMessage]); + + return { + // Plugin update state from context + bulkUpdateProgress, + + // Functions + updateAllPlugins, + checkForAllUpdates + }; +}; + +export default useUpdateActions; \ No newline at end of file diff --git a/src/types/events.types.ts b/src/types/events.types.ts new file mode 100644 index 0000000..73b5c0d --- /dev/null +++ b/src/types/events.types.ts @@ -0,0 +1,27 @@ +import { Plugin } from './plugin.types'; + +export interface BulkUpdateProgressPayload { + processed: number; + total: number; + current_plugin_name: string; +} + +export interface SingleUpdateResultPayload { + original_file_path: string; + plugin: Plugin | null; + error: string | null; +} + +export interface DownloadProgress { + pluginName: string; + version: string; + percentage: number; + status: 'downloading' | 'extracting' | 'installing' | 'completed' | 'error'; + message?: string; +} + +export interface PremiumPluginInfo { + name: string; + version: string; + url: string; +} \ No newline at end of file diff --git a/src/types/plugin.types.ts b/src/types/plugin.types.ts new file mode 100644 index 0000000..404e732 --- /dev/null +++ b/src/types/plugin.types.ts @@ -0,0 +1,41 @@ + +export interface Plugin { + name: string; + version: string; + latest_version?: string; + description?: string; + authors: string[]; + has_update: boolean; + api_version?: string; + main_class?: string; + depend?: string[] | null; + soft_depend?: string[] | null; + load_before?: string[] | null; + commands?: any; + permissions?: any; + file_path: string; + file_hash: string; + website?: string; + changelog?: string; + repository_source?: string; + repository_id?: string; + repository_url?: string; + platform_compatibility?: string[]; + found_by_flexible_search?: boolean; +} + +export interface PluginDetailsProps { + plugin: Plugin; + onClose: () => void; +} + +export interface PotentialPluginMatch { + name: string; + version: string; + repository: string; + repository_id: string; + page_url: string; + description?: string; + minecraft_versions: string[]; + download_count?: number; +} \ No newline at end of file diff --git a/src/types/server.types.ts b/src/types/server.types.ts new file mode 100644 index 0000000..e80ef38 --- /dev/null +++ b/src/types/server.types.ts @@ -0,0 +1,30 @@ +export type ServerType = + | 'Paper' + | 'Spigot' + | 'Bukkit' + | 'Vanilla' + | 'Forge' + | 'Fabric' + | 'Velocity' + | 'BungeeCord' + | 'Waterfall' + | 'Unknown'; + +export interface ServerInfo { + server_type: ServerType; + minecraft_version?: string; + plugins_directory: string; + plugins_count: number; +} + +export interface ScanResult { + server_info: ServerInfo; + plugins: import('./plugin.types').Plugin[]; +} + +export interface ScanProgress { + current: number; + processed?: number; + total: number; + current_file: string; +} \ No newline at end of file diff --git a/src/types/tauri.d.ts b/src/types/tauri.d.ts new file mode 100644 index 0000000..915a267 --- /dev/null +++ b/src/types/tauri.d.ts @@ -0,0 +1,114 @@ +declare module '@tauri-apps/api/shell' { + /** + * Opens a path or URL with the default application. + * @param path The path or URL to open. + * @returns A promise that resolves when the command finishes. + */ + export function open(path: string): Promise; +} + +declare module '@tauri-apps/api/core' { + /** + * Invokes a Tauri command. + * @param cmd The command name. + * @param args Command arguments. + * @returns A promise resolving to the command result. + */ + export function invoke(cmd: string, args?: Record): Promise; +} + +declare module '@tauri-apps/api/core/updater' { + export interface UpdateResult { + available: boolean; + manifest?: { + version: string; + date: string; + body: string; + }; + shouldUpdate: boolean; + } + + /** + * Checks if an update is available. + * @returns A promise resolving to the update check result. + */ + export function checkUpdate(): Promise; + + /** + * Installs the available update. + * @returns A promise that resolves when the update is installed. + */ + export function installUpdate(): Promise; +} + +declare module '@tauri-apps/api/core/path' { + /** + * Returns the path to the application data directory. + * @returns A promise resolving to the app data directory path. + */ + export function appDataDir(): Promise; + + /** + * Returns the path to the application configuration directory. + * @returns A promise resolving to the app config directory path. + */ + export function appConfigDir(): Promise; + + /** + * Returns the path to the application local data directory. + * @returns A promise resolving to the app local data directory path. + */ + export function appLocalDataDir(): Promise; + + /** + * Returns the path to the application cache directory. + * @returns A promise resolving to the app cache directory path. + */ + export function appCacheDir(): Promise; + + /** + * Returns the path to the application log directory. + * @returns A promise resolving to the app log directory path. + */ + export function appLogDir(): Promise; +} + +declare module '@tauri-apps/api/core/event' { + export interface Event { + payload: T; + windowLabel?: string; + event: string; + } + + export type EventCallback = (event: Event) => void; + export type UnlistenFn = () => void; + + /** + * Listen to an event from the backend. + * @param event The event name. + * @param handler The event handler. + * @returns A promise resolving to a function to unlisten to the event. + */ + export function listen(event: string, handler: EventCallback): Promise; + + /** + * Listen to an event from the backend once. + * @param event The event name. + * @param handler The event handler. + * @returns A promise resolving to a function to unlisten to the event. + */ + export function once(event: string, handler: EventCallback): Promise; +} + +// Keep the old path declarations for backward compatibility +declare module '@tauri-apps/api/event' { + export * from '@tauri-apps/api/core/event'; +} + +declare module '@tauri-apps/api/updater' { + export * from '@tauri-apps/api/core/updater'; +} + +declare module '@tauri-apps/api/path' { + export * from '@tauri-apps/api/core/path'; +} \ No newline at end of file diff --git a/src/utils/appUpdates.ts b/src/utils/appUpdates.ts new file mode 100644 index 0000000..3fa4764 --- /dev/null +++ b/src/utils/appUpdates.ts @@ -0,0 +1,61 @@ +export interface UpdateInfo { + available: boolean; + version?: string; + body?: string; + date?: string; +} + +// Environment detection - check if running in a Tauri environment +const isTauriApp = window && typeof window !== 'undefined' && 'window.__TAURI__' in window; + +/** + * Checks if there's an app update available + * @returns Promise with update information + */ +export async function checkForAppUpdate(): Promise { + if (!isTauriApp) { + console.log('Update check skipped - not running in Tauri environment'); + return { available: false }; + } + + try { + // Dynamically import the module only when in Tauri context + // We need to use a variable for the import path to prevent Vite from trying to resolve it at build time + const updaterModule = '@tauri-apps/api/updater'; + const { checkUpdate } = await import(/* @vite-ignore */ updaterModule); + const response = await checkUpdate(); + + return { + available: response.available, + version: response.manifest?.version, + body: response.manifest?.body, + date: response.manifest?.date + }; + } catch (error) { + console.error('Error checking for update:', error); + return { available: false }; + } +} + +/** + * Installs the available app update + * Note: This will restart the app if successful + * @returns Promise that resolves when update is complete or fails + */ +export async function installAppUpdate(): Promise { + if (!isTauriApp) { + console.log('Update installation skipped - not running in Tauri environment'); + return; + } + + try { + // Dynamically import the module only when in Tauri context + const updaterModule = '@tauri-apps/api/updater'; + const { installUpdate } = await import(/* @vite-ignore */ updaterModule); + await installUpdate(); + // The app will be restarted by Tauri if successful + } catch (error) { + console.error('Error installing update:', error); + throw error; + } +} \ No newline at end of file diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts new file mode 100644 index 0000000..8abcb26 --- /dev/null +++ b/src/utils/formatters.ts @@ -0,0 +1,61 @@ +import { ServerType } from '../types/server.types'; +import { Plugin } from '../types/plugin.types'; + +/** + * Formats a list of authors into a comma-separated string + * @param authors Array of author names + * @returns Formatted authors string + */ +export function formatAuthors(authors: string[]): string { + if (!authors || authors.length === 0) { + return 'Unknown'; + } + + return authors.join(', '); +} + +/** + * Formats platform compatibility information into a user-friendly label + * @param plugin The plugin object + * @param currentServerType The current server type + * @returns A formatted compatibility string + */ +export function getPlatformCompatibilityLabel(plugin: Plugin, currentServerType?: ServerType): string { + const platformCompatibility = plugin.platform_compatibility || []; + if (platformCompatibility.length === 0) { + return ''; + } + + // Check if current server type is in the compatibility list + const isCompatible = currentServerType && + platformCompatibility.includes(currentServerType); + + if (isCompatible) { + return `Compatible with your ${currentServerType} server`; + } else if (currentServerType) { + return `Compatible with ${platformCompatibility.join(', ')} (You're using ${currentServerType})`; + } else { + return `Compatible with ${platformCompatibility.join(', ')}`; + } +} + +/** + * Creates a status message for plugin updates + * @param pluginName Name of the plugin + * @param version Version being updated to + * @param platformCompatibility Array of compatible platforms + * @returns Formatted update message + */ +export function createUpdateMessage(pluginName: string, version?: string, platformCompatibility?: string[]): string { + if (!version) { + return `Updating ${pluginName}`; + } + + let updateMessage = `Updating ${pluginName} to version ${version}`; + + if (platformCompatibility && platformCompatibility.length > 0) { + updateMessage += ` (${platformCompatibility.join(', ')} compatible)`; + } + + return updateMessage; +} \ No newline at end of file diff --git a/src/utils/serverUtils.ts b/src/utils/serverUtils.ts new file mode 100644 index 0000000..8c06161 --- /dev/null +++ b/src/utils/serverUtils.ts @@ -0,0 +1,54 @@ +import { ServerType } from '../types/server.types'; + +/** + * Returns an emoji icon representing the server type + * @param serverType The type of Minecraft server + * @returns A string emoji representing the server type + */ +export function getServerTypeIcon(serverType: ServerType): string { + switch (serverType) { + case 'Paper': + return '📄'; + case 'Spigot': + return '🔌'; + case 'Bukkit': + return '🪣'; + case 'Vanilla': + return '🧊'; + case 'Forge': + return '🔨'; + case 'Fabric': + return '🧵'; + case 'Velocity': + return '⚡'; + case 'BungeeCord': + return '🔗'; + case 'Waterfall': + return '🌊'; + default: + return '❓'; + } +} + +/** + * Returns a formatted display name for the server type + * @param serverType The type of Minecraft server + * @returns A formatted string for display + */ +export function getServerTypeName(serverType: ServerType): string { + return serverType === 'Unknown' ? 'Unknown Server' : serverType; +} + +/** + * Checks if a plugin is compatible with the current server type + * @param compatibility Array of compatible server types + * @param currentServerType The current server type + * @returns True if compatible, false otherwise + */ +export function isCompatibleWithServer(compatibility: string[] | undefined, currentServerType: ServerType | undefined): boolean { + if (!compatibility || compatibility.length === 0 || !currentServerType) { + return false; + } + + return compatibility.includes(currentServerType); +} \ No newline at end of file diff --git a/src/utils/validators.ts b/src/utils/validators.ts new file mode 100644 index 0000000..4508f4c --- /dev/null +++ b/src/utils/validators.ts @@ -0,0 +1,63 @@ +import { Plugin } from '../types/plugin.types'; + +/** + * Check if a plugin can be updated + * @param plugin The plugin to check + * @returns True if the plugin can be updated, false otherwise + */ +export function canUpdatePlugin(plugin: Plugin): boolean { + return !!( + plugin.has_update && + plugin.latest_version && + (plugin.repository_source || plugin.repository_id) + ); +} + +/** + * Check if a plugin is from a premium source that requires manual download + * @param errorMessage The error message to check + * @returns True if the plugin is premium, false otherwise + */ +export function isPremiumPluginError(errorMessage: string): boolean { + return errorMessage.startsWith("PREMIUM_RESOURCE:"); +} + +/** + * Extract premium plugin information from an error message + * @param errorMessage The error message to parse + * @returns An object with plugin id, version, and resource URL or null if parsing fails + */ +export function extractPremiumPluginInfo(errorMessage: string): { pluginId?: string, version?: string, resourceUrl: string } | null { + if (!isPremiumPluginError(errorMessage)) { + return null; + } + + let resourceUrl = ""; + let pluginId = ""; + let version = ""; + + // Handle the format with all parts (PREMIUM_RESOURCE:id:version:url) + const parts = errorMessage.split(":"); + if (parts.length >= 4) { + pluginId = parts[1]; + version = parts[2]; + resourceUrl = parts.slice(3).join(":"); // Rejoin in case URL contains colons + return { pluginId, version, resourceUrl }; + } + // Handle the simpler format (PREMIUM_RESOURCE:url) + else if (parts.length >= 2) { + resourceUrl = parts.slice(1).join(":"); // Get everything after PREMIUM_RESOURCE: + return { resourceUrl }; + } + + return null; +} + +/** + * Check if a path string is empty or invalid + * @param path The path to validate + * @returns True if the path is valid, false otherwise + */ +export function isValidPath(path: string | undefined): boolean { + return !!path && path.trim().length > 0; +} \ No newline at end of file