Compare commits
No commits in common. "61becf8d22fa1dafbb8363a2aef610ffced5e248" and "7b772bb1bb96fcb2f8636ca315177e0197985c41" have entirely different histories.
61becf8d22
...
7b772bb1bb
@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
description:
|
|
||||||
globs:
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
You must update your current roadmap file before/after EVERY Change.
|
|
@ -1,211 +0,0 @@
|
|||||||
# 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.
|
|
22
ROADMAP.md
@ -57,9 +57,7 @@
|
|||||||
- [x] Improve matching algorithm to reduce false positives
|
- [x] Improve matching algorithm to reduce false positives
|
||||||
- [x] Complete GitHub integration
|
- [x] Complete GitHub integration
|
||||||
- [ ] Add GitHub API authentication for higher rate limits (environment variable support exists)
|
- [ ] Add GitHub API authentication for higher rate limits (environment variable support exists)
|
||||||
- [x] Fix command parameter naming issues for update checks
|
- [ ] 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)
|
- [ ] Optimize duplicate plugin search results (e.g., ViaVersion plugin)
|
||||||
- [x] Implement changelog extraction
|
- [x] Implement changelog extraction
|
||||||
- [x] Create plugin backup functionality
|
- [x] Create plugin backup functionality
|
||||||
@ -74,14 +72,14 @@
|
|||||||
- [x] Display up-to-date version information for all plugins
|
- [x] Display up-to-date version information for all plugins
|
||||||
- [x] Handle premium plugins with user guidance for manual downloads
|
- [x] Handle premium plugins with user guidance for manual downloads
|
||||||
- [ ] Present multiple potential matches for ambiguous plugins
|
- [ ] Present multiple potential matches for ambiguous plugins
|
||||||
- [x] Make version numbers clickable links to repository sources
|
- [ ] Make version numbers clickable links to repository sources
|
||||||
- [ ] Allow user selection of correct plugin match when multiple are found
|
- [ ] Allow user selection of correct plugin match when multiple are found
|
||||||
- [x] Server platform compatibility matching
|
- [ ] Server platform compatibility matching (High Priority)
|
||||||
- [x] Detect server platform and version accurately (Paper, Spigot, Forge, NeoForge, Fabric, etc.)
|
- [ ] Detect server platform and version accurately (Paper, Spigot, Forge, NeoForge, Fabric, etc.)
|
||||||
- [x] Filter plugin updates to match the server platform
|
- [ ] Filter plugin updates to match the server platform
|
||||||
- [x] Prevent incompatible version updates
|
- [ ] Prevent incompatible version updates
|
||||||
- [x] Add compatibility indicators in the code for available updates
|
- [ ] Add platform indicators in the UI for available updates
|
||||||
- [x] Add platform indicators in the UI for available updates
|
- [ ] Allow manual selection of target platform when multiple are available
|
||||||
|
|
||||||
## UI Development (In Progress)
|
## UI Development (In Progress)
|
||||||
- [x] Design and implement main dashboard
|
- [x] Design and implement main dashboard
|
||||||
@ -97,11 +95,9 @@
|
|||||||
- [ ] Create settings panel
|
- [ ] Create settings panel
|
||||||
- [x] Implement dark mode
|
- [x] Implement dark mode
|
||||||
- [ ] Implement plugin matching disambiguation UI
|
- [ ] Implement plugin matching disambiguation UI
|
||||||
- [x] Add clickable version links to repository pages
|
- [ ] Add clickable version links to repository pages
|
||||||
- [ ] Add error recovery UI for failed updates
|
- [ ] Add error recovery UI for failed updates
|
||||||
- [ ] Add detailed progress logging in UI for debugging
|
- [ ] Add detailed progress logging in UI for debugging
|
||||||
- [x] Implement application update checking functionality
|
|
||||||
- [x] Display application update notifications
|
|
||||||
|
|
||||||
## Security Features (Upcoming)
|
## Security Features (Upcoming)
|
||||||
- [ ] Implement sandboxing for network requests
|
- [ ] Implement sandboxing for network requests
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
description:
|
|
||||||
globs:
|
|
||||||
alwaysApply: false
|
|
||||||
---
|
|
||||||
|
|
@ -2,10 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/tauri.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="PlugSnatcher - A tool for managing and updating Minecraft server plugins" />
|
<title>Tauri + React + Typescript</title>
|
||||||
<title>PlugSnatcher - Minecraft Server Plugin Manager</title>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
26
package-lock.json
generated
@ -8,15 +8,14 @@
|
|||||||
"name": "plugsnatcher",
|
"name": "plugsnatcher",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.4.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.0.0",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.0.0",
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.1",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
@ -1304,19 +1303,10 @@
|
|||||||
"@tauri-apps/api": "^2.0.0"
|
"@tauri-apps/api": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/plugin-fs": {
|
"node_modules/@tauri-apps/plugin-opener": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.6.tgz",
|
||||||
"integrity": "sha512-+08mApuONKI8/sCNEZ6AR8vf5vI9DXD4YfrQ9NQmhRxYKMLVhRW164vdW5BSLmMpuevftpQ2FVoL9EFkfG9Z+g==",
|
"integrity": "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==",
|
||||||
"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",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0"
|
"@tauri-apps/api": "^2.0.0"
|
||||||
|
12
package.json
@ -7,19 +7,17 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri"
|
||||||
"typecheck": "tsc --noEmit"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.4.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.0.0",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.0.0",
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.1",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
3
src-tauri/.gitignore
vendored
@ -1,4 +1,7 @@
|
|||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
/target/
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
/gen/schemas
|
/gen/schemas
|
||||||
|
1262
src-tauri/Cargo.lock
generated
@ -1,48 +1,48 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "plugsnatcher"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
|
||||||
repository = ""
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.77.2"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "app_lib"
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "plugsnatcher_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.1.0", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Tauri dependencies
|
tauri = { version = "2", features = [] }
|
||||||
serde_json = "1.0"
|
tauri-plugin-opener = "2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
tauri-plugin-dialog = "2"
|
||||||
log = "0.4"
|
serde = { version = "1", features = ["derive"] }
|
||||||
tauri = { version = "2.4.0", features = [] }
|
serde_json = "1"
|
||||||
tauri-plugin-log = "2.0.0-rc"
|
zip = "0.6"
|
||||||
tauri-plugin-shell = "~2.0"
|
yaml-rust = "0.4"
|
||||||
tauri-plugin-dialog = "~2.0"
|
# walkdir = "2.4" # Not currently used, commented out
|
||||||
tauri-plugin-fs = "~2.0"
|
regex = "1.10" # Still needed elsewhere in the codebase
|
||||||
|
sha2 = "0.10"
|
||||||
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } # Updated version, enabled rustls
|
||||||
|
# scraper = "0.19.0" # No longer used for SpigotMCCrawler
|
||||||
|
urlencoding = "2.1.3" # Reverted version
|
||||||
|
semver = "1.0"
|
||||||
|
url = "2.5"
|
||||||
|
futures = "0.3"
|
||||||
|
async-trait = "0.1"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } # Changed features from "full"
|
||||||
|
# --- Add Caching Dependencies ---
|
||||||
|
cached = { version = "0.52", features = ["proc_macro", "async", "tokio"] }
|
||||||
|
async-mutex = "1.4" # For locking cache access within HttpClient
|
||||||
|
# --- End Caching Dependencies ---
|
||||||
|
base64 = "0.21" # For decoding SpigotMC changelog data
|
||||||
|
walkdir = "2.5.0"
|
||||||
|
|
||||||
# Plugin scanner dependencies
|
[features]
|
||||||
walkdir = "2.3.3"
|
# default = ["custom-protocol"]
|
||||||
regex = "1.9.1"
|
|
||||||
yaml-rust = "0.4.5"
|
|
||||||
zip = "0.6.6"
|
|
||||||
sha2 = "0.10.7"
|
|
||||||
|
|
||||||
# Network and async dependencies
|
|
||||||
reqwest = { version = "0.11.18", features = ["json"] }
|
|
||||||
tokio = { version = "1.29.1", features = ["full"] }
|
|
||||||
futures = "0.3.28"
|
|
||||||
async-trait = "0.1.71"
|
|
||||||
cached = "0.44.0"
|
|
||||||
urlencoding = "2.1.2"
|
|
||||||
base64 = "0.21.2"
|
|
||||||
|
|
||||||
# Version management
|
|
||||||
semver = "1.0.18"
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,12 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Default capability set for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": [
|
"windows": ["main"],
|
||||||
"main"
|
|
||||||
],
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{ "identifier": "core:default" },
|
"core:default",
|
||||||
{ "identifier": "dialog:default" },
|
"opener:default",
|
||||||
{ "identifier": "dialog:allow-open" },
|
"dialog:default",
|
||||||
{ "identifier": "shell:default" },
|
"dialog:allow-open"
|
||||||
{ "identifier": "shell:allow-open" },
|
|
||||||
{ "identifier": "fs:default" },
|
|
||||||
{
|
|
||||||
"identifier": "fs:allow-read-dir",
|
|
||||||
"allow": ["**", "//**"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identifier": "fs:allow-read-file",
|
|
||||||
"allow": ["**", "//**"]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 974 B |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 903 B |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 14 KiB |
@ -289,51 +289,39 @@ pub async fn lib_download_plugin_from_repository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tauri v2 plugin initialization
|
/// Configure and run the Tauri application
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
// Build the Tauri application
|
||||||
.plugin(tauri_plugin_shell::init())
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.invoke_handler(tauri::generate_handler![
|
||||||
.setup(|app| {
|
// Plugin discovery commands
|
||||||
if cfg!(debug_assertions) {
|
scan_server_dir,
|
||||||
app.handle().plugin(
|
scan_server_dir_sync,
|
||||||
tauri_plugin_log::Builder::default()
|
|
||||||
.level(log::LevelFilter::Info)
|
|
||||||
.build(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
|
||||||
// Plugin discovery commands
|
|
||||||
scan_server_dir,
|
|
||||||
scan_server_dir_sync,
|
|
||||||
|
|
||||||
// Plugin repository commands
|
// Plugin repository commands
|
||||||
search_plugins,
|
search_plugins,
|
||||||
get_plugin_details,
|
get_plugin_details,
|
||||||
|
|
||||||
// Update commands
|
// Update commands
|
||||||
update_plugin,
|
update_plugin,
|
||||||
check_plugin_updates,
|
check_plugin_updates,
|
||||||
check_single_plugin_update_command,
|
check_single_plugin_update_command,
|
||||||
backup_plugin_command,
|
backup_plugin_command,
|
||||||
|
|
||||||
// Plugin management commands
|
// Plugin management commands
|
||||||
download_plugin,
|
download_plugin,
|
||||||
set_plugin_repository,
|
set_plugin_repository,
|
||||||
get_plugin_versions,
|
get_plugin_versions,
|
||||||
load_plugin_data,
|
load_plugin_data,
|
||||||
save_plugin_data,
|
save_plugin_data,
|
||||||
|
|
||||||
// Utility commands
|
// Utility commands
|
||||||
get_potential_plugin_matches,
|
get_potential_plugin_matches,
|
||||||
compare_versions,
|
compare_versions,
|
||||||
is_plugin_compatible,
|
is_plugin_compatible,
|
||||||
greet
|
greet
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
app_lib::run();
|
plugsnatcher_lib::run();
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,28 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"identifier": "com.plugsnatcher.app",
|
|
||||||
"productName": "PlugSnatcher",
|
"productName": "PlugSnatcher",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.plugsnatcher.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
"beforeBuildCommand": "npm run build",
|
"beforeBuildCommand": "npm run build",
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist"
|
||||||
"devUrl": "http://localhost:1420"
|
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"security": {
|
|
||||||
"csp": null
|
|
||||||
},
|
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"label": "main",
|
|
||||||
"title": "PlugSnatcher",
|
"title": "PlugSnatcher",
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"minWidth": 800,
|
"minWidth": 800,
|
||||||
"minHeight": 600,
|
"minHeight": 600,
|
||||||
"resizable": true,
|
|
||||||
"fullscreen": false,
|
|
||||||
"center": true
|
"center": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
@ -39,10 +36,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"shell": {
|
|
||||||
"open": true
|
|
||||||
},
|
|
||||||
"dialog": null,
|
"dialog": null,
|
||||||
"fs": null
|
"opener": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
742
src/App.css
@ -11,29 +11,18 @@
|
|||||||
--background-color: #202124;
|
--background-color: #202124;
|
||||||
--surface-color: #292a2d;
|
--surface-color: #292a2d;
|
||||||
--text-color: #e8eaed;
|
--text-color: #e8eaed;
|
||||||
--text-secondary-color: #a8adb4;
|
--text-secondary-color: #9aa0a6;
|
||||||
--border-color: #3c4043;
|
--border-color: #3c4043;
|
||||||
--error-color: #f44336;
|
--error-color: #f44336;
|
||||||
--warning-color: #ff9800;
|
--warning-color: #ff9800;
|
||||||
--success-color: #4caf50;
|
--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-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.5;
|
line-height: 24px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color: var(--text-color);
|
color: #0f0f0f;
|
||||||
background-color: var(--background-color);
|
background-color: #f6f6f6;
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
@ -49,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
@ -62,58 +51,45 @@ body {
|
|||||||
|
|
||||||
.app-header {
|
.app-header {
|
||||||
background-color: var(--surface-color);
|
background-color: var(--surface-color);
|
||||||
padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 3);
|
padding: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header h1 {
|
.app-header h1 {
|
||||||
font-size: 1.8rem;
|
font-size: 2rem;
|
||||||
margin-bottom: var(--spacing-unit);
|
margin-bottom: 0.5rem;
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-subtitle {
|
|
||||||
color: var(--text-secondary-color);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: calc(var(--spacing-unit) * 3);
|
padding: 1rem;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-section {
|
|
||||||
margin-bottom: calc(var(--spacing-unit) * 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-selector {
|
.server-selector {
|
||||||
background-color: var(--surface-color);
|
background-color: var(--surface-color);
|
||||||
padding: calc(var(--spacing-unit) * 3);
|
padding: 1.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: calc(var(--spacing-unit) * 3);
|
margin-bottom: 1.5rem;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-selector h2 {
|
.server-selector h2 {
|
||||||
margin-bottom: calc(var(--spacing-unit) * 2);
|
margin-bottom: 1rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group {
|
.input-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: var(--spacing-unit);
|
margin-bottom: 1rem;
|
||||||
gap: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group input {
|
.input-group input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: calc(var(--spacing-unit) * 1.5);
|
padding: 0.75rem;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
@ -122,145 +98,39 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-group button {
|
.input-group button {
|
||||||
padding: calc(var(--spacing-unit) * 1.5);
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: var(--primary-color);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 0 4px 4px 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
transition: background-color 0.3s;
|
||||||
transition: background-color 0.3s, opacity 0.3s;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.2;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group button:hover {
|
.input-group button:hover {
|
||||||
opacity: 0.9;
|
background-color: #1967d2;
|
||||||
background-color: color-mix(in srgb, var(--primary-color) 90%, black);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-button,
|
.scan-button {
|
||||||
.update-button,
|
padding: 0.75rem 1.5rem;
|
||||||
.info-button,
|
background-color: var(--secondary-color);
|
||||||
.check-button,
|
|
||||||
.continue-button,
|
|
||||||
.cancel-button,
|
|
||||||
.close-modal-button,
|
|
||||||
.plugin-page-button {
|
|
||||||
padding: calc(var(--spacing-unit) * 1.5);
|
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1rem;
|
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%;
|
width: 100%;
|
||||||
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-button:hover {
|
.scan-button:hover {
|
||||||
background-color: color-mix(in srgb, var(--secondary-color) 90%, black);
|
background-color: #43a047;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-button:disabled {
|
.scan-button:disabled {
|
||||||
background-color: var(--text-secondary-color);
|
background-color: #666;
|
||||||
cursor: not-allowed;
|
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 {
|
.plugins-list {
|
||||||
@ -287,33 +157,34 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-info {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-version {
|
.plugin-version {
|
||||||
color: var(--text-secondary-color);
|
color: var(--text-secondary-color);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-available {
|
.update-available {
|
||||||
background-color: var(--success-color);
|
background-color: #4caf50;
|
||||||
color: white;
|
color: white;
|
||||||
padding: calc(var(--spacing-unit) * 0.5) var(--spacing-unit);
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-left: var(--spacing-unit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugins-count {
|
.plugins-count {
|
||||||
margin-top: 0.5rem;
|
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 {
|
.plugin-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1fr 1fr 1fr;
|
grid-template-columns: 2fr 1fr 1fr 1fr;
|
||||||
@ -331,42 +202,45 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plugin-actions {
|
.plugin-actions {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-actions > div {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-button {
|
.update-button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
background-color: var(--warning-color);
|
background-color: var(--warning-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-button:hover {
|
.update-button:hover {
|
||||||
background-color: color-mix(in srgb, var(--warning-color) 90%, black);
|
background-color: #f57c00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-button {
|
.info-button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-button:hover {
|
.info-button:hover {
|
||||||
background-color: color-mix(in srgb, var(--primary-color) 90%, black);
|
background-color: #1967d2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-footer {
|
.app-footer {
|
||||||
background-color: var(--surface-color);
|
background-color: var(--surface-color);
|
||||||
padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 3);
|
padding: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
margin-top: calc(var(--spacing-unit) * 4);
|
margin-top: auto;
|
||||||
color: var(--text-secondary-color);
|
color: var(--text-secondary-color);
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@ -563,8 +437,9 @@ button {
|
|||||||
|
|
||||||
.server-info {
|
.server-info {
|
||||||
background-color: var(--surface-color);
|
background-color: var(--surface-color);
|
||||||
padding: calc(var(--spacing-unit) * 2.5);
|
padding: 1.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -683,7 +558,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.update-available-badge {
|
.update-available-badge {
|
||||||
background-color: var(--warning-color);
|
background-color: var(--secondary-color);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -691,58 +566,22 @@ button {
|
|||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.up-to-date-badge {
|
||||||
|
color: var(--success-color, #4caf50);
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.up-to-date-text {
|
.up-to-date-text {
|
||||||
color: var(--success-color);
|
color: var(--success-color, #4caf50);
|
||||||
display: inline-flex;
|
font-size: 0.85em;
|
||||||
align-items: center;
|
margin-left: 0.4rem;
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
.bulk-update-progress {
|
||||||
margin-top: 1rem;
|
margin-top: 0.5rem;
|
||||||
background-color: var(--surface-color);
|
font-size: 0.9rem;
|
||||||
padding: 1rem;
|
color: var(--text-secondary-color);
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-update-progress progress {
|
.bulk-update-progress progress {
|
||||||
@ -877,448 +716,3 @@ button {
|
|||||||
.close-modal-button:hover {
|
.close-modal-button:hover {
|
||||||
background-color: var(--surface-hover-color, #616161);
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
1094
src/App.tsx
@ -1,143 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
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<BadgeProps> = ({
|
|
||||||
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<HTMLSpanElement>) => {
|
|
||||||
if (onClick && (event.key === 'Enter' || event.key === ' ')) {
|
|
||||||
event.preventDefault(); // Prevent default space bar scroll
|
|
||||||
onClick();
|
|
||||||
}
|
|
||||||
}, [onClick]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={badgeClasses}
|
|
||||||
title={tooltip}
|
|
||||||
onClick={onClick}
|
|
||||||
onKeyDown={onClick ? handleKeyDown : undefined}
|
|
||||||
tabIndex={onClick ? 0 : undefined}
|
|
||||||
role={onClick ? 'button' : undefined}
|
|
||||||
{...dataAttrs}
|
|
||||||
>
|
|
||||||
{icon && <span className="app-badge-icon">{icon}</span>}
|
|
||||||
<span className="app-badge-label">{label}</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(BadgeComponent);
|
|
@ -1,151 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
import React, { memo } from 'react';
|
|
||||||
import './Button.css';
|
|
||||||
|
|
||||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
||||||
/**
|
|
||||||
* 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<ButtonProps> = ({
|
|
||||||
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 (
|
|
||||||
<button
|
|
||||||
className={buttonClasses}
|
|
||||||
disabled={disabled || isLoading}
|
|
||||||
aria-busy={isLoading}
|
|
||||||
title={tooltip}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{isLoading && <span role="status" className="app-button-spinner"></span>}
|
|
||||||
{startIcon && !isLoading && <span className="app-button-start-icon">{startIcon}</span>}
|
|
||||||
<span className="app-button-text">{children}</span>
|
|
||||||
{endIcon && !isLoading && <span className="app-button-end-icon">{endIcon}</span>}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(ButtonComponent);
|
|
@ -1,102 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,114 +0,0 @@
|
|||||||
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<ModalProps> = ({
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
className = '',
|
|
||||||
showCloseButton = true,
|
|
||||||
closeOnOutsideClick = true
|
|
||||||
}) => {
|
|
||||||
const modalRef = useRef<HTMLDivElement>(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<HTMLDivElement>) => {
|
|
||||||
if (closeOnOutsideClick && modalRef.current && !modalRef.current.contains(event.target as Node)) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="modal-backdrop" onClick={handleBackdropClick}>
|
|
||||||
<div
|
|
||||||
className={`modal-container ${className}`}
|
|
||||||
ref={modalRef}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby={title ? "modal-title" : undefined}
|
|
||||||
aria-describedby="modal-content"
|
|
||||||
>
|
|
||||||
{(title || showCloseButton) && (
|
|
||||||
<div className="modal-header">
|
|
||||||
{title && <h3 className="modal-title" id="modal-title">{title}</h3>}
|
|
||||||
{showCloseButton && (
|
|
||||||
<button
|
|
||||||
className="modal-close-button"
|
|
||||||
onClick={onClose}
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="modal-content" id="modal-content">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Modal;
|
|
@ -1,51 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
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 (
|
|
||||||
<div className={`notification-display notification-${type}`}>
|
|
||||||
<span className="notification-text">{text}</span>
|
|
||||||
{/* Add a close button only for persistent errors? */}
|
|
||||||
{type === 'error' && (
|
|
||||||
<button
|
|
||||||
className="notification-close-button"
|
|
||||||
onClick={clearWarningMessage}
|
|
||||||
aria-label="Close notification"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NotificationDisplay;
|
|
@ -1,69 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
@ -1,114 +0,0 @@
|
|||||||
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<ProgressBarProps> = ({
|
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
className={`progress-container ${className}`}
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
{label && <div className="progress-label" id={`progress-label-${label}`}>{label}</div>}
|
|
||||||
<div
|
|
||||||
className="progress-bar-wrapper"
|
|
||||||
role="progressbar"
|
|
||||||
aria-valuenow={value}
|
|
||||||
aria-valuemin={min}
|
|
||||||
aria-valuemax={max}
|
|
||||||
aria-label={label ? undefined : 'Progress'}
|
|
||||||
aria-labelledby={label ? `progress-label-${label}` : undefined}
|
|
||||||
aria-valuetext={showValues ? formattedValue : `${percentage}%`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`progress-bar progress-bar-${color}`}
|
|
||||||
style={{ width: `${percentage}%` }}
|
|
||||||
>
|
|
||||||
{showPercentage && (
|
|
||||||
<span className="progress-percentage">{percentage}%</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{showValues && (
|
|
||||||
<div className="progress-values" aria-hidden="true">{formattedValue}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProgressBar;
|
|
@ -1,62 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
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<FooterProps> = ({ 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 (
|
|
||||||
<footer className="footer">
|
|
||||||
<div className="footer-left">
|
|
||||||
<span className="footer-version">PlugSnatcher v{appVersion}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="footer-right">
|
|
||||||
<a
|
|
||||||
href="https://git.spacetrainclubhouse.com/Space-Train-Clubhouse/PlugSnatcher"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="github-link"
|
|
||||||
>
|
|
||||||
Gitea
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Footer;
|
|
@ -1,112 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
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<HeaderProps> = ({ appVersion = '1.0.0' }) => {
|
|
||||||
const { isCheckingAppUpdate, appUpdateAvailable, checkForAppUpdate } = useAppUpdates();
|
|
||||||
const { serverPath } = useServerContext();
|
|
||||||
const { isCheckingUpdates } = usePluginContext();
|
|
||||||
|
|
||||||
const handleCheckForUpdates = async () => {
|
|
||||||
if (!isCheckingAppUpdate) {
|
|
||||||
await checkForAppUpdate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="header">
|
|
||||||
<div className="header-left">
|
|
||||||
<h1 className="app-title">PlugSnatcher</h1>
|
|
||||||
<span className="app-version">v{appVersion}</span>
|
|
||||||
{appUpdateAvailable && (
|
|
||||||
<span className="update-badge">Update Available</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="header-center">
|
|
||||||
{serverPath && (
|
|
||||||
<div className="server-path-display">
|
|
||||||
<span className="server-label">Selected Server:</span>
|
|
||||||
<span className="server-path" title={serverPath}>
|
|
||||||
{serverPath.length > 40 ? `...${serverPath.slice(-40)}` : serverPath}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="header-right">
|
|
||||||
<button
|
|
||||||
className="update-check-button"
|
|
||||||
onClick={handleCheckForUpdates}
|
|
||||||
disabled={isCheckingAppUpdate || isCheckingUpdates}
|
|
||||||
>
|
|
||||||
{isCheckingAppUpdate ? 'Checking...' : 'Check for Updates'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
@ -1,78 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import React, { ReactNode } from 'react';
|
|
||||||
import { useUIContext } from '../../../context/UIContext/useUIContext';
|
|
||||||
import './MainContent.css';
|
|
||||||
|
|
||||||
interface MainContentProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MainContent: React.FC<MainContentProps> = ({ children }) => {
|
|
||||||
const { warningMessage, clearWarningMessage } = useUIContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="main-content">
|
|
||||||
{warningMessage && (
|
|
||||||
<div className={`warning-message warning-${warningMessage.type}`}>
|
|
||||||
<span className="warning-text">{warningMessage.text}</span>
|
|
||||||
<button
|
|
||||||
className="close-warning"
|
|
||||||
onClick={clearWarningMessage}
|
|
||||||
aria-label="Close message"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="content-container">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MainContent;
|
|
@ -1,74 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
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 (
|
|
||||||
<div className="no-plugins-message scanning">
|
|
||||||
<div className="spinner"></div>
|
|
||||||
<h3>Scanning for plugins...</h3>
|
|
||||||
<p>This might take a moment, please wait.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!serverPath) {
|
|
||||||
return (
|
|
||||||
<div className="no-plugins-message no-server">
|
|
||||||
<div className="icon">📁</div>
|
|
||||||
<h3>No Server Selected</h3>
|
|
||||||
<p>Please select a Minecraft server directory to get started.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scanComplete && !isScanning) {
|
|
||||||
return (
|
|
||||||
<div className="no-plugins-message empty">
|
|
||||||
<div className="icon">🔍</div>
|
|
||||||
<h3>No Plugins Found</h3>
|
|
||||||
<p>We couldn't find any plugins in the selected server directory.</p>
|
|
||||||
<p className="suggestion">Make sure you've selected a valid Minecraft server with plugins installed.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default message if we're in an intermediate state
|
|
||||||
return (
|
|
||||||
<div className="no-plugins-message">
|
|
||||||
<div className="icon">🧩</div>
|
|
||||||
<h3>Ready to Scan</h3>
|
|
||||||
<p>Click "Scan for Plugins" to discover plugins in your server.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NoPluginsMessage;
|
|
@ -1,228 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,291 +0,0 @@
|
|||||||
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<PluginDetailsProps> = ({ 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 <p className="no-dependencies">No dependencies</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className="dependency-list">
|
|
||||||
{dependencies.map((dep, index) => (
|
|
||||||
<li key={index}>
|
|
||||||
<span className="dependency-name">{dep}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSizeInMB = (bytes: number | undefined): string => {
|
|
||||||
if (!bytes) return 'Unknown';
|
|
||||||
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="plugin-details-overlay" onClick={handleOverlayClick}>
|
|
||||||
<div className="plugin-details-container" role="dialog" aria-labelledby="plugin-details-title">
|
|
||||||
<div className="plugin-details-header">
|
|
||||||
<div>
|
|
||||||
<h2 id="plugin-details-title" className="plugin-name">{plugin.name}</h2>
|
|
||||||
<div className="plugin-version-info">
|
|
||||||
<span className="version">
|
|
||||||
Version: {
|
|
||||||
plugin.repository_url || (plugin.repository_source && plugin.repository_id) ? (
|
|
||||||
<a
|
|
||||||
href={getRepositoryUrl(plugin) || '#'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="version-link"
|
|
||||||
>
|
|
||||||
{plugin.version}
|
|
||||||
</a>
|
|
||||||
) : plugin.version
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
{plugin.has_update && plugin.latest_version && (
|
|
||||||
<div className="update-info">
|
|
||||||
<Badge
|
|
||||||
label="Update Available"
|
|
||||||
variant="warning"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<span className="latest-version">
|
|
||||||
Latest: {
|
|
||||||
plugin.repository_url || (plugin.repository_source && plugin.repository_id) ? (
|
|
||||||
<a
|
|
||||||
href={getRepositoryUrl(plugin) || '#'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="version-link"
|
|
||||||
>
|
|
||||||
{plugin.latest_version}
|
|
||||||
</a>
|
|
||||||
) : plugin.latest_version
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="close-button"
|
|
||||||
onClick={onClose}
|
|
||||||
aria-label="Close details"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-details-content">
|
|
||||||
<div className="plugin-details-section">
|
|
||||||
<h3>Description</h3>
|
|
||||||
<p className="plugin-description">
|
|
||||||
{plugin.description || 'No description available.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-details-columns">
|
|
||||||
<div className="plugin-details-column">
|
|
||||||
<div className="plugin-details-section">
|
|
||||||
<h3>Author Information</h3>
|
|
||||||
<p className="plugin-authors">
|
|
||||||
{plugin.authors && plugin.authors.length > 0
|
|
||||||
? plugin.authors.join(', ')
|
|
||||||
: 'Unknown author'}
|
|
||||||
</p>
|
|
||||||
{plugin.website && (
|
|
||||||
<a
|
|
||||||
href={plugin.website}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="plugin-website"
|
|
||||||
>
|
|
||||||
Visit Website
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-details-section">
|
|
||||||
<h3>Dependencies</h3>
|
|
||||||
{renderDependencies()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-details-column">
|
|
||||||
<div className="plugin-details-section">
|
|
||||||
<h3>File Information</h3>
|
|
||||||
<div className="file-info-item">
|
|
||||||
<span className="file-info-label">Path:</span>
|
|
||||||
<span className="file-info-value file-path">{plugin.file_path}</span>
|
|
||||||
</div>
|
|
||||||
<div className="file-info-item">
|
|
||||||
<span className="file-info-label">Hash:</span>
|
|
||||||
<span className="file-info-value">{plugin.file_hash.substring(0, 10)}...</span>
|
|
||||||
</div>
|
|
||||||
{plugin.repository_source && (
|
|
||||||
<div className="file-info-item">
|
|
||||||
<span className="file-info-label">Source:</span>
|
|
||||||
<span className="file-info-value">
|
|
||||||
{plugin.repository_url || getRepositoryUrl(plugin) ? (
|
|
||||||
<a
|
|
||||||
href={getRepositoryUrl(plugin) || '#'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{plugin.repository_source}
|
|
||||||
</a>
|
|
||||||
) : plugin.repository_source}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{plugin.platform_compatibility && Object.keys(plugin.platform_compatibility).length > 0 && (
|
|
||||||
<div className="file-info-item">
|
|
||||||
<span className="file-info-label">Compatible with:</span>
|
|
||||||
<div className="platform-badges">
|
|
||||||
{Object.entries(plugin.platform_compatibility).map(([platform, isCompatible]) => (
|
|
||||||
<span
|
|
||||||
key={platform}
|
|
||||||
className="platform-badge"
|
|
||||||
data-server-type={platform}
|
|
||||||
title={`${isCompatible ? 'Compatible' : 'May not be compatible'} with ${platform}`}
|
|
||||||
>
|
|
||||||
{platform}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-actions-section">
|
|
||||||
{plugin.has_update ? (
|
|
||||||
<Button
|
|
||||||
variant="warning"
|
|
||||||
onClick={handleUpdate}
|
|
||||||
disabled={isLoading}
|
|
||||||
isLoading={isLoading}
|
|
||||||
tooltip={`Update ${plugin.name} to version ${plugin.latest_version}`}
|
|
||||||
aria-label={`Update ${plugin.name} to version ${plugin.latest_version}`}
|
|
||||||
>
|
|
||||||
Update to {plugin.latest_version}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleCheckUpdate}
|
|
||||||
disabled={isLoading}
|
|
||||||
isLoading={isLoading}
|
|
||||||
tooltip={`Check for updates for ${plugin.name}`}
|
|
||||||
aria-label={`Check for updates for ${plugin.name}`}
|
|
||||||
>
|
|
||||||
Check for Updates
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PluginDetails;
|
|
@ -1,172 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,159 +0,0 @@
|
|||||||
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<PluginItemProps> = ({ 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 (
|
|
||||||
<div className={`plugin-item ${plugin.has_update ? 'has-update' : ''}`}>
|
|
||||||
{/* Plugin Name Column */}
|
|
||||||
<div className="plugin-name-column">
|
|
||||||
<h3 className="plugin-name" title={plugin.name}>{plugin.name}</h3>
|
|
||||||
{plugin.authors && plugin.authors.length > 0 && (
|
|
||||||
<div className="plugin-authors">
|
|
||||||
By {plugin.authors.join(', ')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{plugin.description && (
|
|
||||||
<p className="plugin-description" title={plugin.description}>
|
|
||||||
{plugin.description.length > 100
|
|
||||||
? `${plugin.description.substring(0, 100)}...`
|
|
||||||
: plugin.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Version Column */}
|
|
||||||
<div className="plugin-version-column">
|
|
||||||
<div className="plugin-version">
|
|
||||||
<span className="current-version" title={`Current version: ${plugin.version}`}>v{plugin.version}</span>
|
|
||||||
{plugin.has_update && plugin.latest_version && (
|
|
||||||
<>
|
|
||||||
<span className="version-arrow">→</span>
|
|
||||||
<span className="latest-version" title={`Latest version: ${plugin.latest_version}`}>v{plugin.latest_version}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Compatibility Column */}
|
|
||||||
<div className="plugin-compatibility-column">
|
|
||||||
{compatibility ? (
|
|
||||||
<div className="platform-badges">
|
|
||||||
{Object.entries(plugin.platform_compatibility || {}).map(([platform, isCompatible]) => (
|
|
||||||
<span
|
|
||||||
key={platform}
|
|
||||||
className="platform-badge"
|
|
||||||
data-server-type={platform}
|
|
||||||
title={`${isCompatible ? 'Compatible' : 'May not be compatible'} with ${platform}`}
|
|
||||||
>
|
|
||||||
{platform}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="compatibility-unknown">Unknown</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions Column */}
|
|
||||||
<div className="plugin-actions-column">
|
|
||||||
<div className="plugin-actions">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={handleInfoClick}
|
|
||||||
className="info-button"
|
|
||||||
tooltip={`View details for ${plugin.name}`}
|
|
||||||
aria-label={`View details for ${plugin.name}`}
|
|
||||||
>
|
|
||||||
Info
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{plugin.has_update ? (
|
|
||||||
<Button
|
|
||||||
variant="warning"
|
|
||||||
size="small"
|
|
||||||
onClick={handleUpdateClick}
|
|
||||||
disabled={isLoading}
|
|
||||||
isLoading={isLoading}
|
|
||||||
className="update-button"
|
|
||||||
tooltip={`Update ${plugin.name} to version ${plugin.latest_version}`}
|
|
||||||
aria-label={`Update ${plugin.name} to version ${plugin.latest_version}`}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="small"
|
|
||||||
onClick={handleCheckUpdateClick}
|
|
||||||
disabled={isLoading}
|
|
||||||
isLoading={isLoading}
|
|
||||||
className="check-button"
|
|
||||||
tooltip={`Check for updates for ${plugin.name}`}
|
|
||||||
aria-label={`Check for updates for ${plugin.name}`}
|
|
||||||
>
|
|
||||||
Check
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PluginItem = memo(PluginItemComponent);
|
|
@ -1,164 +0,0 @@
|
|||||||
.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 */
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
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<string>('');
|
|
||||||
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 (
|
|
||||||
<div className="plugin-list-container">
|
|
||||||
<div className="search-container">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search plugins..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="search-input"
|
|
||||||
/>
|
|
||||||
{searchTerm && (
|
|
||||||
<button
|
|
||||||
className="clear-search"
|
|
||||||
onClick={() => setSearchTerm('')}
|
|
||||||
aria-label="Clear search"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-list-controls">
|
|
||||||
<div className="sort-controls">
|
|
||||||
<span className="sort-label">Sort by:</span>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="small"
|
|
||||||
className={`sort-button ${sortBy === 'name' ? 'active' : ''}`}
|
|
||||||
onClick={() => handleSort('name')}
|
|
||||||
>
|
|
||||||
Name{getSortIndicator('name')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="small"
|
|
||||||
className={`sort-button ${sortBy === 'version' ? 'active' : ''}`}
|
|
||||||
onClick={() => handleSort('version')}
|
|
||||||
>
|
|
||||||
Version{getSortIndicator('version')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="small"
|
|
||||||
className={`sort-button ${sortBy === 'update' ? 'active' : ''}`}
|
|
||||||
onClick={() => handleSort('update')}
|
|
||||||
>
|
|
||||||
Updates{getSortIndicator('update')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="filter-info">
|
|
||||||
Showing {filteredPlugins.length} of {plugins.length} plugins
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-list">
|
|
||||||
{sortedPlugins.length > 0 ? (
|
|
||||||
sortedPlugins.map(plugin => (
|
|
||||||
<PluginItem
|
|
||||||
key={plugin.file_path}
|
|
||||||
plugin={plugin}
|
|
||||||
onSelect={showPluginDetails}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="no-results">
|
|
||||||
<p>No plugins match your search.</p>
|
|
||||||
<Button variant="secondary" onClick={() => setSearchTerm('')}>Clear search</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PluginList;
|
|
@ -1,78 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
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<ScanProgressProps> = ({ 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 (
|
|
||||||
<div className="scan-progress">
|
|
||||||
<div className="scan-progress-header">
|
|
||||||
<span className="scan-progress-title">Scanning for plugins...</span>
|
|
||||||
<span className="scan-progress-percentage">{percentage}%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
value={percentage}
|
|
||||||
color="primary"
|
|
||||||
showPercentage={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{progress && (
|
|
||||||
<div className="scan-progress-details">
|
|
||||||
<span className="scan-progress-count">
|
|
||||||
{progress.current} of {progress.total} files processed
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ScanProgress;
|
|
@ -1,109 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
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<ServerInfoProps> = ({ 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 (
|
|
||||||
<div className="server-info-container">
|
|
||||||
<div className="server-info-header">
|
|
||||||
<div className="server-icon">{getServerTypeIcon()}</div>
|
|
||||||
<div className="server-details">
|
|
||||||
<h3 className="server-type-name">
|
|
||||||
{formatServerType(serverInfo.server_type)}
|
|
||||||
<Badge
|
|
||||||
label={serverInfo.minecraft_version || 'Unknown Version'}
|
|
||||||
variant={getStatusColor()}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
className="server-path clickable"
|
|
||||||
onClick={handleOpenDirectory}
|
|
||||||
title="Click to open folder"
|
|
||||||
>
|
|
||||||
Server directory <span className="folder-icon">📂</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="server-stats">
|
|
||||||
<div className="stat-item">
|
|
||||||
<span className="stat-label">Plugins Directory</span>
|
|
||||||
<span className="stat-value">{serverInfo.plugins_directory}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stat-item">
|
|
||||||
<span className="stat-label">Plugins Found</span>
|
|
||||||
<span className="stat-value">{serverInfo.plugins_count}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServerInfo;
|
|
@ -1,32 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
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 (
|
|
||||||
<div className="server-selector">
|
|
||||||
<div className="server-actions">
|
|
||||||
<Button
|
|
||||||
onClick={handleSelectDirectory}
|
|
||||||
disabled={isScanning}
|
|
||||||
variant="primary"
|
|
||||||
tooltip="Choose the root folder of your Minecraft server"
|
|
||||||
>
|
|
||||||
Select Server Directory
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{serverPath && !isScanning && (
|
|
||||||
<Button
|
|
||||||
onClick={handleScanForPlugins}
|
|
||||||
disabled={!serverPath || isScanning}
|
|
||||||
isLoading={isScanning}
|
|
||||||
variant="success"
|
|
||||||
tooltip="Scan the selected server directory for plugins"
|
|
||||||
>
|
|
||||||
Scan for Plugins
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{serverPath && scanComplete && (
|
|
||||||
<Button
|
|
||||||
onClick={handleResetSelection}
|
|
||||||
disabled={isScanning}
|
|
||||||
variant="secondary"
|
|
||||||
tooltip="Clear the current server selection and plugin list"
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isScanning && (
|
|
||||||
<>
|
|
||||||
{/* Add debug info */}
|
|
||||||
<div style={{ display: 'none' }}>
|
|
||||||
<pre>Debug - scanProgress: {JSON.stringify(scanProgress, null, 2)}</pre>
|
|
||||||
</div>
|
|
||||||
<ScanProgress progress={scanProgress} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="server-selector-error">
|
|
||||||
<p className="error-message" role="alert">Error: {error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServerSelector;
|
|
@ -1,87 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
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 (
|
|
||||||
<div className="bulk-update-progress">
|
|
||||||
<div className="bulk-update-header">
|
|
||||||
<h3 className="bulk-update-title">
|
|
||||||
Checking for Updates
|
|
||||||
</h3>
|
|
||||||
<span className="bulk-update-count">
|
|
||||||
{bulkUpdateProgress.processed} of {bulkUpdateProgress.total}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
value={progressPercentage}
|
|
||||||
color={progressPercentage === 100 ? 'success' : 'primary'}
|
|
||||||
showPercentage={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="bulk-update-details">
|
|
||||||
<span className="current-plugin">
|
|
||||||
Processing: <strong>{bulkUpdateProgress.current_plugin_name}</strong>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BulkUpdateProgress;
|
|
@ -1,81 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
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<CompatibilityCheckDialogProps> = ({
|
|
||||||
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 (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
title="Compatibility Check"
|
|
||||||
>
|
|
||||||
<div className="compatibility-check-content">
|
|
||||||
<div className="compatibility-status">
|
|
||||||
{supportsVersion ? (
|
|
||||||
<div className="compatibility-supported">
|
|
||||||
<span className="status-icon">✓</span>
|
|
||||||
<p>
|
|
||||||
This plugin update is marked as compatible with your server version ({serverVersion}).
|
|
||||||
It should work correctly after updating.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="compatibility-warning">
|
|
||||||
<span className="status-icon">⚠️</span>
|
|
||||||
<p>
|
|
||||||
This plugin update is not explicitly marked as compatible with your server version ({serverVersion}).
|
|
||||||
Updating may cause compatibility issues or server errors.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-details">
|
|
||||||
<h4>Plugin Details</h4>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Name:</strong> {plugin.name}</li>
|
|
||||||
<li><strong>Current Version:</strong> {plugin.version}</li>
|
|
||||||
<li><strong>Latest Version:</strong> {plugin.latest_version}</li>
|
|
||||||
{plugin.platform_compatibility && (
|
|
||||||
<li>
|
|
||||||
<strong>Supported Versions:</strong> {plugin.platform_compatibility.join(', ')}
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!supportsVersion && (
|
|
||||||
<div className="risk-acknowledgment">
|
|
||||||
<label className="checkbox-container">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={acknowledgedRisk}
|
|
||||||
onChange={() => setAcknowledgedRisk(!acknowledgedRisk)}
|
|
||||||
/>
|
|
||||||
<span className="checkbox-label">
|
|
||||||
I understand the risks and still want to update this plugin
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="dialog-actions">
|
|
||||||
<Button
|
|
||||||
onClick={onClose}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleConfirm}
|
|
||||||
variant="primary"
|
|
||||||
disabled={!supportsVersion && !acknowledgedRisk}
|
|
||||||
>
|
|
||||||
Update Anyway
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CompatibilityCheckDialog;
|
|
@ -1,106 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
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<DownloadProgressIndicatorProps> = ({
|
|
||||||
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 (
|
|
||||||
<div className={`download-progress-indicator download-status-${status}`}>
|
|
||||||
<div className="download-progress-header">
|
|
||||||
<div className="download-progress-plugin-info">
|
|
||||||
<span className="status-icon">{getStatusIcon()}</span>
|
|
||||||
<span className="plugin-name">{pluginName}</span>
|
|
||||||
<span className="plugin-version">v{version}</span>
|
|
||||||
</div>
|
|
||||||
<div className="download-status">
|
|
||||||
{getStatusText()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
value={percentage}
|
|
||||||
color={getStatusColor()}
|
|
||||||
showPercentage={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{message && (
|
|
||||||
<div className="download-message">
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DownloadProgressIndicator;
|
|
@ -1,132 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
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<PluginMatchSelectorProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
pluginName,
|
|
||||||
potentialMatches,
|
|
||||||
onSelectMatch
|
|
||||||
}) => {
|
|
||||||
const [selectedMatchIndex, setSelectedMatchIndex] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const handleSelectMatch = () => {
|
|
||||||
if (selectedMatchIndex !== null) {
|
|
||||||
onSelectMatch(potentialMatches[selectedMatchIndex]);
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
title="Select Plugin Match"
|
|
||||||
>
|
|
||||||
<div className="plugin-match-selector">
|
|
||||||
<div className="plugin-match-header">
|
|
||||||
<p>
|
|
||||||
We found multiple potential matches for <strong>{pluginName}</strong>.
|
|
||||||
Please select the correct plugin from the list below:
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-matches-list">
|
|
||||||
{potentialMatches.map((match, index) => (
|
|
||||||
<div
|
|
||||||
key={`${match.repository}-${match.name}-${match.version}`}
|
|
||||||
className={`plugin-match-item ${selectedMatchIndex === index ? 'selected' : ''}`}
|
|
||||||
onClick={() => setSelectedMatchIndex(index)}
|
|
||||||
>
|
|
||||||
<div className="plugin-match-info">
|
|
||||||
<h4 className="plugin-match-name">{match.name}</h4>
|
|
||||||
<span className="plugin-match-version">v{match.version}</span>
|
|
||||||
<span className="plugin-match-source">{match.repository}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-match-details">
|
|
||||||
{match.description && (
|
|
||||||
<p className="plugin-match-description">{match.description}</p>
|
|
||||||
)}
|
|
||||||
<div className="plugin-match-metadata">
|
|
||||||
<span className="plugin-match-mc-versions">
|
|
||||||
MC: {match.minecraft_versions.join(', ')}
|
|
||||||
</span>
|
|
||||||
{match.download_count !== undefined && (
|
|
||||||
<span className="plugin-match-downloads">
|
|
||||||
{new Intl.NumberFormat().format(match.download_count)} downloads
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-match-selection">
|
|
||||||
<div className="plugin-match-radio">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="plugin-match"
|
|
||||||
checked={selectedMatchIndex === index}
|
|
||||||
onChange={() => setSelectedMatchIndex(index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="plugin-match-actions">
|
|
||||||
<Button
|
|
||||||
onClick={onClose}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSelectMatch}
|
|
||||||
variant="primary"
|
|
||||||
disabled={selectedMatchIndex === null}
|
|
||||||
>
|
|
||||||
Select Plugin
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PluginMatchSelector;
|
|
@ -1,53 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
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<PremiumPluginModalProps> = ({
|
|
||||||
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 (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
title="Premium Plugin"
|
|
||||||
>
|
|
||||||
<div className="premium-plugin-content">
|
|
||||||
<div className="premium-plugin-icon">
|
|
||||||
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor">
|
|
||||||
<path d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="premium-plugin-message">
|
|
||||||
<h3>Premium Plugin Detected</h3>
|
|
||||||
<p>
|
|
||||||
<strong>{pluginInfo.name}</strong> (version {pluginInfo.version}) is a premium plugin
|
|
||||||
that requires manual download from the official source.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Click the button below to visit the plugin's website where you can purchase or
|
|
||||||
download the update if you already own it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="premium-plugin-actions">
|
|
||||||
<Button
|
|
||||||
onClick={onClose}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleOpenUrl}
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Open Download Page
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PremiumPluginModal;
|
|
@ -1,114 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
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 (
|
|
||||||
<div className="update-controls" role="region" aria-label="Plugin update controls">
|
|
||||||
<div className="update-status">
|
|
||||||
{isCheckingUpdates ? (
|
|
||||||
<div className="checking-status" aria-live="polite">
|
|
||||||
<div className="spinner small" aria-hidden="true"></div>
|
|
||||||
<span>Checking for plugin updates{bulkUpdateProgress ? ` (${bulkUpdateProgress.processed}/${bulkUpdateProgress.total})` : '...'}</span>
|
|
||||||
</div>
|
|
||||||
) : hasUpdates ? (
|
|
||||||
<div className="updates-available" aria-live="polite">
|
|
||||||
<span className="update-count">{outdatedCount}</span> plugin update{outdatedCount !== 1 ? 's' : ''} available
|
|
||||||
</div>
|
|
||||||
) : updatesHaveBeenChecked ? (
|
|
||||||
<div className="no-updates" aria-live="polite">All plugins are up to date</div>
|
|
||||||
) : (
|
|
||||||
<div className="no-updates-checked">Click "Check for Updates" to check plugin versions</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="update-actions">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={handleCheckForUpdates}
|
|
||||||
disabled={isLoading}
|
|
||||||
isLoading={isCheckingUpdates}
|
|
||||||
startIcon={!isCheckingUpdates ? <span className="check-icon" aria-hidden="true">↻</span> : undefined}
|
|
||||||
aria-label="Check for plugin updates"
|
|
||||||
>
|
|
||||||
Check for Updates
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{hasUpdates && (
|
|
||||||
<Button
|
|
||||||
variant="warning"
|
|
||||||
onClick={handleUpdateAll}
|
|
||||||
disabled={isLoading}
|
|
||||||
isLoading={isUpdatingAll}
|
|
||||||
startIcon={<span className="update-icon" aria-hidden="true">↑</span>}
|
|
||||||
aria-label={`Update all ${outdatedCount} plugins`}
|
|
||||||
>
|
|
||||||
Update All ({outdatedCount})
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UpdateControls;
|
|
@ -1,62 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
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<WarningModalProps> = ({
|
|
||||||
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 (
|
|
||||||
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor">
|
|
||||||
<path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
case 'info':
|
|
||||||
return (
|
|
||||||
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor">
|
|
||||||
<path d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
case 'warning':
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor">
|
|
||||||
<path d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<div className={`warning-modal warning-modal-${variant}`}>
|
|
||||||
<div className="warning-modal-icon">
|
|
||||||
{renderIcon()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="warning-modal-content">
|
|
||||||
<p className="warning-modal-message">{message}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="warning-modal-actions">
|
|
||||||
{onConfirm ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={onClose}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{cancelLabel}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleConfirm}
|
|
||||||
variant={variant === 'error' ? 'danger' : 'primary'}
|
|
||||||
>
|
|
||||||
{confirmLabel}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={onClose}
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
OK
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WarningModal;
|
|
@ -1,504 +0,0 @@
|
|||||||
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<string, boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to check for updates for a single plugin
|
|
||||||
*/
|
|
||||||
checkSinglePlugin: (plugin: Plugin) => Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to update a plugin to the latest version
|
|
||||||
*/
|
|
||||||
updatePlugin: (plugin: Plugin) => Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<PluginContextProps>({} as PluginContextProps);
|
|
||||||
|
|
||||||
interface PluginProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provider component for managing plugin-related state
|
|
||||||
*/
|
|
||||||
export const PluginProvider: React.FC<PluginProviderProps> = ({
|
|
||||||
children
|
|
||||||
}) => {
|
|
||||||
// Get server context directly
|
|
||||||
const { serverPath, serverInfo } = useServerContext();
|
|
||||||
const serverType = serverInfo?.server_type;
|
|
||||||
|
|
||||||
const [plugins, setPluginsState] = useState<Plugin[]>([]);
|
|
||||||
const [selectedPlugin, setSelectedPlugin] = useState<Plugin | null>(null);
|
|
||||||
const [isCheckingUpdates, setIsCheckingUpdates] = useState<boolean>(false);
|
|
||||||
const [updateError, setUpdateError] = useState<string | null>(null);
|
|
||||||
const [pluginLoadingStates, setPluginLoadingStates] = useState<Record<string, boolean>>({});
|
|
||||||
const [bulkUpdateProgress, setBulkUpdateProgress] = useState<BulkUpdateProgressPayload | null>(null);
|
|
||||||
const [isCheckingSinglePlugin, setIsCheckingSinglePlugin] = useState<boolean>(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<BulkUpdateProgressPayload>("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<SingleUpdateResultPayload>("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<Plugin[]>("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<string>("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 (
|
|
||||||
<PluginContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</PluginContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PluginProvider;
|
|
@ -1,20 +0,0 @@
|
|||||||
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;
|
|
@ -1,246 +0,0 @@
|
|||||||
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<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to scan for plugins in the selected directory
|
|
||||||
*/
|
|
||||||
scanForPlugins: () => Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<ServerContextProps>({} as ServerContextProps);
|
|
||||||
|
|
||||||
interface ServerProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
onScanComplete?: (result: ScanResult) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provider component for managing server-related state
|
|
||||||
*/
|
|
||||||
export const ServerProvider: React.FC<ServerProviderProps> = ({
|
|
||||||
children,
|
|
||||||
onScanComplete
|
|
||||||
}) => {
|
|
||||||
const [serverPath, setServerPathState] = useState<string>("");
|
|
||||||
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
|
|
||||||
const [isScanning, setIsScanning] = useState<boolean>(false);
|
|
||||||
const [scanComplete, setScanComplete] = useState<boolean>(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [scanProgress, setScanProgress] = useState<ScanProgress | null>(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 (
|
|
||||||
<ServerContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</ServerContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServerProvider;
|
|
@ -1,20 +0,0 @@
|
|||||||
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;
|
|
@ -1,150 +0,0 @@
|
|||||||
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<UIContextProps | null>(null);
|
|
||||||
|
|
||||||
interface UIProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
let messageIdCounter = 0;
|
|
||||||
|
|
||||||
export const UIProvider: React.FC<UIProviderProps> = ({ children }) => {
|
|
||||||
// State for warning messages
|
|
||||||
const [warningMessage, setWarningMessage] = useState<WarningMessage | null>(null);
|
|
||||||
|
|
||||||
// State for premium plugin info
|
|
||||||
const [premiumPluginInfo, setPremiumPluginInfo] = useState<PremiumPluginInfo | null>(null);
|
|
||||||
|
|
||||||
// State for download progress
|
|
||||||
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
|
||||||
|
|
||||||
// State for compatibility dialog
|
|
||||||
const [isCompatibilityDialogOpen, setIsCompatibilityDialogOpen] = useState<boolean>(false);
|
|
||||||
const [currentPluginForCompatibility, setCurrentPluginForCompatibility] = useState<Plugin | null>(null);
|
|
||||||
|
|
||||||
// State for update available notification
|
|
||||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false);
|
|
||||||
const [updateVersion, setUpdateVersion] = useState<string | null>(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 (
|
|
||||||
<UIContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</UIContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UIProvider;
|
|
@ -1,20 +0,0 @@
|
|||||||
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;
|
|
@ -1,59 +0,0 @@
|
|||||||
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<boolean>(false);
|
|
||||||
const [appUpdateAvailable, setAppUpdateAvailable] = useState<boolean>(false);
|
|
||||||
const [appUpdateVersion, setAppUpdateVersion] = useState<string | null>(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;
|
|
@ -1,215 +0,0 @@
|
|||||||
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;
|
|
@ -1,418 +0,0 @@
|
|||||||
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<string | null>(null);
|
|
||||||
const [currentUpdateProgress, setCurrentUpdateProgress] = useState<number>(0);
|
|
||||||
|
|
||||||
// Add state for potential matches
|
|
||||||
const [potentialMatches, setPotentialMatches] = useState<PotentialPluginMatch[]>([]);
|
|
||||||
const [currentPluginForMatch, setCurrentPluginForMatch] = useState<Plugin | null>(null);
|
|
||||||
const [isMatchSelectorOpen, setIsMatchSelectorOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// Create a state object for plugin loading states that we manage locally
|
|
||||||
const [localPluginLoadingStates, setLocalPluginLoadingStates] = useState<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
// 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<PotentialMatchesPayload>('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>): 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;
|
|
@ -1,109 +0,0 @@
|
|||||||
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<ScanResult | null>(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;
|
|
@ -1,113 +0,0 @@
|
|||||||
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;
|
|
@ -1,27 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
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;
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
114
src/types/tauri.d.ts
vendored
@ -1,114 +0,0 @@
|
|||||||
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<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<T = any>(cmd: string, args?: Record<string, unknown>): Promise<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<UpdateResult>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Installs the available update.
|
|
||||||
* @returns A promise that resolves when the update is installed.
|
|
||||||
*/
|
|
||||||
export function installUpdate(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path to the application configuration directory.
|
|
||||||
* @returns A promise resolving to the app config directory path.
|
|
||||||
*/
|
|
||||||
export function appConfigDir(): Promise<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path to the application local data directory.
|
|
||||||
* @returns A promise resolving to the app local data directory path.
|
|
||||||
*/
|
|
||||||
export function appLocalDataDir(): Promise<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path to the application cache directory.
|
|
||||||
* @returns A promise resolving to the app cache directory path.
|
|
||||||
*/
|
|
||||||
export function appCacheDir(): Promise<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path to the application log directory.
|
|
||||||
* @returns A promise resolving to the app log directory path.
|
|
||||||
*/
|
|
||||||
export function appLogDir(): Promise<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@tauri-apps/api/core/event' {
|
|
||||||
export interface Event<T> {
|
|
||||||
payload: T;
|
|
||||||
windowLabel?: string;
|
|
||||||
event: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventCallback<T> = (event: Event<T>) => 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<T>(event: string, handler: EventCallback<T>): Promise<UnlistenFn>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<T>(event: string, handler: EventCallback<T>): Promise<UnlistenFn>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
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<UpdateInfo> {
|
|
||||||
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<void> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|