Initial commit: PlugSnatcher basic structure

This commit is contained in:
Rbanh 2025-03-29 00:52:17 -04:00
commit d9cf404402
46 changed files with 10036 additions and 0 deletions

View File

@ -0,0 +1,202 @@
---
description: Outlines the technical architecture of the PlugSnatcher application, providing an overview of the system design, component interactions, and data flow.
globs:
alwaysApply: false
---
## System Overview
PlugSnatcher is a desktop application built using Tauri, combining a Rust backend with a React/TypeScript frontend:
- **Frontend**: React + TypeScript for the user interface
- **Backend**: Rust for file system operations and plugin analysis
- **Bridge**: Tauri for integration between frontend and backend
## Architecture Diagram
```
+------------------------------------+ +------------------------------------+
| FRONTEND | | BACKEND |
| (React + TypeScript) | | (Rust) |
| | | |
| +----------------------------+ | | +----------------------------+ |
| | Components | | | | Core Functions | |
| | | | | | | |
| | - App.tsx | | | | - Plugin Scanner | |
| | - ServerInfoDisplay | | | | - Metadata Extractor | |
| | - PluginDetails |<---|------|->| - Server Type Detector | |
| | | | | | - Hash Calculator | |
| +----------------------------+ | | +----------------------------+ |
| | | |
| +----------------------------+ | | +----------------------------+ |
| | State Management | | | | Data Models | |
| | | | | | | |
| | - useState Hooks | | | | - Plugin | |
| | - Future Context API |<---|------|->| - ServerInfo | |
| | | | | | - PluginMeta | |
| +----------------------------+ | | +----------------------------+ |
| | | |
+------------------------------------+ +------------------------------------+
^ ^
| |
| Tauri Bridge |
| |
v v
+------------------------------------+ +------------------------------------+
| EXTERNAL SERVICES | | FILESYSTEM ACCESS |
| | | |
| - Web Crawler | | - Server Directory |
| - Plugin Repositories API | | - Plugin JAR Files |
| - Update Detection | | - Configuration Files |
| | | |
+------------------------------------+ +------------------------------------+
```
## Component Breakdown
### Frontend (React + TypeScript)
1. **Main Components**:
- `App`: The main application component that orchestrates the UI
- `ServerInfoDisplay`: Displays information about the Minecraft server
- `PluginDetails`: Shows detailed information about a selected plugin
2. **State Management**:
- Currently uses React's `useState` for local component state
- Future: May implement Context API for global state as app complexity grows
3. **Frontend Services**:
- Tauri invoke calls to the Rust backend
- UI state management
- Event handling
- Future: Web service integration
### Backend (Rust)
1. **Core Modules**:
- `Plugin Scanner`: Scans directories for plugin JAR files
- `Metadata Extractor`: Extracts plugin.yml data from JAR files
- `Server Type Detector`: Identifies the Minecraft server type
- `Hash Calculator`: Generates SHA-256 hashes for plugin files
2. **Data Models**:
- `Plugin`: Represents a Minecraft plugin with its metadata
- `ServerInfo`: Contains information about the server type and configuration
- `PluginMeta`: Raw metadata extracted from plugin files
- `ScanResult`: Combined result of a server scan operation
3. **Command API**:
- `scan_server_directory`: Scans a server for plugins and server information
- Future: Commands for update checking, plugin installation, etc.
## Data Flow
1. **User Initiates Server Scan**:
```
UI Action -> Frontend Event Handler -> Tauri Invoke -> Rust Command ->
Filesystem Access -> Plugin Analysis -> Result Serialization ->
Frontend State Update -> UI Rendering
```
2. **Plugin Detail View**:
```
Plugin Selection -> State Update -> UI Component Rendering
```
3. **Future: Plugin Update Process**:
```
Update Check -> Web API Call -> Compare Versions ->
UI Notification -> User Confirmation -> Download ->
Backup Original -> Replace Plugin -> Refresh UI
```
## Database/Storage
Current implementation uses in-memory state with no persistence. Future versions will implement:
1. **Local Storage Options**:
- SQLite database for plugin metadata and scan history
- JSON file storage for configuration and preferences
- Filesystem for plugin backups
2. **Data Schema** (Planned):
```
Servers
- ServerID
- ServerPath
- ServerType
- LastScan
Plugins
- PluginID
- ServerID (FK)
- Name
- Version
- FilePath
- FileHash
- LastUpdated
Updates
- UpdateID
- PluginID (FK)
- AvailableVersion
- ReleaseDate
- ChangelogURL
```
## Security Considerations
1. **File System Access**:
- Limited to reading plugin directories and JAR files
- No modification of server files without explicit permission
2. **Network Security** (Future):
- Verification of downloaded plugins via checksums
- HTTPS for all external API communications
- Sandboxed plugin downloads
3. **Error Handling**:
- Graceful error recovery
- User-friendly error messages
- Detailed error logging
## Scalability Considerations
1. **Performance Optimizations**:
- Asynchronous plugin scanning for large server directories
- Caching of plugin metadata to reduce repeated JAR parsing
- Incremental updates of the plugin list
2. **Modularity**:
- Design for extensibility with new features
- Clear separation between UI and business logic
- Modular crawler system for multiple plugin sources
## Future Architecture Extensions
1. **Plugin Update System**:
- Web crawler for multiple plugin repositories
- Version comparison engine
- Plugin download and installation manager
2. **Server Management**:
- Multiple server profile support
- Server start/stop controls
- Configuration file editing
3. **Enhanced UI**:
- Dashboard with server health metrics
- Plugin conflict detection
- Compatibility matrix
## Development Environment
- **Frontend**: Node.js, npm, TypeScript, React
- **Backend**: Rust, Cargo
- **Build System**: Tauri CLI
- **Deployment**: Self-contained executables for Windows (primary), potentially macOS and Linux
## Conclusion
This architecture provides a solid foundation for the PlugSnatcher application while allowing for scalability and future enhancements. The separation of concerns between the frontend and backend ensures that each part can evolve independently while maintaining a coherent system.
As the application grows, this document should be updated to reflect architectural changes and new components.

View File

@ -0,0 +1,10 @@
---
description:
globs:
alwaysApply: true
---
# Your rule content
- You can @ files here
- You can use markdown but dont have to

View File

@ -0,0 +1,173 @@
---
description:
globs:
alwaysApply: true
---
# PlugSnatcher Development Workflow
This document outlines the development workflow and best practices for the PlugSnatcher application. Following these guidelines will help maintain consistency and scalability as the project grows.
## Project Architecture
PlugSnatcher is a desktop application built with:
- **Frontend**: React + TypeScript
- **Backend**: Rust + Tauri
- **Data Handling**: In-memory state, potential future SQLite/JSON storage
### Project Structure
```
PlugSnatcher/
├── src/ # Frontend code (React + TypeScript)
│ ├── App.tsx # Main application component
│ ├── App.css # Application styles
│ └── assets/ # Static assets
├── src-tauri/ # Backend code (Rust)
│ ├── src/ # Rust source code
│ │ ├── lib.rs # Core backend functionality
│ │ └── main.rs # Tauri application entry point
│ ├── Cargo.toml # Rust dependencies
│ ├── tauri.conf.json # Tauri configuration
│ └── capabilities/ # Tauri v2 capabilities configs
├── public/ # Public static files
└── ROADMAP.md # Project roadmap and progress tracking
```
## Development Workflow
### 1. Feature Planning
1. **Consult Roadmap**: Check the `ROADMAP.md` file to identify the next feature to implement
2. **Define Scope**: Clearly define what the feature will do and identify any dependencies
3. **Update Roadmap**: Mark features as "In Progress" when starting work
### 2. Implementation Process
For each new feature, follow this general workflow:
#### Backend (Rust) Development
1. **Define Data Models**: Create necessary structs and enums in `lib.rs`
2. **Implement Core Logic**: Develop the backend functionality in Rust
3. **Create Tauri Commands**: Expose functionality to the frontend through command annotations
4. **Update Dependencies**: Add any new dependencies to `Cargo.toml`
5. **Configure Permissions**: Update capability files if new permissions are needed
#### Frontend (React) Development
1. **Define Interfaces**: Create TypeScript interfaces that match Rust structs
2. **Implement UI Components**: Create or update React components
3. **Connect to Backend**: Use Tauri's invoke to call Rust functions
4. **Style Components**: Update CSS for new UI elements
5. **Handle Edge Cases**: Ensure proper error handling and loading states
### 3. Testing
- **Manual Testing**: Test features thoroughly on Windows (primary platform)
- **Edge Cases**: Test with various plugin formats and server setups
- **Error Handling**: Verify all error paths work correctly
### 4. Documentation
- **Update Roadmap**: Mark completed features
- **Code Documentation**: Add comments for complex logic
- **User Documentation**: Update usage instructions if needed
## Code Conventions
### Rust Conventions
1. **Error Handling**:
- Use `Result<T, String>` for functions that may fail
- Provide descriptive error messages
- Use the `?` operator with `.map_err()` for context
2. **Struct Definitions**:
- Include `#[derive]` attributes for serialization
- Organize fields logically
- Use proper visibility modifiers (pub when needed)
3. **Function Organization**:
- Group related functions together
- Extract helper functions for reusable logic
- Add documentation comments for public functions
### TypeScript/React Conventions
1. **Component Structure**:
- Use functional components with hooks
- Define props interfaces for all components
- Keep components focused on a single responsibility
2. **State Management**:
- Use React's `useState` for local state
- Use `useContext` if state needs to be shared
- Consider state management libraries for complex state (future enhancement)
3. **Styling Approach**:
- Use CSS variables for theming
- Organize CSS by component hierarchy
- Use consistent naming conventions
## Scalability Considerations
### Performance Optimization
1. **Large Plugin Collections**:
- Implement pagination or virtualization for long lists
- Optimize file scanning operations
- Consider background processing for intensive operations
2. **Memory Management**:
- Be mindful of memory usage with large JAR files
- Release resources properly after use
### Future Extensions
1. **Plugin Architecture**:
- Design the crawler system with flexibility for multiple sources
- Use interfaces and traits for different plugin repository types
2. **Database Integration**:
- Prepare for potential migration to a database for persistent storage
- Design data models with this in mind
3. **Multi-Server Management**:
- Consider how the architecture might expand to handle multiple servers
## Troubleshooting Common Issues
### Rust/Tauri Issues
1. **Permission Errors**:
- Check Tauri capability files in `capabilities/` directory
- Ensure proper permissions are granted for each plugin
2. **Build Errors**:
- Verify Rust dependencies in `Cargo.toml`
- Check for incompatible versions
### TypeScript/React Issues
1. **Type Errors**:
- Ensure TypeScript interfaces match Rust structs
- Use proper type assertions for nullable values
2. **UI Rendering Issues**:
- Check CSS specificity and hierarchy
- Verify conditional rendering logic
## Deployment Process
1. **Version Bump**:
- Update version in `package.json` and `Cargo.toml`
2. **Build Process**:
- Run `npm run tauri build` for production builds
- Test the packaged application before distribution
3. **Distribution**:
- Package for Windows (primary platform)
- Consider multi-platform support as needed
## Conclusion
By following this workflow, the PlugSnatcher project will maintain a consistent structure and approach as it scales. This document should be updated as the project evolves to reflect any changes in best practices or architecture.

View File

@ -0,0 +1,192 @@
---
description:
globs:
alwaysApply: true
---
# Git Workflow for PlugSnatcher
This document outlines the Git workflow for PlugSnatcher development to maintain code quality and enable collaboration.
## Initial Setup
If you haven't already set up Git for the project:
```bash
# Initialize git repository
git init
# Add all current files
git add .
# Make the initial commit
git commit -m "Initial commit: PlugSnatcher basic structure"
# Create a remote repository on GitHub (if not already done)
# Then connect your local repository
git remote add origin https://git.spacetrainclubhouse.com/Space-Train-Clubhouse/PlugSnatcher.git
git push -u origin main
```
## Branch Structure
Maintain a structured branching strategy:
- `main`: Production-ready code
- `develop`: Main development branch
- Feature branches: `feature/feature-name`
- Bug fix branches: `fix/bug-description`
## Feature Development Workflow
1. **Create a Feature Branch**
Always branch from `develop` for new features:
```bash
git checkout develop
git pull
git checkout -b feature/plugin-scanner
```
2. **Make Regular Commits**
Commit changes with descriptive messages:
```bash
git add .
git commit -m "Add plugin metadata extraction functionality"
```
3. **Merge Back to Develop**
When the feature is complete:
```bash
git checkout develop
git pull
git merge feature/plugin-scanner
git push
```
## Bug Fix Workflow
1. **Create a Bug Fix Branch**
```bash
git checkout develop
git pull
git checkout -b fix/plugin-metadata-error
```
2. **Fix and Test**
Make the necessary changes and test thoroughly.
3. **Commit and Merge**
```bash
git add .
git commit -m "Fix plugin metadata extraction for malformed YML files"
git checkout develop
git pull
git merge fix/plugin-metadata-error
git push
```
## Versioning
Use semantic versioning (MAJOR.MINOR.PATCH):
- **MAJOR**: Incompatible API changes
- **MINOR**: New functionality in a backward-compatible manner
- **PATCH**: Backward-compatible bug fixes
When releasing a new version:
```bash
# Tag the release
git tag -a v0.1.0 -m "Initial beta release with plugin scanning"
git push origin v0.1.0
```
## Commit Message Guidelines
Write clear, concise commit messages following these guidelines:
- Use present tense ("Add feature" not "Added feature")
- Start with a verb
- Keep the first line under 50 characters
- Reference issues or tickets where applicable
- Include details in commit body if needed
Examples:
- "Add server type detection for Paper and Spigot"
- "Fix permission error in dialog plugin initialization"
- "Improve UI layout for plugin details modal"
## Pull Request Process
When working in a team:
1. Push your branch to GitHub
2. Create a Pull Request (PR) to the `develop` branch
3. Request code review from at least one team member
4. Address any comments or feedback
5. Merge PR once approved
## Git Best Practices for PlugSnatcher
1. **Handle Large Binary Files Properly**
Avoid committing large JAR files or test plugins to the repository. Use `.gitignore` to exclude these files.
2. **Handle Sensitive Information**
Never commit API keys, credentials, or sensitive configuration files. Use environment variables or configuration templates.
3. **Regular Backups**
Regularly push to the remote repository to maintain backups of your work.
## Git Ignore Configuration
Ensure your `.gitignore` file contains the following patterns:
```
# Node dependencies
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
# Rust build artifacts
target/
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Build outputs
dist/
build/
# OS-specific files
.DS_Store
Thumbs.db
# Test plugins
test-plugins/
*.jar
# IDE files
.idea/
.vscode/
*.swp
```
## Conclusion
Following these Git practices will help maintain code quality and enable collaboration as the PlugSnatcher project grows. Adjust these guidelines as needed based on team preferences and project requirements.

46
.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Node dependencies
node_modules
dist
dist-ssr
*.local
# Rust build artifacts
target/
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Build outputs
dist/
build/
# OS-specific files
.DS_Store
Thumbs.db
# Test plugins
test-plugins/
*.jar
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

204
README.md Normal file
View File

@ -0,0 +1,204 @@
# 🔧 PlugSnatcher
A powerful Minecraft plugin manager for server administrators.
## Features
- **Plugin Discovery**: Automatically detect installed plugins in your server
- **Update Management**: Keep your plugins up-to-date with minimal effort
- **Multi-Source Support**: Find plugins across HangarMC, SpigotMC, Modrinth, GitHub, and more
- **Plugin Intelligence**: Smart matching of plugins for accurate updates
- **Dark Mode**: Easy on the eyes for those late-night server maintenance sessions
## Development Status
PlugSnatcher is currently in early development (v0.1.0). Check our [ROADMAP.md](ROADMAP.md) for current progress and upcoming features.
## Setup
1. Install Rust: https://www.rust-lang.org/tools/install
2. Install Node.js and npm: https://nodejs.org/
3. Navigate to the PlugSnatcher directory:
```
cd PlugSnatcher
```
4. Install dependencies:
```
npm install
```
5. Run the development server:
```
npm run tauri dev
```
## Building
```
cd PlugSnatcher
npm run tauri build
```
## Project Structure
- `PlugSnatcher/src/` - React frontend
- `PlugSnatcher/src-tauri/` - Rust backend
- `ROADMAP.md` - Development roadmap and progress
## Contributing
Contributions are welcome! Feel free to submit issues and pull requests.
## License
[MIT](LICENSE)
---
Built with 💻 and ☕ using [Tauri](https://tauri.app/) and [React](https://reactjs.org/)
🔧 Minecraft Plugin Manager - Desktop App Spec Sheet
Codename: PlugSnatcher
Version: 0.1.0 (initial dev)
Target Platforms: Windows, Linux, macOS (Electron / Tauri)
🧠 Core Features
🔍 Plugin Discovery
Auto-detect server type: Paper, Spigot, Velocity, Bukkit
Locate /plugins/ folder based on selected server root
Parse installed .jar files
Extract plugin metadata:
Name
Version
Description
Authors
API version
Source URL (if embedded or in plugin.yml)
Fallback to hash-based identification (SHA256 of JAR)
🌐 Plugin Intelligence Web Crawler
Crawl and parse:
HangarMC
SpigotMC.org
Modrinth
GitHub releases + tags
CurseForge if you hate yourself
Smart matching:
Fuzzy match plugin name + author
Use plugin.yml metadata for confidence scoring
GitHub fallback for obscure shit
🔄 Update Management
Notify user of available updates
Changelog/commit messages when available
Prompt user to confirm update
Automatically replace .jar in /plugins/ directory
Optional plugin data backup (zip old plugin + config)
Restart server after update (optional checkbox)
🧰 Tech Stack
🖥️ Frontend
Tauri + Svelte/React (blazingly sexy and native AF)
Dark mode default (because we're not monsters)
🧠 Backend
Rust or Node.js for system access + networking
Plugin parser using:
ZIP extraction (JAR = ZIP) → plugin.yml
YAML parser
Web crawler/scraper:
Puppeteer/Playwright for JavaScript-heavy sites
Regex + HTML parser for faster sites
📦 Storage
Local SQLite DB or JSON for cache (plugin registry, version history)
Logs for updates, errors, changelogs
🛡️ Permissions & Security
Sandboxed (no outbound requests unless plugin sources are being queried)
All downloads via HTTPS
Optional SHA256 checksum verification
Download warning if unverified source (manual override possible)
🤖 Extra Features (Future Roadmap)
Plugin conflict checker (API version mismatches, dupe commands)
Compatibility checker for Minecraft versions
Server config backup + restore manager
Scheduled plugin update check (daily, weekly, etc.)
Rollback system (previous plugin version restore)
Plugin recommendation system based on existing server meta
Discord webhook for plugin update logs
🧪 Example Flow
User launches PlugSnatcher, selects server folder
App lists all plugins, shows current version vs latest
User clicks "Check for Updates"
App crawls the web like an info-hungry stalker
Update found → Prompt user
User says "ye" → Old plugin backed up, new one slotted in
Optional: Server restarts, all is good in blockyland
🧼 UX Expectations
Drag & drop server folder
Clear plugin list UI with version indicators
One-click updates with optional review
Update logs saved per session
Error messages that don't suck
👹 Known Challenges
Some plugins are unlisted or distributed on Discord/GitHub only
No consistent plugin metadata across sites (we'll brute force it)
API rate limits (user token support for GitHub)

75
ROADMAP.md Normal file
View File

@ -0,0 +1,75 @@
# 🔧 PlugSnatcher - Development Roadmap
## Project Setup ✅
- [x] Create project roadmap
- [x] Initialize Tauri + React project
- [x] Setup basic project structure
- [ ] Create GitHub repository (optional)
## Core Infrastructure (In Progress)
- [x] Setup SQLite or JSON storage for plugin data
- [x] Create core data models
- [x] Build server/plugin directory scanner (basic implementation)
- [x] Implement JAR file parser for plugin.yml extraction
## Plugin Discovery (In Progress)
- [x] Create server type detection (Paper, Spigot, etc.)
- [x] Implement plugins folder detection logic
- [x] Design plugin metadata extraction system
- [x] Build plugin hash identification system
## Web Crawler Development (Upcoming)
- [ ] Create base web crawler architecture
- [ ] Implement HangarMC crawler
- [ ] Implement SpigotMC crawler
- [ ] Implement Modrinth crawler
- [ ] Implement GitHub releases crawler
- [ ] Create plugin matching algorithm
## Update Management (Upcoming)
- [ ] Build update detection system
- [ ] Implement changelog extraction
- [ ] Create plugin backup functionality
- [ ] Develop plugin replacement logic
- [ ] Add server restart capabilities (optional)
## UI Development (In Progress)
- [x] Design and implement main dashboard
- [x] Create plugin list view with version indicators
- [x] Build server folder selection interface
- [x] Implement plugin detail view
- [x] Add update notification system
- [ ] Create settings panel
- [x] Implement dark mode
## Security Features (Upcoming)
- [ ] Implement sandboxing for network requests
- [ ] Add SHA256 checksum verification
- [ ] Create download warning system for unverified sources
## Testing & Refinement (Upcoming)
- [ ] Comprehensive testing with various server setups
- [ ] Performance optimization
- [ ] Error handling improvements
- [ ] User acceptance testing
## Documentation (Upcoming)
- [ ] Create user documentation
- [ ] Write developer documentation
- [ ] Create installation guide
## Deployment (Upcoming)
- [ ] Prepare release process
- [ ] Package application for Windows
- [ ] Package application for macOS
- [ ] Package application for Linux
- [ ] Create project website (optional)
## Future Enhancements (v0.2.0+)
- [ ] Plugin conflict checker
- [ ] Minecraft version compatibility checker
- [ ] Server config backup & restore manager
- [ ] Scheduled plugin update checks
- [ ] Rollback system
- [ ] Plugin recommendation system
- [ ] Discord webhook integration

14
index.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1946
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "plugsnatcher",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2.4.0",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-opener": "^2",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "~5.6.2",
"vite": "^6.0.3"
}
}

6
public/tauri.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
rustup-init.exe Normal file

Binary file not shown.

7
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

5508
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

31
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "plugsnatcher"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[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"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
zip = "0.6"
yaml-rust = "0.4"
walkdir = "2.4"
regex = "1.10"
sha2 = "0.10"

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,12 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default",
"dialog:default",
"dialog:allow-open"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

614
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,614 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use serde::{Serialize, Deserialize};
use std::path::Path;
use std::fs;
use std::io::Read;
use tauri::command;
use zip::ZipArchive;
use yaml_rust::{YamlLoader, Yaml};
use std::fs::File;
use sha2::{Sha256, Digest};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum ServerType {
Paper,
Spigot,
Bukkit,
Vanilla,
Forge,
Fabric,
Velocity,
BungeeCord,
Waterfall,
Unknown,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ServerInfo {
server_type: ServerType,
minecraft_version: Option<String>,
plugins_directory: String,
plugins_count: usize,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Plugin {
name: String,
version: String,
latest_version: Option<String>,
description: Option<String>,
authors: Vec<String>,
has_update: bool,
api_version: Option<String>,
main_class: Option<String>,
depend: Option<Vec<String>>,
soft_depend: Option<Vec<String>>,
load_before: Option<Vec<String>>,
commands: Option<serde_json::Value>,
permissions: Option<serde_json::Value>,
file_path: String,
file_hash: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PluginMeta {
pub name: String,
pub version: String,
pub description: Option<String>,
pub authors: Vec<String>,
pub api_version: Option<String>,
pub main_class: Option<String>,
pub depend: Option<Vec<String>>,
pub soft_depend: Option<Vec<String>>,
pub load_before: Option<Vec<String>>,
pub commands: Option<serde_json::Value>,
pub permissions: Option<serde_json::Value>,
pub file_path: String,
pub file_size: u64,
pub file_hash: String,
}
/// Calculates SHA-256 hash for a given file path
pub fn calculate_file_hash(file_path: &str) -> Result<String, String> {
let mut file = File::open(file_path).map_err(|e| format!("Failed to open file for hashing: {}", e))?;
let mut hasher = Sha256::new();
let mut buffer = [0; 1024];
loop {
let bytes_read = file.read(&mut buffer).map_err(|e| format!("Failed to read file for hashing: {}", e))?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
let hash = hasher.finalize();
Ok(format!("{:x}", hash))
}
/// Extract metadata from a plugin.yml file inside a JAR
fn extract_plugin_metadata(jar_path: &Path) -> Result<PluginMeta, String> {
let file = fs::File::open(jar_path)
.map_err(|e| format!("Failed to open JAR file: {}", e))?;
let file_size = file.metadata()
.map_err(|e| format!("Failed to read file metadata: {}", e))?
.len();
let mut archive = ZipArchive::new(file)
.map_err(|e| format!("Invalid JAR file: {}", e))?;
// Try to find and read plugin.yml or bungee.yml
let yaml_content = match read_yaml_from_archive(&mut archive, "plugin.yml") {
Ok(content) => content,
Err(_) => match read_yaml_from_archive(&mut archive, "bungee.yml") {
Ok(content) => content,
Err(_) => {
// If no plugin metadata file is found, try to infer from filename
let filename = jar_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.jar");
// Extract name and version from filename (e.g., "WorldEdit-7.2.8.jar" → name: "WorldEdit", version: "7.2.8")
let mut parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
let version = if parts.len() > 1 {
parts.pop().unwrap_or("1.0.0").to_string()
} else {
"1.0.0".to_string()
};
let name = parts.join("-");
return Ok(PluginMeta {
name,
version,
description: None,
authors: Vec::new(),
api_version: None,
main_class: None,
depend: None,
soft_depend: None,
load_before: None,
commands: None,
permissions: None,
file_path: jar_path.to_string_lossy().to_string(),
file_size,
file_hash: calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string()),
});
}
}
};
// Parse the YAML content
let docs = match YamlLoader::load_from_str(&yaml_content) {
Ok(docs) => docs,
Err(e) => {
println!("Failed to parse plugin YAML: {}", e);
return fallback_plugin_meta(jar_path, file_size);
}
};
if docs.is_empty() {
return fallback_plugin_meta(jar_path, file_size);
}
let doc = &docs[0];
// Extract basic metadata with fallbacks for missing fields
let name = yaml_str_with_fallback(doc, "name", jar_path);
let version = yaml_str_with_fallback(doc, "version", jar_path);
// Extract optional fields
let description = yaml_str_opt(doc, "description");
// Handle authors (can be a single string or an array)
let authors = match &doc["authors"] {
Yaml::Array(arr) => {
arr.iter()
.filter_map(|a| a.as_str().map(|s| s.to_string()))
.collect()
},
Yaml::String(s) => vec![s.clone()],
_ => {
// Fallback to 'author' field which is sometimes used
match &doc["author"] {
Yaml::String(s) => vec![s.clone()],
_ => Vec::new(),
}
}
};
// Extract other optional metadata
let api_version = yaml_str_opt(doc, "api-version");
let main_class = yaml_str_opt(doc, "main");
// Handle dependency lists
let depend = yaml_str_array(doc, "depend");
let soft_depend = yaml_str_array(doc, "softdepend");
let load_before = yaml_str_array(doc, "loadbefore");
// Handle complex structures as generic JSON values
let commands = match &doc["commands"] {
Yaml::Hash(_) => {
Some(serde_json::Value::String("Commands data present".to_string()))
},
_ => None
};
let permissions = match &doc["permissions"] {
Yaml::Hash(_) => {
Some(serde_json::Value::String("Permissions data present".to_string()))
},
_ => None
};
// Calculate the file hash
let file_hash = calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string());
Ok(PluginMeta {
name,
version,
description,
authors,
api_version,
main_class,
depend,
soft_depend,
load_before,
commands,
permissions,
file_path: jar_path.to_string_lossy().to_string(),
file_size,
file_hash,
})
}
// Helper function to read a YAML file from the ZIP archive
fn read_yaml_from_archive(archive: &mut ZipArchive<fs::File>, file_name: &str) -> Result<String, String> {
match archive.by_name(file_name) {
Ok(mut file) => {
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| format!("Failed to read {}: {}", file_name, e))?;
Ok(contents)
},
Err(e) => Err(format!("Failed to find {}: {}", file_name, e))
}
}
// Helper function to create plugin metadata with fallback values
fn fallback_plugin_meta(jar_path: &Path, file_size: u64) -> Result<PluginMeta, String> {
let filename = jar_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.jar");
// Extract name and version from filename (e.g., "WorldEdit-7.2.8.jar" → name: "WorldEdit", version: "7.2.8")
let mut parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
let version = if parts.len() > 1 {
parts.pop().unwrap_or("1.0.0").to_string()
} else {
"1.0.0".to_string()
};
let name = parts.join("-");
Ok(PluginMeta {
name,
version,
description: None,
authors: Vec::new(),
api_version: None,
main_class: None,
depend: None,
soft_depend: None,
load_before: None,
commands: None,
permissions: None,
file_path: jar_path.to_string_lossy().to_string(),
file_size,
file_hash: calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string()),
})
}
// Extract a string from YAML with fallback to filename
fn yaml_str_with_fallback(yaml: &Yaml, key: &str, jar_path: &Path) -> String {
match yaml[key].as_str() {
Some(s) => s.to_string(),
None => {
// Extract from filename as fallback
let filename = jar_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.jar");
if key == "name" {
// Extract name (e.g., "WorldEdit-7.2.8.jar" → "WorldEdit")
let parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
parts[0].to_string()
} else if key == "version" {
// Extract version (e.g., "WorldEdit-7.2.8.jar" → "7.2.8")
let parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
if parts.len() > 1 {
parts[1].to_string()
} else {
"1.0.0".to_string()
}
} else {
String::new()
}
}
}
}
fn yaml_str_opt(yaml: &Yaml, key: &str) -> Option<String> {
yaml[key].as_str().map(|s| s.to_string())
}
fn yaml_str_array(yaml: &Yaml, key: &str) -> Option<Vec<String>> {
match &yaml[key] {
Yaml::Array(arr) => {
let strings: Vec<String> = arr.iter()
.filter_map(|a| a.as_str().map(|s| s.to_string()))
.collect();
if strings.is_empty() { None } else { Some(strings) }
},
_ => None
}
}
/// Detect the server type based on files in the server directory
fn detect_server_type(server_path: &Path) -> ServerType {
// Check for Paper
if server_path.join("cache").join("patched_1.19.2.jar").exists() ||
server_path.join("paper.yml").exists() {
return ServerType::Paper;
}
// Check for Spigot
if server_path.join("spigot.yml").exists() {
return ServerType::Spigot;
}
// Check for Bukkit
if server_path.join("bukkit.yml").exists() {
return ServerType::Bukkit;
}
// Check for Forge
if server_path.join("forge-server.jar").exists() ||
server_path.join("mods").exists() {
return ServerType::Forge;
}
// Check for Fabric
if server_path.join("fabric-server-launch.jar").exists() ||
(server_path.join("mods").exists() && server_path.join("fabric-server-launcher.properties").exists()) {
return ServerType::Fabric;
}
// Check for Velocity
if server_path.join("velocity.toml").exists() {
return ServerType::Velocity;
}
// Check for BungeeCord
if server_path.join("BungeeCord.jar").exists() ||
server_path.join("config.yml").exists() {
return ServerType::BungeeCord;
}
// Check for Waterfall
if server_path.join("waterfall.jar").exists() ||
server_path.join("waterfall.yml").exists() {
return ServerType::Waterfall;
}
// Check if it's at least a vanilla server
if server_path.join("server.properties").exists() ||
server_path.join("vanilla_server.jar").exists() {
return ServerType::Vanilla;
}
// If no server type detected
ServerType::Unknown
}
/// Guess the Minecraft version from various files in the server directory
fn detect_minecraft_version(server_path: &Path, server_type: &ServerType) -> Option<String> {
// Try from version.json if it exists
if let Ok(content) = fs::read_to_string(server_path.join("version.json")) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(version) = json["name"].as_str() {
return Some(version.to_string());
}
}
}
// Try from the server jar name pattern
if let Ok(entries) = fs::read_dir(server_path) {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "jar") {
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
// Extract version from various common patterns in jar names
if filename.starts_with("paper-") ||
filename.starts_with("spigot-") ||
filename.starts_with("craftbukkit-") {
// Pattern: paper-1.19.2.jar, spigot-1.19.2.jar
let parts: Vec<&str> = filename.split('-').collect();
if parts.len() > 1 {
let version_part = parts[1].trim_end_matches(".jar");
if version_part.contains('.') { // Basic version format check
return Some(version_part.to_string());
}
}
}
// Look for version patterns like minecraft_server.1.19.2.jar
if filename.starts_with("minecraft_server.") {
let version_part = filename
.trim_start_matches("minecraft_server.")
.trim_end_matches(".jar");
if version_part.contains('.') {
return Some(version_part.to_string());
}
}
}
}
}
}
// If server type is proxy, look in config files
if server_type == &ServerType::BungeeCord ||
server_type == &ServerType::Waterfall ||
server_type == &ServerType::Velocity {
// Velocity uses TOML, others use YAML
if server_type == &ServerType::Velocity {
if let Ok(content) = fs::read_to_string(server_path.join("velocity.toml")) {
// Very basic TOML parsing just for this field
for line in content.lines() {
if line.contains("minecraft-version") {
if let Some(version) = line.split('=').nth(1) {
return Some(version.trim().trim_matches('"').to_string());
}
}
}
}
} else {
// Try to parse config.yml for BungeeCord/Waterfall
if let Ok(content) = fs::read_to_string(server_path.join("config.yml")) {
if let Ok(docs) = YamlLoader::load_from_str(&content) {
if !docs.is_empty() {
let doc = &docs[0];
if let Some(version) = doc["minecraft_version"].as_str() {
return Some(version.to_string());
}
}
}
}
}
}
// Default fallback
None
}
/// Get plugins directory path based on server type
fn get_plugins_directory(server_path: &Path, server_type: &ServerType) -> String {
match server_type {
ServerType::Velocity => server_path.join("plugins").to_string_lossy().to_string(),
ServerType::BungeeCord => server_path.join("plugins").to_string_lossy().to_string(),
ServerType::Waterfall => server_path.join("plugins").to_string_lossy().to_string(),
_ => server_path.join("plugins").to_string_lossy().to_string(),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ScanResult {
server_info: ServerInfo,
plugins: Vec<Plugin>,
}
#[command]
fn scan_server_directory(path: &str) -> Result<ScanResult, String> {
let server_path = Path::new(path);
if !server_path.exists() {
return Err(format!("Server path does not exist: {}", path));
}
// Detect server type and version
let server_type = detect_server_type(server_path);
let minecraft_version = detect_minecraft_version(server_path, &server_type);
println!("Detected server type: {:?}", server_type);
if let Some(version) = &minecraft_version {
println!("Detected Minecraft version: {}", version);
}
// Determine plugins directory based on server type
let plugins_dir_str = get_plugins_directory(server_path, &server_type);
let plugins_dir = Path::new(&plugins_dir_str);
if !plugins_dir.exists() {
return Err(format!("Plugins directory not found at: {}", plugins_dir.display()));
}
// Scan for JAR files in the plugins directory
let mut plugins = Vec::new();
match fs::read_dir(&plugins_dir) {
Ok(entries) => {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
// Check if this is a JAR file
if path.is_file() && path.extension().map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) {
match extract_plugin_metadata(&path) {
Ok(meta) => {
// Create a Plugin from PluginMeta
let plugin = Plugin {
name: meta.name,
version: meta.version,
latest_version: None, // Will be filled by update checker
description: meta.description,
authors: meta.authors,
has_update: false, // Will be determined by update checker
api_version: meta.api_version,
main_class: meta.main_class,
depend: meta.depend,
soft_depend: meta.soft_depend,
load_before: meta.load_before,
commands: meta.commands,
permissions: meta.permissions,
file_path: meta.file_path,
file_hash: meta.file_hash,
};
plugins.push(plugin);
},
Err(e) => {
// Log error but continue with other plugins
println!("Error reading plugin from {}: {}", path.display(), e);
}
}
}
}
}
},
Err(e) => {
return Err(format!("Failed to read plugins directory: {}", e));
}
}
// If no plugins were found, fall back to mock data for testing
if plugins.is_empty() && server_type == ServerType::Unknown {
// For testing only - in production, we'd just return an empty list
plugins = vec![
Plugin {
name: "EssentialsX".to_string(),
version: "2.19.0".to_string(),
latest_version: Some("2.20.0".to_string()),
description: Some("Essential server tools for Minecraft".to_string()),
authors: vec!["md_5".to_string(), "SupaHam".to_string()],
has_update: true,
api_version: Some("1.13".to_string()),
main_class: Some("com.earth2me.essentials.Essentials".to_string()),
depend: None,
soft_depend: None,
load_before: None,
commands: None,
permissions: None,
file_path: "EssentialsX.jar".to_string(),
file_hash: calculate_file_hash("EssentialsX.jar").unwrap_or_else(|_| "unknown".to_string()),
},
Plugin {
name: "WorldEdit".to_string(),
version: "7.2.8".to_string(),
latest_version: Some("7.2.8".to_string()),
description: Some("In-game map editor".to_string()),
authors: vec!["sk89q".to_string(), "wizjany".to_string()],
has_update: false,
api_version: Some("1.13".to_string()),
main_class: Some("com.sk89q.worldedit.bukkit.WorldEditPlugin".to_string()),
depend: None,
soft_depend: None,
load_before: None,
commands: None,
permissions: None,
file_path: "WorldEdit.jar".to_string(),
file_hash: calculate_file_hash("WorldEdit.jar").unwrap_or_else(|_| "unknown".to_string()),
},
];
}
// Create server info
let server_info = ServerInfo {
server_type,
minecraft_version,
plugins_directory: plugins_dir_str,
plugins_count: plugins.len(),
};
Ok(ScanResult {
server_info,
plugins,
})
}
#[command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![greet, scan_server_directory])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
plugsnatcher_lib::run()
}

42
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,42 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "PlugSnatcher",
"version": "0.1.0",
"identifier": "com.plugsnatcher.app",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "PlugSnatcher",
"width": 1024,
"height": 768,
"minWidth": 800,
"minHeight": 600,
"center": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"plugins": {
"dialog": null,
"opener": null
}
}

538
src/App.css Normal file
View File

@ -0,0 +1,538 @@
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafb);
}
:root {
--primary-color: #1a73e8;
--secondary-color: #4caf50;
--background-color: #202124;
--surface-color: #292a2d;
--text-color: #e8eaed;
--text-secondary-color: #9aa0a6;
--border-color: #3c4043;
--error-color: #f44336;
--warning-color: #ff9800;
--success-color: #4caf50;
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
}
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-header {
background-color: var(--surface-color);
padding: 1rem;
text-align: center;
border-bottom: 1px solid var(--border-color);
}
.app-header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.app-content {
flex: 1;
padding: 1rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.server-selector {
background-color: var(--surface-color);
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid var(--border-color);
}
.server-selector h2 {
margin-bottom: 1rem;
font-size: 1.5rem;
}
.input-group {
display: flex;
margin-bottom: 1rem;
}
.input-group input {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--border-color);
background-color: rgba(255, 255, 255, 0.05);
color: var(--text-color);
border-radius: 4px 0 0 4px;
font-size: 1rem;
}
.input-group button {
padding: 0.75rem 1.5rem;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s;
}
.input-group button:hover {
background-color: #1967d2;
}
.scan-button {
padding: 0.75rem 1.5rem;
background-color: var(--secondary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
width: 100%;
transition: background-color 0.3s;
}
.scan-button:hover {
background-color: #43a047;
}
.scan-button:disabled {
background-color: #666;
cursor: not-allowed;
}
.plugins-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.plugins-list h2 {
grid-column: 1 / -1;
margin-bottom: 1rem;
}
.plugin-card {
background-color: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
height: 100%;
min-height: 120px;
}
.plugin-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.plugin-name {
font-size: 1.1rem;
font-weight: bold;
margin-bottom: 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.plugin-details {
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.plugin-version {
color: var(--text-secondary-color);
font-size: 0.9rem;
}
.update-available {
background-color: #4caf50;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: bold;
}
.plugins-count {
margin-top: 0.5rem;
}
.plugins-header {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
padding: 0.75rem 1rem;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 4px 4px 0 0;
font-weight: bold;
border-bottom: 1px solid var(--border-color);
}
.plugin-item {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
padding: 1rem;
border-bottom: 1px solid var(--border-color);
align-items: center;
}
.plugin-item:last-child {
border-bottom: none;
}
.plugin-item.has-update {
background-color: rgba(255, 152, 0, 0.1);
}
.plugin-actions {
display: flex;
gap: 0.5rem;
}
.update-button {
padding: 0.5rem 1rem;
background-color: var(--warning-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.update-button:hover {
background-color: #f57c00;
}
.info-button {
padding: 0.5rem 1rem;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.info-button:hover {
background-color: #1967d2;
}
.app-footer {
background-color: var(--surface-color);
padding: 1rem;
text-align: center;
border-top: 1px solid var(--border-color);
margin-top: auto;
color: var(--text-secondary-color);
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}
.error-message {
background-color: rgba(244, 67, 54, 0.1);
color: var(--error-color);
padding: 0.75rem;
border-radius: 4px;
margin-top: 1rem;
border: 1px solid var(--error-color);
}
.plugin-details-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: 10;
}
.plugin-details-modal {
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;
}
.plugin-details-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.plugin-details-header h2 {
margin: 0;
font-size: 1.5rem;
}
.close-button {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-color);
}
.close-button:hover {
color: var(--text-color);
}
.plugin-details-content {
background-color: var(--background-color);
border-radius: 8px;
padding: 2rem;
width: 80%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
position: relative;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.detail-row {
margin-bottom: 0.75rem;
display: flex;
flex-direction: column;
}
.detail-label {
font-weight: bold;
color: var(--text-secondary-color);
margin-bottom: 0.25rem;
}
.detail-value {
color: var(--text-color);
}
.plugin-details-footer {
padding: 1rem;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
}
.server-info {
background-color: var(--surface-color);
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid var(--border-color);
}
.server-info h2 {
margin-bottom: 1rem;
font-size: 1.5rem;
}
.server-type {
display: flex;
align-items: center;
margin-bottom: 0.75rem;
}
.server-icon {
font-size: 1.5rem;
margin-right: 0.5rem;
}
.server-type-name {
font-size: 1.2rem;
font-weight: bold;
}
.minecraft-version, .plugins-path {
margin-bottom: 0.75rem;
display: flex;
flex-direction: column;
}
.version-label, .path-label {
font-weight: bold;
color: var(--text-secondary-color);
margin-bottom: 0.25rem;
}
.version-value, .path-value {
color: var(--text-color);
word-break: break-all;
}
.plugin-version-display {
margin-bottom: 1rem;
font-size: 1.1rem;
color: var(--text-secondary-color);
}
.plugin-description {
margin-bottom: 1.5rem;
line-height: 1.5;
border-left: 3px solid var(--accent-color);
padding-left: 1rem;
}
.plugin-authors, .plugin-dependencies, .plugin-soft-dependencies, .plugin-file-info {
margin-bottom: 1.5rem;
}
.section-label {
font-weight: bold;
margin-bottom: 0.25rem;
color: var(--text-secondary-color);
}
.file-path, .file-hash {
word-break: break-all;
font-family: monospace;
background-color: var(--surface-color);
padding: 0.5rem;
border-radius: 4px;
margin-bottom: 1rem;
font-size: 0.9rem;
border: 1px solid var(--border-color);
}

298
src/App.tsx Normal file
View File

@ -0,0 +1,298 @@
import { useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
import "./App.css";
type ServerType =
| 'Paper'
| 'Spigot'
| 'Bukkit'
| 'Vanilla'
| 'Forge'
| 'Fabric'
| 'Velocity'
| 'BungeeCord'
| 'Waterfall'
| 'Unknown';
interface ServerInfo {
server_type: ServerType;
minecraft_version?: string;
plugins_directory: string;
plugins_count: number;
}
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;
}
interface ScanResult {
server_info: ServerInfo;
plugins: Plugin[];
}
interface PluginDetailsProps {
plugin: Plugin;
onClose: () => void;
}
// Get server type icon
function getServerTypeIcon(serverType: ServerType): string {
switch (serverType) {
case 'Paper':
return '📄';
case 'Spigot':
return '🔌';
case 'Bukkit':
return '🪣';
case 'Vanilla':
return '🧊';
case 'Forge':
return '🔨';
case 'Fabric':
return '🧵';
case 'Velocity':
return '⚡';
case 'BungeeCord':
return '🔗';
case 'Waterfall':
return '🌊';
default:
return '❓';
}
}
// Get a formatted server type name for display
function getServerTypeName(serverType: ServerType): string {
return serverType === 'Unknown' ? 'Unknown Server' : serverType;
}
function PluginDetails({ plugin, onClose }: PluginDetailsProps) {
return (
<div className="plugin-details-modal">
<div className="plugin-details-content">
<button className="close-button" onClick={onClose}>&times;</button>
<h2>{plugin.name}</h2>
<div className="plugin-version-display">Version: {plugin.version}</div>
{plugin.description && (
<div className="plugin-description">{plugin.description}</div>
)}
{plugin.authors && plugin.authors.length > 0 && (
<div className="plugin-authors">
<div className="section-label">Authors:</div>
<div>{plugin.authors.join(", ")}</div>
</div>
)}
{plugin.depend && plugin.depend.length > 0 && (
<div className="plugin-dependencies">
<div className="section-label">Dependencies:</div>
<div>{plugin.depend.join(", ")}</div>
</div>
)}
{plugin.soft_depend && plugin.soft_depend.length > 0 && (
<div className="plugin-soft-dependencies">
<div className="section-label">Soft Dependencies:</div>
<div>{plugin.soft_depend.join(", ")}</div>
</div>
)}
<div className="plugin-file-info">
<div className="section-label">File Path:</div>
<div className="file-path">{plugin.file_path}</div>
<div className="section-label">File Hash (SHA-256):</div>
<div className="file-hash">{plugin.file_hash}</div>
</div>
</div>
</div>
);
}
// Add this component after the PluginDetails component and before the App component
function ServerInfoDisplay({ serverInfo }: { serverInfo: ServerInfo | null }) {
if (!serverInfo) return null;
return (
<div className="server-info">
<h2>Server Information</h2>
<div className="server-type">
<span className="server-icon">{getServerTypeIcon(serverInfo.server_type)}</span>
<span className="server-type-name">{getServerTypeName(serverInfo.server_type)}</span>
</div>
<div className="minecraft-version">
<span className="version-label">Minecraft Version</span>
<span className="version-value">{serverInfo.minecraft_version || "Unknown"}</span>
</div>
<div className="plugins-path">
<span className="path-label">Plugins Directory</span>
<span className="path-value">{serverInfo.plugins_directory || "Unknown"}</span>
</div>
<div className="plugins-count">
<b>{serverInfo.plugins_count}</b> plugins found
</div>
</div>
);
}
function App() {
const [serverPath, setServerPath] = useState("");
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
const [plugins, setPlugins] = useState<Plugin[]>([]);
const [isScanning, setIsScanning] = useState(false);
const [scanComplete, setScanComplete] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedPlugin, setSelectedPlugin] = useState<Plugin | null>(null);
async function selectDirectory() {
try {
const selected = await open({
directory: true,
multiple: false,
title: 'Select Minecraft Server Directory'
});
if (selected !== null) {
if (typeof selected === 'string') {
setServerPath(selected);
setError(null);
} else if (Array.isArray(selected)) {
const selectedArr = selected as string[];
if (selectedArr.length > 0) {
setServerPath(selectedArr[0]);
setError(null);
}
}
}
} catch (err) {
console.error('Failed to open directory:', err);
setError("Failed to open directory selector. Please enter path manually.");
}
}
async function scanForPlugins() {
try {
setIsScanning(true);
setError(null);
// Call the Rust backend
const result = await invoke<ScanResult>("scan_server_directory", { path: serverPath });
setServerInfo(result.server_info);
setPlugins(result.plugins);
setIsScanning(false);
setScanComplete(true);
} catch (err) {
console.error("Error scanning for plugins:", err);
setError(err as string);
setIsScanning(false);
}
}
const showPluginDetails = (plugin: Plugin) => {
setSelectedPlugin(plugin);
};
const closePluginDetails = () => {
setSelectedPlugin(null);
};
return (
<div className="app-container">
<header className="app-header">
<h1>🔧 PlugSnatcher</h1>
<p>Minecraft Plugin Manager</p>
</header>
<main className="app-content">
<section className="server-selector">
<h2>Select Server Directory</h2>
<div className="input-group">
<input
type="text"
value={serverPath}
onChange={(e) => setServerPath(e.target.value)}
placeholder="Enter server directory path..."
/>
<button onClick={selectDirectory}>Browse</button>
</div>
<button
className="scan-button"
onClick={scanForPlugins}
disabled={isScanning || !serverPath}
>
{isScanning ? "Scanning..." : "Scan for Plugins"}
</button>
{error && (
<div className="error-message">
{error}
</div>
)}
</section>
{scanComplete && serverInfo && (
<ServerInfoDisplay serverInfo={serverInfo} />
)}
{scanComplete && (
<section className="plugins-list">
<h2>Installed Plugins ({plugins.length})</h2>
{plugins.length > 0 ? (
<>
<div className="plugins-header">
<span>Name</span>
<span>Current Version</span>
<span>Latest Version</span>
<span>Actions</span>
</div>
{plugins.map((plugin, index) => (
<div key={index} className={`plugin-item ${plugin.has_update ? 'has-update' : ''}`}>
<div className="plugin-name">{plugin.name}</div>
<div className="plugin-version">{plugin.version}</div>
<div className="plugin-latest-version">{plugin.latest_version || 'Unknown'}</div>
<div className="plugin-actions">
{plugin.has_update && (
<button className="update-button">Update</button>
)}
<button className="info-button" onClick={() => showPluginDetails(plugin)}>Info</button>
</div>
</div>
))}
</>
) : (
<p>No plugins found in this directory.</p>
)}
</section>
)}
{selectedPlugin && (
<PluginDetails plugin={selectedPlugin} onClose={closePluginDetails} />
)}
</main>
<footer className="app-footer">
<p>PlugSnatcher v0.1.0 - Developed with 💻 and </p>
</footer>
</div>
);
}
export default App;

1
src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

9
src/main.tsx Normal file
View File

@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

25
tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

32
vite.config.ts Normal file
View File

@ -0,0 +1,32 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [react()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));