Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f733ed39b | ||
|
|
0133b61944 | ||
|
|
20592954f7 | ||
|
|
06b2f0ffaf |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
[package]
|
||||
name = "cursor_rush"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
iced = { version = "0.13.1", default-features = true, features = ["image", "advanced"] } # GUI, bundles wgpu + winit
|
||||
tray-icon = "0.20.1" # cross‑platform system tray
|
||||
image = "0.24.7" # Image processing
|
||||
walkdir = "2.4.0" # Directory traversal
|
||||
global-hotkey = "0.4.2" # Global hotkey registration
|
||||
lazy_static = "1.4.0" # Thread-safe static initialization
|
||||
steamworks = { version = "0.11.0", features = ["raw-bindings"] }
|
||||
anyhow = "1.0.98"
|
||||
once_cell = "1.18.0" # Thread-safe lazy initialized global variables
|
||||
|
||||
# Platform-specific dependencies
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3.9", features = ["winuser", "wingdi", "consoleapi"] } # Windows API
|
||||
ctor = "0.2.4" # Constructor/destructor attributes
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2.7" # Objective-C bindings
|
||||
cocoa = "0.24.1" # macOS Cocoa bindings
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
x11 = { version = "2.21.0", features = ["xlib"] } # X11 bindings
|
||||
@@ -11,47 +11,31 @@ A cross-platform application for customizing your cursor with gameified logic.
|
||||
- System tray integration with context menu
|
||||
- Global hotkey support
|
||||
- Steam integration for item drops
|
||||
- Tauri-based frontend with React
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Application core logic
|
||||
│ ├── mod.rs # Main app module exports
|
||||
│ ├── state.rs # Application state management
|
||||
│ ├── messages.rs # Message definitions
|
||||
│ └── cursor.rs # Cursor-related functionality
|
||||
├── ui/ # User interface components
|
||||
│ ├── mod.rs # UI module exports
|
||||
│ ├── view.rs # View rendering logic
|
||||
│ └── tray.rs # System tray functionality
|
||||
├── platform/ # Platform-specific implementations
|
||||
│ ├── mod.rs # Platform module exports
|
||||
│ ├── windows/ # Windows-specific code
|
||||
│ │ ├── mod.rs # Windows module exports
|
||||
│ │ ├── types.rs # Windows type definitions
|
||||
│ │ ├── state.rs # Windows state management
|
||||
│ │ └── cursor.rs # Windows cursor handling
|
||||
│ ├── macos/ # macOS-specific code (in development)
|
||||
│ │ ├── mod.rs # macOS module exports
|
||||
│ │ ├── state.rs # macOS state management
|
||||
│ │ └── cursor.rs # macOS cursor handling
|
||||
│ ├── linux/ # Linux-specific code (in development)
|
||||
│ │ ├── mod.rs # Linux module exports
|
||||
│ │ ├── state.rs # Linux state management
|
||||
│ │ └── cursor.rs # Linux cursor handling
|
||||
│ └── unsupported/ # Fallback for unsupported platforms
|
||||
│ ├── mod.rs # Unsupported module exports
|
||||
│ ├── state.rs # Unsupported state management
|
||||
│ └── cursor.rs # Unsupported cursor handling
|
||||
├── integrations/ # External integrations
|
||||
│ ├── mod.rs # Integrations module exports
|
||||
│ └── steam.rs # Steam integration
|
||||
├── utils/ # Utility functions and helpers
|
||||
│ ├── mod.rs # Utils module exports
|
||||
│ ├── config.rs # Configuration management
|
||||
│ └── logging.rs # Logging utilities
|
||||
└── main.rs # Application entry point
|
||||
frontend/
|
||||
├── src/ # React frontend code
|
||||
│ ├── App.jsx # Main React component
|
||||
│ ├── components/ # UI components
|
||||
│ └── cursors/ # Cursor image files
|
||||
├── src-tauri/ # Rust backend code
|
||||
│ ├── src/ # Rust source code
|
||||
│ │ ├── lib.rs # Main library code
|
||||
│ │ ├── main.rs # Application entry point
|
||||
│ │ ├── integrations/ # External integrations
|
||||
│ │ │ ├── mod.rs # Integrations module exports
|
||||
│ │ │ └── steam.rs # Steam integration
|
||||
│ │ └── platform/ # Platform-specific code
|
||||
│ │ ├── mod.rs # Platform module exports
|
||||
│ │ ├── types.rs # Common type definitions
|
||||
│ │ ├── windows/ # Windows-specific code
|
||||
│ │ ├── macos/ # macOS-specific code
|
||||
│ │ └── linux/ # Linux-specific code
|
||||
│ └── Cargo.toml # Rust dependencies
|
||||
└── package.json # npm dependencies
|
||||
```
|
||||
|
||||
## Building
|
||||
@@ -60,6 +44,8 @@ src/
|
||||
|
||||
- Rust 1.70.0 or later
|
||||
- Cargo
|
||||
- Node.js 16.0.0 or later
|
||||
- npm 7.0.0 or later
|
||||
- Platform-specific dependencies:
|
||||
- Windows: None (fully supported)
|
||||
- macOS: Development in progress
|
||||
@@ -68,41 +54,49 @@ src/
|
||||
### Dependencies
|
||||
|
||||
The application uses the following major dependencies:
|
||||
- iced 0.13.1 - GUI framework
|
||||
- tray-icon 0.20.1 - System tray functionality
|
||||
- Tauri 1.6.0 - Cross-platform framework
|
||||
- React - Frontend framework
|
||||
- image 0.24.7 - Image processing
|
||||
- global-hotkey 0.4.2 - Global hotkey registration
|
||||
- steamworks 0.11.0 - Steam integration
|
||||
- winapi 0.3.9 - Windows API bindings
|
||||
- once_cell 1.19.0 - Lazy initialization
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Debug build
|
||||
cargo build
|
||||
# Navigate to the frontend directory
|
||||
cd frontend
|
||||
|
||||
# Release build
|
||||
cargo build --release
|
||||
# Install npm dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode
|
||||
npm run tauri dev
|
||||
|
||||
# Build for production
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Place your cursor PNG files in the `assets` directory (PNG format only)
|
||||
2. Run the application using `cargo run` or the compiled executable
|
||||
3. The application will load all PNG files from the assets directory
|
||||
4. Select a cursor from the list in the UI
|
||||
5. The application will replace your system cursor with the selected image
|
||||
6. When closing the window, the application minimizes to the system tray
|
||||
7. To fully exit, use the system tray menu or press the exit hotkey
|
||||
1. Make sure you have Node.js and npm installed
|
||||
2. Navigate to the frontend directory: `cd frontend`
|
||||
3. Install dependencies if you haven't already: `npm install`
|
||||
4. Run the application in development mode: `npm run tauri dev`
|
||||
5. The application will start with a React frontend and Rust backend
|
||||
6. Use the UI to browse and select cursors
|
||||
7. The application will replace your system cursor with the selected image
|
||||
8. To exit, use the "Quit" button in the application or from the system tray
|
||||
|
||||
### Custom Cursors
|
||||
|
||||
The application comes with two sample cursors in the `assets` directory:
|
||||
The application comes with sample cursors:
|
||||
- `default.png` - A simple default cursor
|
||||
- `icon.png` - An icon cursor
|
||||
- `red.png` - A red-colored cursor
|
||||
|
||||
To add your own cursors:
|
||||
1. Create PNG images with transparent backgrounds
|
||||
2. Place them in the `assets` directory
|
||||
2. Place them in the `frontend/src/cursors` directory
|
||||
3. Restart the application to load the new cursors
|
||||
4. The cursor name in the UI will be derived from the filename
|
||||
|
||||
@@ -147,6 +141,7 @@ Contributions to Cursor Rush are welcome! Here are some ways you can contribute:
|
||||
- Submit pull requests with bug fixes or new features
|
||||
- Help with documentation
|
||||
- Contribute to macOS and Linux platform support
|
||||
- Improve the Tauri integration
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,12 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
@@ -0,0 +1,33 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
<!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>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@tauri-apps/cli": "^1.6.3",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -0,0 +1,3 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "cursor-rush"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
image = "0.24"
|
||||
# Ensure "consoleapi" is in this features list
|
||||
winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] }
|
||||
once_cell = "1.19.0"
|
||||
steamworks = "0.10.0"
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 407 B |
@@ -0,0 +1,26 @@
|
||||
// Steam integration
|
||||
|
||||
use steamworks::{AppId, Client, SingleClient};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Steam {
|
||||
client: Client,
|
||||
single: SingleClient,
|
||||
}
|
||||
|
||||
impl Steam {
|
||||
pub fn new() -> Self {
|
||||
let (client, single) = Client::init_app(AppId(1430270)).unwrap();
|
||||
Self { client, single }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn drop_item(&self) {
|
||||
// Placeholder for item drop functionality
|
||||
// The actual implementation would use the Steam API to trigger an item drop
|
||||
println!("Steam item drop triggered (placeholder)");
|
||||
|
||||
// In a real implementation with the correct API version, we would use:
|
||||
// self.client.get_inventory().trigger_item_drop(item_def_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
use std::sync::Mutex;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tauri::{Manager, AppHandle, CustomMenuItem, SystemTray, SystemTrayMenu, SystemTrayMenuItem, SystemTrayEvent, WindowEvent};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::env;
|
||||
|
||||
mod platform;
|
||||
mod integrations;
|
||||
|
||||
// Define cursor structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Cursor {
|
||||
name: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
// App state to track cursors
|
||||
pub struct AppState {
|
||||
cursors: Vec<Cursor>,
|
||||
current_index: usize,
|
||||
}
|
||||
|
||||
// Thread-safe app state wrapper
|
||||
pub struct AppStateWrapper(Mutex<AppState>);
|
||||
|
||||
// Command to get all cursors
|
||||
#[tauri::command]
|
||||
fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> {
|
||||
let app_state = state.0.lock().unwrap();
|
||||
app_state.cursors.clone()
|
||||
}
|
||||
|
||||
// Command to get current cursor index
|
||||
#[tauri::command]
|
||||
fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize {
|
||||
let app_state = state.0.lock().unwrap();
|
||||
app_state.current_index
|
||||
}
|
||||
|
||||
// Command to select a cursor
|
||||
#[tauri::command]
|
||||
fn select_cursor(index: usize, state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<(), String> {
|
||||
println!("=== RESOURCE DEBUG [select_cursor] ===");
|
||||
|
||||
let mut app_state = state.0.lock().unwrap();
|
||||
if index < app_state.cursors.len() {
|
||||
app_state.current_index = index;
|
||||
|
||||
let cursor = &app_state.cursors[index];
|
||||
println!("Cursor selected: {}", cursor.name);
|
||||
println!("Original path: {}", cursor.path);
|
||||
|
||||
// Get the resource directory
|
||||
let resource_dir = app_handle.path_resolver().resource_dir();
|
||||
println!("Resource dir: {:?}", resource_dir);
|
||||
|
||||
// List contents of resource dir
|
||||
if let Some(res_dir) = &resource_dir {
|
||||
println!("Listing contents of resource dir: {}", res_dir.display());
|
||||
if let Ok(entries) = std::fs::read_dir(res_dir) {
|
||||
for (i, entry) in entries.filter_map(Result::ok).enumerate() {
|
||||
println!(" {}: {}", i, entry.path().display());
|
||||
|
||||
// List subdirectories
|
||||
if entry.path().is_dir() {
|
||||
if let Ok(subentries) = std::fs::read_dir(entry.path()) {
|
||||
for (j, subentry) in subentries.filter_map(Result::ok).enumerate() {
|
||||
println!(" {}.{}: {}", i, j, subentry.path().display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Failed to read resource directory");
|
||||
}
|
||||
}
|
||||
|
||||
let filename = Path::new(&cursor.path)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
|
||||
|
||||
println!("Extracted filename: {}", filename);
|
||||
|
||||
// Try using the actual file path directly
|
||||
println!("Using direct file path: {}", cursor.path);
|
||||
|
||||
// Verify the file exists
|
||||
if Path::new(&cursor.path).exists() {
|
||||
println!("File exists, applying cursor...");
|
||||
|
||||
if let Err(e) = platform::set_system_cursor(&cursor.path) {
|
||||
println!("Error applying cursor: {}", e);
|
||||
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
|
||||
}
|
||||
|
||||
// Emit an event to notify the frontend
|
||||
let _ = app_handle.emit_all("cursor_changed", index); // Ignore error
|
||||
|
||||
println!("Successfully applied cursor using actual file path");
|
||||
return Ok(());
|
||||
} else {
|
||||
println!("File doesn't exist: {}", cursor.path);
|
||||
return Err(format!("Cursor file does not exist: {}", cursor.path));
|
||||
}
|
||||
} else {
|
||||
Err("Index out of bounds".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Command to go to the next cursor
|
||||
#[tauri::command]
|
||||
fn next_cursor(state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<usize, String> {
|
||||
println!("=== RESOURCE DEBUG [next_cursor] ===");
|
||||
|
||||
let mut app_state = state.0.lock().unwrap();
|
||||
if !app_state.cursors.is_empty() {
|
||||
app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len();
|
||||
|
||||
let cursor = &app_state.cursors[app_state.current_index];
|
||||
println!("Next cursor: {} ({})", cursor.name, cursor.path);
|
||||
|
||||
let filename = Path::new(&cursor.path)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
|
||||
|
||||
// Try both resource paths like in select_cursor
|
||||
println!("Extracted filename: {}", filename);
|
||||
|
||||
// Try using the actual file path directly
|
||||
println!("Using direct file path: {}", cursor.path);
|
||||
|
||||
// Verify the file exists
|
||||
if Path::new(&cursor.path).exists() {
|
||||
println!("File exists, applying cursor...");
|
||||
|
||||
if let Err(e) = platform::set_system_cursor(&cursor.path) {
|
||||
println!("Error applying cursor: {}", e);
|
||||
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
|
||||
}
|
||||
|
||||
// Emit an event to notify the frontend
|
||||
let _ = app_handle.emit_all("cursor_changed", app_state.current_index); // Ignore error
|
||||
|
||||
println!("Successfully applied cursor using actual file path");
|
||||
return Ok(app_state.current_index);
|
||||
} else {
|
||||
println!("File doesn't exist: {}", cursor.path);
|
||||
return Err(format!("Cursor file does not exist: {}", cursor.path));
|
||||
}
|
||||
} else {
|
||||
Err("No cursors available".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Command to quit the application
|
||||
#[tauri::command]
|
||||
fn quit_app(app_handle: AppHandle) {
|
||||
// Clean up and restore default cursor before exit
|
||||
match platform::restore_system_cursor() {
|
||||
Ok(_) => println!("Default cursor restored"),
|
||||
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
|
||||
}
|
||||
|
||||
// Exit the application
|
||||
std::thread::spawn(move || {
|
||||
app_handle.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// Command to minimize the window to tray
|
||||
#[tauri::command]
|
||||
fn minimize_to_tray(app_handle: AppHandle) -> Result<(), String> {
|
||||
println!("Minimizing to tray");
|
||||
|
||||
// Get the main window
|
||||
let window = app_handle.get_window("main")
|
||||
.ok_or_else(|| "Failed to get main window".to_string())?;
|
||||
|
||||
// Hide the window
|
||||
window.hide().map_err(|e| format!("Failed to hide window: {}", e))?;
|
||||
|
||||
// The tray should already be visible as configured in tauri.conf.json
|
||||
println!("Window hidden, application should now be accessible from the system tray");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Function to load cursor files from src/cursors directory
|
||||
fn load_cursor_files() -> Vec<Cursor> {
|
||||
println!("=== RESOURCE DEBUG [load_cursor_files] ===");
|
||||
let mut cursors = Vec::new();
|
||||
|
||||
// Determine the path to src/cursors relative to src-tauri (CARGO_MANIFEST_DIR)
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
println!("CARGO_MANIFEST_DIR: {}", manifest_dir.display());
|
||||
|
||||
// Check multiple possible locations
|
||||
let possible_cursor_dirs = vec![
|
||||
manifest_dir.join("../src/cursors"),
|
||||
manifest_dir.join("src/cursors"),
|
||||
manifest_dir.join("cursors"),
|
||||
manifest_dir.clone(), // Check manifest dir itself
|
||||
];
|
||||
|
||||
println!("CHECKING FOR PNG/CUR FILES IN MULTIPLE DIRECTORIES:");
|
||||
for (i, cursors_dir) in possible_cursor_dirs.iter().enumerate() {
|
||||
println!("Checking dir {}: {}", i+1, cursors_dir.display());
|
||||
println!(" Directory exists: {}", cursors_dir.exists());
|
||||
|
||||
if cursors_dir.exists() {
|
||||
println!(" Looking for cursor files in this directory...");
|
||||
|
||||
if let Ok(entries) = std::fs::read_dir(cursors_dir) {
|
||||
for entry in entries.filter_map(Result::ok) {
|
||||
let path = entry.path();
|
||||
println!(" Found: {}", path.display());
|
||||
|
||||
if path.is_file() && path.extension().map_or(false, |ext| ext == "png" || ext == "cur") {
|
||||
let name = path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
|
||||
println!(" ADDING CURSOR: {} at path {}", name, path.display());
|
||||
cursors.push(Cursor {
|
||||
name,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !cursors.is_empty() {
|
||||
println!(" Found {} cursor files in this directory.", cursors.len());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we found no cursors, use mock data with paths relative to what resolve_resource expects
|
||||
if cursors.is_empty() {
|
||||
println!("No cursors found in any directory. Using mock data instead.");
|
||||
println!("IMPORTANT: This is probably the source of your problem!");
|
||||
println!("The app could not find your cursor PNG files. Check that they exist in one of these directories:");
|
||||
for dir in &possible_cursor_dirs {
|
||||
println!(" {}", dir.display());
|
||||
}
|
||||
|
||||
// Mock data paths should reflect their eventual resource path structure
|
||||
cursors.push(Cursor {
|
||||
name: "Default".to_string(),
|
||||
path: "src/cursors/default.png".to_string(), // Filename will be 'default.png'
|
||||
});
|
||||
|
||||
cursors.push(Cursor {
|
||||
name: "Icon".to_string(),
|
||||
path: "src/cursors/icon.png".to_string(), // Filename will be 'icon.png'
|
||||
});
|
||||
|
||||
cursors.push(Cursor {
|
||||
name: "Red".to_string(),
|
||||
path: "src/cursors/red.png".to_string(), // Filename will be 'red.png'
|
||||
});
|
||||
|
||||
println!("Created {} mock cursor entries", cursors.len());
|
||||
}
|
||||
|
||||
println!("Loaded {} cursor files", cursors.len());
|
||||
|
||||
// Sort cursors by name for consistent ordering
|
||||
cursors.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
cursors
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
println!("Starting Cursor Rush application");
|
||||
|
||||
// Initialize platform-specific cursor handling
|
||||
if let Err(e) = platform::initialize() {
|
||||
eprintln!("Failed to initialize platform: {}", e);
|
||||
}
|
||||
|
||||
// Load cursors from assets directory
|
||||
let cursors = load_cursor_files();
|
||||
|
||||
// Create app state
|
||||
let app_state = AppState {
|
||||
cursors,
|
||||
current_index: 0,
|
||||
};
|
||||
|
||||
// Create system tray menu
|
||||
let next_cursor_item = CustomMenuItem::new("next_cursor".to_string(), "Next cursor");
|
||||
let show_window_item = CustomMenuItem::new("show_window".to_string(), "Show window");
|
||||
let quit_item = CustomMenuItem::new("quit".to_string(), "Quit");
|
||||
|
||||
let tray_menu = SystemTrayMenu::new()
|
||||
.add_item(next_cursor_item)
|
||||
.add_item(show_window_item)
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(quit_item);
|
||||
|
||||
let system_tray = SystemTray::new().with_menu(tray_menu);
|
||||
|
||||
// This is one of the simplest ways to initialize Tauri
|
||||
tauri::Builder::default()
|
||||
.manage(AppStateWrapper(Mutex::new(app_state)))
|
||||
.system_tray(system_tray)
|
||||
.on_system_tray_event(|app, event| match event {
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
println!("System tray received a left click");
|
||||
if let Some(window) = app.get_window("main") {
|
||||
match window.is_visible() {
|
||||
Ok(visible) => {
|
||||
if visible {
|
||||
println!("Window is already visible, focusing it");
|
||||
let _ = window.set_focus();
|
||||
} else {
|
||||
println!("Window is hidden, showing it");
|
||||
if let Err(e) = window.show() {
|
||||
eprintln!("Failed to show window: {}", e);
|
||||
}
|
||||
if let Err(e) = window.set_focus() {
|
||||
eprintln!("Failed to focus window: {}", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to check window visibility: {}", e);
|
||||
// Try to show it anyway
|
||||
if let Err(e) = window.show() {
|
||||
eprintln!("Failed to show window: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Main window not found");
|
||||
}
|
||||
}
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => {
|
||||
println!("Menu item clicked: {}", id);
|
||||
match id.as_str() {
|
||||
"next_cursor" => {
|
||||
println!("Next cursor requested from tray");
|
||||
let state = app.state::<AppStateWrapper>();
|
||||
let mut app_state = state.0.lock().unwrap();
|
||||
|
||||
if !app_state.cursors.is_empty() {
|
||||
app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len();
|
||||
let cursor = &app_state.cursors[app_state.current_index];
|
||||
|
||||
if Path::new(&cursor.path).exists() {
|
||||
if let Err(e) = platform::set_system_cursor(&cursor.path) {
|
||||
eprintln!("Error applying cursor from tray: {}", e);
|
||||
} else {
|
||||
// Emit an event to notify the frontend
|
||||
let _ = app.emit_all("cursor_changed", app_state.current_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"show_window" => {
|
||||
println!("Show window requested from tray");
|
||||
if let Some(window) = app.get_window("main") {
|
||||
if let Err(e) = window.show() {
|
||||
eprintln!("Failed to show window: {}", e);
|
||||
}
|
||||
if let Err(e) = window.set_focus() {
|
||||
eprintln!("Failed to focus window: {}", e);
|
||||
}
|
||||
} else {
|
||||
eprintln!("Main window not found");
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
println!("Quit requested from tray");
|
||||
// Clean up and restore default cursor before exit
|
||||
match platform::restore_system_cursor() {
|
||||
Ok(_) => println!("Default cursor restored"),
|
||||
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
|
||||
}
|
||||
app.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.on_window_event(move |event| {
|
||||
match event.event() {
|
||||
WindowEvent::CloseRequested { api, .. } => {
|
||||
// Prevent the default close action
|
||||
api.prevent_close();
|
||||
|
||||
// Get the window and hide it
|
||||
let window = event.window();
|
||||
println!("Window close requested in backend handler, hiding window");
|
||||
|
||||
// Hide the window instead of closing it
|
||||
if let Err(e) = window.hide() {
|
||||
eprintln!("Failed to hide window: {}", e);
|
||||
} else {
|
||||
println!("Window hidden successfully, app is now in system tray");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_cursors,
|
||||
get_current_cursor_index,
|
||||
select_cursor,
|
||||
next_cursor,
|
||||
quit_app,
|
||||
minimize_to_tray
|
||||
])
|
||||
.setup(|_app| {
|
||||
// Register for events or perform additional setup
|
||||
println!("Tauri application setup complete");
|
||||
|
||||
// We're not setting up a window close handler here anymore
|
||||
// The frontend will handle this based on the minimize-to-tray setting
|
||||
|
||||
Ok(())
|
||||
})
|
||||
// This matches exactly how it was before our changes
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -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() {
|
||||
cursor_rush::run() // Corrected line
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Linux platform implementation for cursor handling
|
||||
use super::Result;
|
||||
|
||||
// Initialize cursor handling
|
||||
pub fn init() -> Result<()> {
|
||||
println!("Linux cursor initialization not yet implemented");
|
||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
||||
}
|
||||
|
||||
pub fn set_cursor(path: &str) -> Result<()> {
|
||||
// Linux cursor implementation would go here
|
||||
println!("Linux cursor setting not yet implemented for path: {}", path);
|
||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
||||
}
|
||||
|
||||
pub fn restore_default_cursor() -> Result<()> {
|
||||
// Linux cursor restoration would go here
|
||||
println!("Linux cursor restoration not yet implemented");
|
||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// macOS platform implementation for cursor handling
|
||||
use super::Result;
|
||||
|
||||
// Initialize cursor handling
|
||||
pub fn init() -> Result<()> {
|
||||
println!("macOS cursor initialization not yet implemented");
|
||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
||||
}
|
||||
|
||||
pub fn set_cursor(path: &str) -> Result<()> {
|
||||
// macOS cursor implementation would go here
|
||||
println!("macOS cursor setting not yet implemented for path: {}", path);
|
||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
||||
}
|
||||
|
||||
pub fn restore_default_cursor() -> Result<()> {
|
||||
// macOS cursor restoration would go here
|
||||
println!("macOS cursor restoration not yet implemented");
|
||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Platform-specific cursor handling
|
||||
|
||||
// Common types for cursor handling
|
||||
pub mod types;
|
||||
|
||||
// Windows implementation
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
// macOS implementation
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
// Linux implementation
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
|
||||
// Error type for platform operations
|
||||
pub type Result<T> = std::result::Result<T, String>;
|
||||
|
||||
// Platform-agnostic interface for cursor handling
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use self::windows::{set_cursor, restore_default_cursor, init};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use self::macos::{set_cursor, restore_default_cursor, init};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use self::linux::{set_cursor, restore_default_cursor, init};
|
||||
|
||||
// Initialize platform-specific cursor handling
|
||||
pub fn initialize() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
init()
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-platform implementation for setting cursor
|
||||
pub fn set_system_cursor(path: &str) -> Result<()> {
|
||||
println!("Setting system cursor: {}", path);
|
||||
set_cursor(path)
|
||||
}
|
||||
|
||||
// Cross-platform implementation for restoring default cursor
|
||||
pub fn restore_system_cursor() -> Result<()> {
|
||||
println!("Restoring system cursor");
|
||||
restore_default_cursor()
|
||||
}
|
||||
|
||||
// Default implementations for non-supported platforms
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
pub fn set_cursor(_path: &str) -> Result<()> {
|
||||
Err("Cursor customization not supported on this platform".to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
pub fn restore_default_cursor() -> Result<()> {
|
||||
Err("Cursor restoration not supported on this platform".to_string())
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::path::PathBuf;
|
||||
use image::RgbaImage;
|
||||
|
||||
// Windows-specific cursor handle wrapper
|
||||
#[cfg(target_os = "windows")]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct WinCursorWrapper {
|
||||
pub handle: winapi::shared::windef::HCURSOR,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe impl Send for WinCursorWrapper {}
|
||||
|
||||
// Common type for cursor information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CursorInfo {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub image: Option<RgbaImage>,
|
||||
pub hotspot_x: i32,
|
||||
pub hotspot_y: i32,
|
||||
|
||||
// Platform-specific fields
|
||||
#[cfg(target_os = "windows")]
|
||||
pub win_cursor: Option<WinCursorWrapper>,
|
||||
}
|
||||
@@ -68,15 +68,17 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
||||
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
// Create the color DIB section
|
||||
let mut color_bits_ptr: *mut std::ffi::c_void = null_mut();
|
||||
let mut color_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
|
||||
let mut ppv_bits_color: *mut winapi::ctypes::c_void = color_bits_ptr;
|
||||
let color_bitmap = CreateDIBSection(
|
||||
color_dc,
|
||||
&bitmap_info,
|
||||
DIB_RGB_COLORS,
|
||||
&mut color_bits_ptr,
|
||||
&mut ppv_bits_color,
|
||||
null_mut(),
|
||||
0,
|
||||
);
|
||||
color_bits_ptr = ppv_bits_color;
|
||||
|
||||
if color_bitmap.is_null() || color_bits_ptr.is_null() {
|
||||
eprintln!("Failed to create color DIB section");
|
||||
@@ -88,15 +90,17 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
||||
|
||||
// Create the mask DIB section (1-bit monochrome)
|
||||
bitmap_info.bmiHeader.biBitCount = 1; // 1 bit per pixel for mask
|
||||
let mut mask_bits_ptr: *mut std::ffi::c_void = null_mut();
|
||||
let mut mask_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
|
||||
let mut ppv_bits: *mut winapi::ctypes::c_void = mask_bits_ptr;
|
||||
let mask_bitmap = CreateDIBSection(
|
||||
mask_dc,
|
||||
&bitmap_info,
|
||||
DIB_RGB_COLORS,
|
||||
&mut mask_bits_ptr,
|
||||
&mut ppv_bits,
|
||||
null_mut(),
|
||||
0,
|
||||
);
|
||||
mask_bits_ptr = ppv_bits;
|
||||
|
||||
if mask_bitmap.is_null() || mask_bits_ptr.is_null() {
|
||||
eprintln!("Failed to create mask DIB section");
|
||||
@@ -328,20 +332,23 @@ pub fn hide_system_cursor() -> bool {
|
||||
}
|
||||
|
||||
// Update the cursor image using CursorInfo (with caching)
|
||||
pub fn update_cursor_info(cursor_info: &mut crate::platform::CursorInfo) -> bool {
|
||||
pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo) -> bool {
|
||||
println!("Updating cursor to: {}", cursor_info.name);
|
||||
|
||||
// Use cached image if available, otherwise load from disk
|
||||
let image = if let Some(ref cached_image) = cursor_info.image {
|
||||
let image: image::RgbaImage = if let Some(ref cached_image) = cursor_info.image {
|
||||
println!("Using cached image for cursor: {}", cursor_info.name);
|
||||
cached_image.clone()
|
||||
let img_clone: image::RgbaImage = cached_image.clone();
|
||||
img_clone
|
||||
} else {
|
||||
println!("Loading image from disk for cursor: {}", cursor_info.name);
|
||||
match image_open(&cursor_info.path) {
|
||||
Ok(img) => {
|
||||
let rgba = img.to_rgba8();
|
||||
// Cache the image for future use
|
||||
cursor_info.image = Some(rgba.clone());
|
||||
// Make sure to specify the type to avoid type inference issues
|
||||
let image_copy: image::RgbaImage = rgba.clone();
|
||||
cursor_info.image = Some(image_copy);
|
||||
rgba
|
||||
},
|
||||
Err(e) => {
|
||||
@@ -381,8 +388,10 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::CursorInfo) -> bool
|
||||
// Update the cached cursor in CursorInfo
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Always update the cached cursor to the fresh one
|
||||
cursor_info.win_cursor = Some(new_cursor_wrapper);
|
||||
// Convert SyncHCURSOR to WinCursorWrapper for storage in cursor_info
|
||||
cursor_info.win_cursor = Some(crate::platform::types::WinCursorWrapper {
|
||||
handle: new_cursor_wrapper.0
|
||||
});
|
||||
}
|
||||
|
||||
if !success {
|
||||
@@ -397,12 +406,14 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::CursorInfo) -> bool
|
||||
|
||||
// If we failed to create a new cursor, try to use the cached one as a fallback
|
||||
#[cfg(windows)]
|
||||
if let Some(cached_cursor) = cursor_info.win_cursor {
|
||||
if !cached_cursor.0.is_null() {
|
||||
if let Some(cached_cursor) = &cursor_info.win_cursor {
|
||||
if !cached_cursor.handle.is_null() {
|
||||
println!("Falling back to cached cursor as a last resort");
|
||||
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
|
||||
*cursor_guard = Some(cached_cursor);
|
||||
SetCursor(cached_cursor.0);
|
||||
// Convert WinCursorWrapper to SyncHCURSOR
|
||||
let sync_cursor = super::types::SyncHCURSOR(cached_cursor.handle);
|
||||
*cursor_guard = Some(sync_cursor);
|
||||
SetCursor(cached_cursor.handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Windows platform implementation module
|
||||
|
||||
pub mod types;
|
||||
mod cursor;
|
||||
mod state;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use crate::platform::Result;
|
||||
|
||||
// Functions expected by the platform-agnostic interface
|
||||
pub fn set_cursor(path: &str) -> Result<()> {
|
||||
println!("Windows platform: setting cursor to {}", path);
|
||||
|
||||
// Create a cursor info struct to pass to update_cursor_info
|
||||
let mut cursor_info = crate::platform::types::CursorInfo {
|
||||
name: path.split('\\').last().unwrap_or("unknown").to_string(),
|
||||
path: PathBuf::from(path),
|
||||
image: None,
|
||||
hotspot_x: 0, // Default hotspot at top-left
|
||||
hotspot_y: 0,
|
||||
#[cfg(target_os = "windows")]
|
||||
win_cursor: None,
|
||||
};
|
||||
|
||||
// Use the existing update_cursor_info function to handle loading and applying the cursor
|
||||
if !cursor::update_cursor_info(&mut cursor_info) {
|
||||
return Err(format!("Failed to update cursor from {}", path));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restore_default_cursor() -> Result<()> {
|
||||
println!("Windows platform: restoring default cursor");
|
||||
cursor::restore_cursor();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init() -> Result<()> {
|
||||
println!("Windows platform: initializing cursor system");
|
||||
if !cursor::hide_system_cursor() {
|
||||
return Err("Failed to initialize cursor system".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"devPath": "http://localhost:5173",
|
||||
"distDir": "../dist"
|
||||
},
|
||||
"package": {
|
||||
"productName": "cursor-rush",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"window": {
|
||||
"all": false,
|
||||
"startDragging": true,
|
||||
"minimize": true,
|
||||
"hide": true,
|
||||
"show": true,
|
||||
"close": true
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
},
|
||||
"fs": {
|
||||
"all": false,
|
||||
"scope": [
|
||||
"$APP/cursors/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"systemTray": {
|
||||
"iconPath": "icons/icon.png",
|
||||
"iconAsTemplate": false,
|
||||
"menuOnLeftClick": false,
|
||||
"title": "Cursor Rush"
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"identifier": "com.cursor-rush.dev",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"targets": "all"
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "Cursor Rush",
|
||||
"width": 310,
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"visible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-color: rgba(24, 24, 37, 0.85);
|
||||
--nav-bg: rgba(40, 40, 56, 0.4);
|
||||
--nav-active: rgba(65, 66, 88, 0.7);
|
||||
--border-color: rgba(49, 50, 68, 0.6);
|
||||
--text-color: #cdd6f4;
|
||||
--accent-color: #89b4fa;
|
||||
--button-bg: rgba(65, 66, 88, 0.6);
|
||||
--button-hover: rgba(70, 71, 90, 0.8);
|
||||
--button-active: rgba(137, 180, 250, 0.8);
|
||||
--tab-bg: rgba(40, 40, 56, 0.4);
|
||||
--tab-active: rgba(65, 66, 88, 0.7);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: transparent;
|
||||
color: var(--text-color);
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* Modal Container - Draggable Area */
|
||||
.modal-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Modal - Main App Window */
|
||||
.modal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--bg-color);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--nav-bg);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--nav-bg);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--nav-active);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
background-color: var(--button-bg);
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--button-hover);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: var(--button-active);
|
||||
color: var(--bg-color);
|
||||
}
|
||||
|
||||
/* Images */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import "./App.css";
|
||||
|
||||
// Import cursor images directly
|
||||
import defaultCursor from "./cursors/default.png";
|
||||
import iconCursor from "./cursors/icon.png";
|
||||
import redCursor from "./cursors/red.png";
|
||||
|
||||
// Components
|
||||
import Header from "./components/Header";
|
||||
import Content from "./components/Content";
|
||||
import Footer from "./components/Footer";
|
||||
|
||||
function App() {
|
||||
const [activePage, setActivePage] = useState("cursors");
|
||||
const [cursors, setCursors] = useState([]);
|
||||
const [currentCursorIndex, setCurrentCursorIndex] = useState(0);
|
||||
const [settings, setSettings] = useState({
|
||||
minimizeToTrayOnClose: true,
|
||||
startWithWindows: false,
|
||||
autoApplyCursorOnSelection: true
|
||||
});
|
||||
|
||||
// Function to minimize app to tray
|
||||
const minimizeToTray = useCallback(async () => {
|
||||
try {
|
||||
console.log('Minimizing app to tray');
|
||||
// First hide the window to ensure it disappears immediately
|
||||
await appWindow.hide();
|
||||
console.log('Window hidden, now ensuring tray is visible');
|
||||
|
||||
// Then call the backend to ensure the tray is visible
|
||||
await invoke("minimize_to_tray");
|
||||
console.log('minimize_to_tray command completed successfully');
|
||||
} catch (error) {
|
||||
console.error("Failed to minimize to tray:", error);
|
||||
// Fallback to window API if the invoke fails
|
||||
try {
|
||||
console.log('Trying fallback hide');
|
||||
await appWindow.hide();
|
||||
console.log('Fallback hide succeeded');
|
||||
} catch (fallbackError) {
|
||||
console.error("Fallback hide also failed:", fallbackError);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load cursors on mount and set up event listeners
|
||||
useEffect(() => {
|
||||
const loadCursors = async () => {
|
||||
try {
|
||||
// Get cursors from the Tauri backend
|
||||
const loadedCursors = await invoke("get_cursors");
|
||||
console.log("Loaded cursors:", loadedCursors);
|
||||
setCursors(loadedCursors || []);
|
||||
|
||||
// Get current cursor index
|
||||
const currentIndex = await invoke("get_current_cursor_index");
|
||||
setCurrentCursorIndex(currentIndex);
|
||||
} catch (error) {
|
||||
console.error("Failed to load cursors:", error);
|
||||
// Use the imported cursor images directly
|
||||
setCursors([
|
||||
{ name: "default", path: defaultCursor },
|
||||
{ name: "icon", path: iconCursor },
|
||||
{ name: "red", path: redCursor },
|
||||
// We don't have blue and gold cursors imported since they don't exist yet
|
||||
// These will be excluded for now
|
||||
]);
|
||||
setCurrentCursorIndex(0);
|
||||
}
|
||||
};
|
||||
|
||||
loadCursors();
|
||||
}, []);
|
||||
|
||||
// Set up window close event listener
|
||||
useEffect(() => {
|
||||
const setupCloseHandler = async () => {
|
||||
const unlisten = await appWindow.onCloseRequested(async (event) => {
|
||||
console.log('Window close requested');
|
||||
|
||||
if (settings.minimizeToTrayOnClose) {
|
||||
console.log('Minimize to tray setting is enabled, preventing close and hiding instead');
|
||||
// Prevent the window from closing
|
||||
event.preventDefault();
|
||||
|
||||
// Hide the window instead
|
||||
try {
|
||||
console.log('Calling minimizeToTray function');
|
||||
await minimizeToTray();
|
||||
console.log('Window should now be hidden and app should be in system tray');
|
||||
} catch (error) {
|
||||
console.error('Failed to minimize to tray on close:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('Minimize to tray setting is disabled, allowing window to close');
|
||||
// Let the window close normally, but we need to call quit_app to ensure proper cleanup
|
||||
try {
|
||||
await invoke("quit_app");
|
||||
} catch (error) {
|
||||
console.error("Failed to quit app:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return unlisten;
|
||||
};
|
||||
|
||||
let unlistenFn;
|
||||
setupCloseHandler().then(unlisten => {
|
||||
unlistenFn = unlisten;
|
||||
console.log('Close handler set up successfully');
|
||||
}).catch(err => console.error('Failed to set up close handler:', err));
|
||||
|
||||
// Clean up event listener on unmount
|
||||
return () => {
|
||||
if (unlistenFn) {
|
||||
unlistenFn();
|
||||
}
|
||||
};
|
||||
}, [settings.minimizeToTrayOnClose, minimizeToTray]);
|
||||
|
||||
// Functions to handle cursor operations using Tauri backend
|
||||
const selectCursor = async (index) => {
|
||||
try {
|
||||
console.log(`Selecting cursor at index ${index}`);
|
||||
await invoke("select_cursor", { index });
|
||||
setCurrentCursorIndex(index);
|
||||
} catch (error) {
|
||||
console.error("Failed to select cursor:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const nextCursor = async () => {
|
||||
if (cursors.length === 0) return;
|
||||
|
||||
try {
|
||||
console.log('Switching to next cursor');
|
||||
const newIndex = await invoke("next_cursor");
|
||||
setCurrentCursorIndex(newIndex);
|
||||
} catch (error) {
|
||||
console.error("Failed to switch to next cursor:", error);
|
||||
// Fallback if backend fails
|
||||
const newIndex = (currentCursorIndex + 1) % cursors.length;
|
||||
setCurrentCursorIndex(newIndex);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to exit app
|
||||
const quitApp = async () => {
|
||||
try {
|
||||
console.log('Quitting app');
|
||||
await invoke("quit_app");
|
||||
} catch (error) {
|
||||
console.error("Failed to quit app:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-container">
|
||||
<div className="modal">
|
||||
<Header
|
||||
activePage={activePage}
|
||||
setActivePage={setActivePage}
|
||||
minimizeToTray={minimizeToTray}
|
||||
quitApp={quitApp}
|
||||
settings={settings}
|
||||
/>
|
||||
|
||||
<Content
|
||||
activePage={activePage}
|
||||
cursors={cursors}
|
||||
currentCursorIndex={currentCursorIndex}
|
||||
selectCursor={selectCursor}
|
||||
nextCursor={nextCursor}
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
/>
|
||||
|
||||
<Footer
|
||||
version="0.1.0"
|
||||
status={cursors.length > 0 ? `${cursors.length} cursors loaded` : "No cursors found"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -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 |
@@ -0,0 +1,14 @@
|
||||
/* Content Component Styles */
|
||||
.app-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1vh;
|
||||
border-top: 2px solid rgba(49, 50, 68, 0.6);
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: 18px;
|
||||
color: var(--accent-color, #89b4fa);
|
||||
margin-bottom: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import './Content.css';
|
||||
|
||||
// Import content modules
|
||||
import Inventory from './content_modules/Inventory';
|
||||
import Cursors from './content_modules/Cursors';
|
||||
import Crates from './content_modules/Crates';
|
||||
import Settings from './content_modules/Settings';
|
||||
|
||||
function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCursor, settings, setSettings }) {
|
||||
// Render the appropriate content based on active page
|
||||
const renderContent = () => {
|
||||
switch (activePage) {
|
||||
case 'inventory':
|
||||
return <Inventory
|
||||
cursors={cursors}
|
||||
currentCursorIndex={currentCursorIndex}
|
||||
selectCursor={selectCursor}
|
||||
/>;
|
||||
case 'cursors':
|
||||
return (
|
||||
<Cursors
|
||||
cursors={cursors}
|
||||
currentCursorIndex={currentCursorIndex}
|
||||
selectCursor={selectCursor}
|
||||
nextCursor={nextCursor}
|
||||
/>
|
||||
);
|
||||
case 'crates':
|
||||
return <Crates />;
|
||||
case 'settings':
|
||||
return <Settings settings={settings} setSettings={setSettings} />;
|
||||
default:
|
||||
return <Cursors
|
||||
cursors={cursors}
|
||||
currentCursorIndex={currentCursorIndex}
|
||||
selectCursor={selectCursor}
|
||||
nextCursor={nextCursor}
|
||||
/>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-content">
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Content;
|
||||
@@ -0,0 +1,21 @@
|
||||
/* Footer Component Styles */
|
||||
.app-footer {
|
||||
width: 100%;
|
||||
background-color: var(--footer-bg, #181825);
|
||||
padding: 8px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--border-color, #313244);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.app-status {
|
||||
color: var(--text-color, #cdd6f4);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
color: var(--text-color, #cdd6f4);
|
||||
opacity: 0.6;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import './Footer.css';
|
||||
|
||||
function Footer({ version, status }) {
|
||||
return (
|
||||
<footer className="app-footer">
|
||||
<div className="app-status">
|
||||
{status}
|
||||
</div>
|
||||
<div className="app-version">
|
||||
v{version}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,100 @@
|
||||
/* Header Component Styles */
|
||||
.app-header {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background-color: transparent; /* Reverted to original */
|
||||
/* cursor: move; */ /* Removed, should be handled by drag region or default */
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
padding-right: 10px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Chrome-like tabs */
|
||||
.tab {
|
||||
-webkit-app-region: no-drag; /* Override drag region for clickable tabs */
|
||||
cursor: pointer; /* Ensure tabs have a pointer cursor */
|
||||
height: 36px;
|
||||
width: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
margin-right: 5px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
color: var(--text-color);
|
||||
background-color: transparent;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tab:hover:not(.active):not(.drag-handle) {
|
||||
background-color: var(--tab-bg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background-color: var(--tab-active);
|
||||
color: var(--accent-color);
|
||||
z-index: 1;
|
||||
height: 40px;
|
||||
margin-top: 0;
|
||||
box-shadow: 0 0 10px rgba(137, 180, 250, 0.3);
|
||||
}
|
||||
|
||||
/* Special drag handle tab */
|
||||
.tab.drag-handle {
|
||||
width: 30px;
|
||||
background-color: transparent;
|
||||
color: rgba(205, 214, 244, 0.5);
|
||||
}
|
||||
|
||||
.tab.drag-handle:hover {
|
||||
color: rgba(205, 214, 244, 0.8);
|
||||
}
|
||||
|
||||
/* Window control buttons */
|
||||
.window-controls {
|
||||
-webkit-app-region: no-drag; /* Override drag region for buttons */
|
||||
cursor: default; /* Keep default cursor for controls area */
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.minimize-button,
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.minimize-button:hover,
|
||||
.close-button:hover {
|
||||
background-color: var(--button-hover);
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #f38ba8;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import './Header.css';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faGripVertical,
|
||||
faBoxOpen,
|
||||
faMousePointer,
|
||||
faBoxes,
|
||||
faCog,
|
||||
faBoxArchive,
|
||||
faBox,
|
||||
faWarehouse,
|
||||
faGraduationCap,
|
||||
faCubesStacked,
|
||||
faBorderAll,
|
||||
faBagShopping
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
function Header({ activePage, setActivePage, minimizeToTray, quitApp, settings }) {
|
||||
// Navigation buttons for each page with icons
|
||||
const navItems = [
|
||||
{ id: 'inventory', icon: faBagShopping, tooltip: 'Inventory' },
|
||||
{ id: 'cursors', icon: faMousePointer, tooltip: 'Cursors' },
|
||||
{ id: 'crates', icon: faBoxOpen, tooltip: 'Crates' },
|
||||
{ id: 'settings', icon: faCog, tooltip: 'Settings' }
|
||||
];
|
||||
|
||||
// Handle close button click based on settings
|
||||
const handleCloseClick = () => {
|
||||
if (settings && settings.minimizeToTrayOnClose) {
|
||||
console.log('Close button clicked, minimizing to tray based on settings');
|
||||
minimizeToTray();
|
||||
} else {
|
||||
console.log('Close button clicked, quitting application based on settings');
|
||||
quitApp();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="app-header" data-tauri-drag-region>
|
||||
<div className="tabs-container">
|
||||
{navItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`tab ${activePage === item.id ? 'active' : ''}`}
|
||||
onClick={() => setActivePage(item.id)}
|
||||
title={item.tooltip}
|
||||
>
|
||||
<FontAwesomeIcon icon={item.icon} />
|
||||
</div>
|
||||
))}
|
||||
<div className="window-controls">
|
||||
<button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray">
|
||||
_
|
||||
</button>
|
||||
<button
|
||||
className="close-button"
|
||||
onClick={handleCloseClick}
|
||||
title={settings && settings.minimizeToTrayOnClose ? "Minimize to Tray" : "Quit Application"}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,351 @@
|
||||
/* Crates Module Styles */
|
||||
.crates-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Current crate section (top part) */
|
||||
.current-crate-section {
|
||||
height: 60%;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 2px solid rgba(49, 50, 68, 0.6);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.current-crate {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: rgba(30, 30, 46, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(49, 50, 68, 0.4);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Color accents for different crate types */
|
||||
.current-crate::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.current-crate.common::before { background-color: #a6adc8; }
|
||||
.current-crate.uncommon::before { background-color: #a6e3a1; }
|
||||
.current-crate.rare::before { background-color: #89b4fa; }
|
||||
.current-crate.epic::before { background-color: #cba6f7; }
|
||||
.current-crate.legendary::before {
|
||||
background: linear-gradient(90deg, #f9e2af, #fab387, #f9e2af);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -100px; }
|
||||
100% { background-position: 100px; }
|
||||
}
|
||||
|
||||
.crate-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
background-color: rgba(49, 50, 68, 0.6);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background-color: rgba(65, 66, 88, 0.8);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.nav-button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.crate-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.crate-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: rgba(24, 24, 37, 0.5);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid rgba(49, 50, 68, 0.6);
|
||||
}
|
||||
|
||||
.crate-icon {
|
||||
font-size: 48px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
/* Crate color styling */
|
||||
.current-crate.common .crate-icon { color: #a6adc8; text-shadow: 0 0 10px rgba(166, 173, 200, 0.4); }
|
||||
.current-crate.uncommon .crate-icon { color: #a6e3a1; text-shadow: 0 0 10px rgba(166, 227, 161, 0.4); }
|
||||
.current-crate.rare .crate-icon { color: #89b4fa; text-shadow: 0 0 10px rgba(137, 180, 250, 0.4); }
|
||||
.current-crate.epic .crate-icon { color: #cba6f7; text-shadow: 0 0 10px rgba(203, 166, 247, 0.4); }
|
||||
.current-crate.legendary .crate-icon { color: #f9e2af; text-shadow: 0 0 15px rgba(249, 226, 175, 0.6); animation: pulse 2s infinite; }
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Opening animation */
|
||||
.crate-image.opening {
|
||||
animation: shake 0.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% { transform: translateX(0); }
|
||||
25% { transform: translateX(-5px); }
|
||||
50% { transform: translateX(5px); }
|
||||
75% { transform: translateX(-5px); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.crate-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.crate-name {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.current-crate.common .crate-name { color: #a6adc8; }
|
||||
.current-crate.uncommon .crate-name { color: #a6e3a1; }
|
||||
.current-crate.rare .crate-name { color: #89b4fa; }
|
||||
.current-crate.epic .crate-name { color: #cba6f7; }
|
||||
.current-crate.legendary .crate-name { color: #f9e2af; }
|
||||
|
||||
.crate-description {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.crate-stats {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.crate-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.open-button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 150px;
|
||||
justify-content: center;
|
||||
background-color: rgba(49, 50, 68, 0.7);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.open-button:hover:not(:disabled) {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.open-button:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.open-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Button colors by rarity */
|
||||
.open-button.common:hover:not(:disabled) { background-color: #a6adc8; color: #1e1e2e; }
|
||||
.open-button.uncommon:hover:not(:disabled) { background-color: #a6e3a1; color: #1e1e2e; }
|
||||
.open-button.rare:hover:not(:disabled) { background-color: #89b4fa; color: #1e1e2e; }
|
||||
.open-button.epic:hover:not(:disabled) { background-color: #cba6f7; color: #1e1e2e; }
|
||||
.open-button.legendary:hover:not(:disabled) { background-color: #f9e2af; color: #1e1e2e; }
|
||||
|
||||
/* Crates inventory section (bottom part) */
|
||||
.crates-inventory-section {
|
||||
height: 40%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.inventory-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inventory-header h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.total-crates {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.crates-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 10px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.crate-item {
|
||||
background-color: rgba(49, 50, 68, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
position: relative;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.crate-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
/* Crate item colors */
|
||||
.crate-item.common::after { background-color: #a6adc8; }
|
||||
.crate-item.uncommon::after { background-color: #a6e3a1; }
|
||||
.crate-item.rare::after { background-color: #89b4fa; }
|
||||
.crate-item.epic::after { background-color: #cba6f7; }
|
||||
.crate-item.legendary::after {
|
||||
background: linear-gradient(90deg, #f9e2af, #fab387, #f9e2af);
|
||||
background-size: 200% auto;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.crate-item:hover:not(.empty) {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.crate-item.selected {
|
||||
border-color: var(--accent-color);
|
||||
background-color: rgba(49, 50, 68, 0.6);
|
||||
box-shadow: 0 0 10px rgba(137, 180, 250, 0.4);
|
||||
}
|
||||
|
||||
.crate-item.empty {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.crate-item-icon {
|
||||
font-size: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.crate-item.common .crate-item-icon { color: #a6adc8; }
|
||||
.crate-item.uncommon .crate-item-icon { color: #a6e3a1; }
|
||||
.crate-item.rare .crate-item-icon { color: #89b4fa; }
|
||||
.crate-item.epic .crate-item-icon { color: #cba6f7; }
|
||||
.crate-item.legendary .crate-item-icon { color: #f9e2af; }
|
||||
|
||||
.crate-count {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -10px;
|
||||
background-color: rgba(30, 30, 46, 0.9);
|
||||
border-radius: 50%;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
|
||||
.crate-item-name {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 500px) {
|
||||
.crate-display {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.crate-info {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useState } from 'react';
|
||||
import './Crates.css';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBox, faBoxOpen, faArrowRight, faArrowLeft, faUnlock } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
function Crates() {
|
||||
// Mock crate data - would come from backend
|
||||
const crateTypes = [
|
||||
{ id: 'common', name: 'Common Crate', count: 5, color: '#a6adc8', description: 'Contains common cursors' },
|
||||
{ id: 'uncommon', name: 'Uncommon Crate', count: 3, color: '#a6e3a1', description: 'Contains uncommon and common cursors' },
|
||||
{ id: 'rare', name: 'Rare Crate', count: 2, color: '#89b4fa', description: 'Contains rare and uncommon cursors' },
|
||||
{ id: 'epic', name: 'Epic Crate', count: 1, color: '#cba6f7', description: 'Contains epic, rare and uncommon cursors' },
|
||||
{ id: 'legendary', name: 'Legendary Crate', count: 0, color: '#f9e2af', description: 'Contains legendary and epic cursors' }
|
||||
];
|
||||
|
||||
const [selectedCrateIndex, setSelectedCrateIndex] = useState(0);
|
||||
const [isOpening, setIsOpening] = useState(false);
|
||||
const selectedCrate = crateTypes[selectedCrateIndex];
|
||||
|
||||
// Navigate through available crates
|
||||
const navigateCrate = (direction) => {
|
||||
// Find next available crate with count > 0
|
||||
let newIndex = selectedCrateIndex;
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < crateTypes.length) {
|
||||
newIndex = (newIndex + direction + crateTypes.length) % crateTypes.length;
|
||||
if (crateTypes[newIndex].count > 0) {
|
||||
break;
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (crateTypes[newIndex].count > 0) {
|
||||
setSelectedCrateIndex(newIndex);
|
||||
}
|
||||
};
|
||||
|
||||
// Simulate opening a crate
|
||||
const openCrate = () => {
|
||||
if (selectedCrate.count <= 0) return;
|
||||
|
||||
setIsOpening(true);
|
||||
// Simulate API call and animation time
|
||||
setTimeout(() => {
|
||||
setIsOpening(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="crates-container">
|
||||
{/* Current selected crate section */}
|
||||
<div className="current-crate-section">
|
||||
<div className={`current-crate ${selectedCrate.id}`}>
|
||||
<div className="crate-navigation">
|
||||
<button
|
||||
className="nav-button"
|
||||
onClick={() => navigateCrate(-1)}
|
||||
disabled={crateTypes.filter(c => c.count > 0).length <= 1}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowLeft} />
|
||||
</button>
|
||||
|
||||
<div className="crate-display">
|
||||
<div className={`crate-image ${isOpening ? 'opening' : ''}`}>
|
||||
<FontAwesomeIcon
|
||||
icon={isOpening ? faBoxOpen : faBox}
|
||||
className="crate-icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="crate-info">
|
||||
<h2 className="crate-name">{selectedCrate.name}</h2>
|
||||
<div className="crate-stats">
|
||||
<span>You have: <strong>{selectedCrate.count}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="nav-button"
|
||||
onClick={() => navigateCrate(1)}
|
||||
disabled={crateTypes.filter(c => c.count > 0).length <= 1}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="crate-actions">
|
||||
<button
|
||||
className={`open-button ${selectedCrate.id}`}
|
||||
onClick={openCrate}
|
||||
disabled={selectedCrate.count === 0 || isOpening}
|
||||
>
|
||||
{isOpening ? 'Opening...' : (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faUnlock} /> Open Crate
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Crates inventory section */}
|
||||
<div className="crates-inventory-section">
|
||||
<div className="inventory-header">
|
||||
<h3>Your Crates</h3>
|
||||
<span className="total-crates">
|
||||
{crateTypes.reduce((total, crate) => total + crate.count, 0)} total
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="crates-grid">
|
||||
{crateTypes.map((crate, index) => (
|
||||
<div
|
||||
key={crate.id}
|
||||
className={`crate-item ${crate.id} ${index === selectedCrateIndex ? 'selected' : ''} ${crate.count === 0 ? 'empty' : ''}`}
|
||||
onClick={() => crate.count > 0 && setSelectedCrateIndex(index)}
|
||||
>
|
||||
<div className="crate-item-icon">
|
||||
<FontAwesomeIcon icon={faBox} />
|
||||
{crate.count > 1 && <span className="crate-count">{crate.count}</span>}
|
||||
</div>
|
||||
<div className="crate-item-name">{crate.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Crates;
|
||||
@@ -0,0 +1,358 @@
|
||||
/* Cursors Module Styles */
|
||||
.cursors-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Equipped cursor banner */
|
||||
.equipped-cursor-banner {
|
||||
height: 25%;
|
||||
min-height: 120px;
|
||||
max-height: 150px;
|
||||
display: flex;
|
||||
background-color: rgba(30, 30, 46, 0.3);
|
||||
border-bottom: 2px solid rgba(49, 50, 68, 0.6);
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.current-preview {
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.3));
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.current-preview.legendary {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
filter: drop-shadow(0 0 8px rgba(249, 226, 175, 0.4));
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-5px); }
|
||||
100% { transform: translateY(0px); }
|
||||
}
|
||||
|
||||
.equipped-cursor-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.equipped-cursor-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.equipped-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: rgba(205, 214, 244, 0.6);
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.equipped-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.equipped-rarity {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.next-button {
|
||||
align-self: flex-start;
|
||||
background-color: rgba(65, 66, 88, 0.8);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 15px;
|
||||
color: var(--text-color);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.next-button:hover {
|
||||
background-color: var(--accent-color);
|
||||
color: #1e1e2e;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Library controls section */
|
||||
.cursors-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 5px 10px 15px 10px;
|
||||
border-bottom: 1px solid rgba(49, 50, 68, 0.4);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: rgba(205, 214, 244, 0.5);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
background-color: rgba(49, 50, 68, 0.3);
|
||||
border: 1px solid rgba(49, 50, 68, 0.6);
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
padding: 8px 10px 8px 32px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 5px rgba(137, 180, 250, 0.3);
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
background-color: rgba(49, 50, 68, 0.4);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.filter-button:hover {
|
||||
background-color: rgba(65, 66, 88, 0.6);
|
||||
}
|
||||
|
||||
.filter-button.active {
|
||||
background-color: rgba(65, 66, 88, 0.8);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* Rarity-based styling for filter buttons */
|
||||
.filter-button.common {
|
||||
border-left-color: #a6adc8;
|
||||
}
|
||||
|
||||
.filter-button.uncommon {
|
||||
border-left-color: #a6e3a1;
|
||||
}
|
||||
|
||||
.filter-button.rare {
|
||||
border-left-color: #89b4fa;
|
||||
}
|
||||
|
||||
.filter-button.epic {
|
||||
border-left-color: #cba6f7;
|
||||
}
|
||||
|
||||
.filter-button.legendary {
|
||||
border-left-color: #f9e2af;
|
||||
}
|
||||
|
||||
/* Main cursor library */
|
||||
.cursor-library {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: 0 10px;
|
||||
scrollbar-width: thin;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.rarity-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.rarity-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.rarity-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.rarity-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Rarity colors */
|
||||
.equipped-name.common, .equipped-rarity.common, .rarity-line.common { background-color: #a6adc8; }
|
||||
.equipped-name.uncommon, .equipped-rarity.uncommon, .rarity-line.uncommon { background-color: #a6e3a1; }
|
||||
.equipped-name.rare, .equipped-rarity.rare, .rarity-line.rare { background-color: #89b4fa; }
|
||||
.equipped-name.epic, .equipped-rarity.epic, .rarity-line.epic { background-color: #cba6f7; }
|
||||
.equipped-name.legendary, .equipped-rarity.legendary, .rarity-line.legendary { background-color: #f9e2af; }
|
||||
|
||||
/* Horizontal scrolling cursor list */
|
||||
.cursor-list {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
padding: 5px 0 15px 0;
|
||||
scrollbar-width: thin;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.cursor-card {
|
||||
min-width: 150px;
|
||||
max-width: 180px;
|
||||
background-color: rgba(49, 50, 68, 0.4);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(49, 50, 68, 0.4);
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Circular rarity indicator in top right */
|
||||
.cursor-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
z-index: 5;
|
||||
background-color: currentColor;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Rarity colors */
|
||||
.cursor-card.common { color: #a6adc8; }
|
||||
.cursor-card.uncommon { color: #a6e3a1; }
|
||||
.cursor-card.rare { color: #89b4fa; }
|
||||
.cursor-card.epic { color: #cba6f7; }
|
||||
.cursor-card.legendary { color: #f9e2af; }
|
||||
|
||||
/* Hover and active states */
|
||||
.cursor-card:hover {
|
||||
transform: translateY(-3px) scale(1.02);
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
|
||||
border-color: currentColor;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.cursor-card:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
}
|
||||
|
||||
.cursor-card.active {
|
||||
border-color: currentColor;
|
||||
box-shadow: 0 0 12px currentColor;
|
||||
}
|
||||
|
||||
/* Preview container */
|
||||
.cursor-preview-container {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(30, 30, 46, 0.5);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.cursor-preview-img {
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
object-fit: contain;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.cursor-card:hover .cursor-preview-img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Cursor details */
|
||||
.cursor-details {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.cursor-name {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cursor-rarity-badge {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
text-transform: capitalize;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Animation for legendary items */
|
||||
.cursor-card.legendary::before {
|
||||
animation: legendaryGlow 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes legendaryGlow {
|
||||
0% { opacity: 0.4; }
|
||||
50% { opacity: 0.9; }
|
||||
100% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* Empty states */
|
||||
.no-cursors, .no-results {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(49, 50, 68, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import React, { useState } from 'react';
|
||||
import './Cursors.css';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSort, faFilter, faSortAmountDown, faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
function Cursors({ cursors, currentCursorIndex, selectCursor, nextCursor }) {
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const currentCursor = cursors.length > 0 && currentCursorIndex < cursors.length
|
||||
? cursors[currentCursorIndex]
|
||||
: null;
|
||||
|
||||
// This will eventually come from backend with proper data
|
||||
const getRarity = (index) => {
|
||||
const rarities = ['common', 'uncommon', 'rare', 'epic', 'legendary'];
|
||||
return rarities[index % rarities.length];
|
||||
};
|
||||
|
||||
// Group cursors by rarity
|
||||
const cursorsByRarity = cursors.reduce((acc, cursor, index) => {
|
||||
const rarity = getRarity(index);
|
||||
if (!acc[rarity]) acc[rarity] = [];
|
||||
acc[rarity].push({ ...cursor, index, rarity });
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Filter logic with search
|
||||
const filteredCursors = Object.entries(cursorsByRarity)
|
||||
.filter(([rarity, _]) => filter === 'all' || rarity === filter)
|
||||
.map(([rarity, cursorsOfRarity]) => [
|
||||
rarity,
|
||||
cursorsOfRarity.filter(c =>
|
||||
!searchTerm || c.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
])
|
||||
.filter(([_, cursors]) => cursors.length > 0);
|
||||
|
||||
return (
|
||||
<div className="cursors-container">
|
||||
{/* Current cursor equipped display */}
|
||||
{currentCursor && (
|
||||
<div className="equipped-cursor-banner">
|
||||
<div className="equipped-cursor-image">
|
||||
<img
|
||||
src={currentCursor.path}
|
||||
alt={currentCursor.name}
|
||||
className={`current-preview ${getRarity(currentCursorIndex)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="equipped-cursor-details">
|
||||
<div className="equipped-cursor-info">
|
||||
<div className="equipped-label">EQUIPPED</div>
|
||||
<div className={`equipped-name ${getRarity(currentCursorIndex)}`}>{currentCursor.name}</div>
|
||||
<div className={`equipped-rarity ${getRarity(currentCursorIndex)}`}>
|
||||
{getRarity(currentCursorIndex).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<button className="next-button" onClick={nextCursor}>Next Cursor</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Library controls */}
|
||||
<div className="cursors-controls">
|
||||
<div className="search-box">
|
||||
<FontAwesomeIcon icon={faSearch} className="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search cursors..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-controls">
|
||||
<button
|
||||
className={`filter-button ${filter === 'all' ? 'active' : ''}`}
|
||||
onClick={() => setFilter('all')}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{Object.keys(cursorsByRarity).map(rarity => (
|
||||
<button
|
||||
key={rarity}
|
||||
className={`filter-button ${filter === rarity ? 'active' : ''} ${rarity}`}
|
||||
onClick={() => setFilter(rarity)}
|
||||
>
|
||||
{rarity.charAt(0).toUpperCase() + rarity.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cursor library */}
|
||||
{cursors.length > 0 ? (
|
||||
<div className="cursor-library">
|
||||
{filteredCursors.map(([rarity, cursorsInRarity]) => (
|
||||
<div className="rarity-section" key={rarity}>
|
||||
<div className={`rarity-header ${rarity}`}>
|
||||
<h3 className="rarity-title">
|
||||
{rarity.toUpperCase()} ({cursorsInRarity.length})
|
||||
</h3>
|
||||
<div className={`rarity-line ${rarity}`}></div>
|
||||
</div>
|
||||
|
||||
<div className="cursor-list">
|
||||
{cursorsInRarity.map((cursor) => (
|
||||
<div
|
||||
key={cursor.index}
|
||||
className={`cursor-card ${cursor.index === currentCursorIndex ? 'active' : ''} ${cursor.rarity}`}
|
||||
onClick={() => selectCursor(cursor.index)}
|
||||
>
|
||||
<div className="cursor-preview-container">
|
||||
<img
|
||||
src={cursor.path}
|
||||
alt={cursor.name}
|
||||
className="cursor-preview-img"
|
||||
/>
|
||||
</div>
|
||||
<div className="cursor-details">
|
||||
<span className="cursor-name">{cursor.name}</span>
|
||||
<span className="cursor-rarity-badge">{cursor.rarity}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredCursors.length === 0 && (
|
||||
<div className="no-results">No cursors match your search</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-cursors">
|
||||
<p>No cursors found in assets directory</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Cursors;
|
||||
@@ -0,0 +1,359 @@
|
||||
/* Inventory Module Styles */
|
||||
.inventory-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Search and filter controls */
|
||||
.inventory-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
background-color: rgba(49, 50, 68, 0.4);
|
||||
border-radius: 8px;
|
||||
margin-top: 2%;
|
||||
border: 1px solid rgba(49, 50, 68, 0.8);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(49, 50, 68, 0.8);
|
||||
background-color: rgba(30, 30, 46, 0.7);
|
||||
color: #cdd6f4;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-bar input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.3);
|
||||
}
|
||||
|
||||
.search-bar input::placeholder {
|
||||
color: rgba(205, 214, 244, 0.5);
|
||||
}
|
||||
|
||||
.rarity-filter {
|
||||
width: 50px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rarity-filter select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(49, 50, 68, 0.8);
|
||||
background-color: rgba(30, 30, 46, 0.7);
|
||||
color: transparent;
|
||||
text-indent: -9999px;
|
||||
font-size: 14px;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 4px center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rarity-filter .rarity-circle {
|
||||
position: absolute;
|
||||
aspect-ratio: 1/1;
|
||||
width: 30%;
|
||||
margin-left: 8px;
|
||||
|
||||
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Rarity colors for circle indicators */
|
||||
.rarity-circle.all {
|
||||
background: linear-gradient(45deg, #a6adc8, #a6e3a1, #89b4fa, #cba6f7, #f9e2af);
|
||||
}
|
||||
.rarity-circle.common { background-color: #a6adc8; }
|
||||
.rarity-circle.uncommon { background-color: #a6e3a1; }
|
||||
.rarity-circle.rare { background-color: #89b4fa; }
|
||||
.rarity-circle.epic { background-color: #cba6f7; }
|
||||
.rarity-circle.legendary {
|
||||
background-color: #f9e2af;
|
||||
box-shadow: 0 0 5px #f9e2af;
|
||||
}
|
||||
|
||||
/* Style dropdown options to show text and circles */
|
||||
.rarity-filter select option {
|
||||
color: white;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
.rarity-filter .rarity-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rarity-filter select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.3);
|
||||
}
|
||||
|
||||
/* Equipped cursor display */
|
||||
.equipped-cursor {
|
||||
display: flex;
|
||||
background-color: rgba(49, 50, 68);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
min-height: 25%;
|
||||
max-height: 25%;
|
||||
border: 1px solid rgba(49, 50, 68, 0.8);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.equipped-cursor-image {
|
||||
aspect-ratio: 1/1;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(30, 30, 46, 0.5);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(49, 50, 68, 0.6);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.equipped-image {
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.equipped-cursor-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.equipped-cursor-info h3 {
|
||||
font-size: 14px;
|
||||
margin: 0 0 3px 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.equipped-cursor-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.equipped-cursor-rarity {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Minecraft-style inventory */
|
||||
.cute-inventory {
|
||||
min-height: 58%;
|
||||
max-height: 58%;
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: rgba(30, 30, 46, 0.3);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
margin-top: 2%;
|
||||
border: 1px solid rgba(49, 50, 68, 0.6);
|
||||
}
|
||||
|
||||
.inventory-grid {
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(auto-fill, minmax(9vh, 1fr));
|
||||
}
|
||||
|
||||
.inventory-slot {
|
||||
aspect-ratio: 1;
|
||||
background-color: rgba(49, 50, 68, 0.5);
|
||||
border: 2px solid rgba(30, 30, 46, 0.8);
|
||||
border-radius: 1vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0.5vh;
|
||||
position: relative;
|
||||
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Rarity indicator as small circle in top right */
|
||||
.inventory-slot.has-item {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
border-color: rgba(30, 30, 46, 0.8);
|
||||
}
|
||||
|
||||
/* Small circle rarity indicator in top right */
|
||||
.inventory-slot.has-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.inventory-slot.has-item.common::after {
|
||||
background-color: #a6adc8;
|
||||
}
|
||||
|
||||
.inventory-slot.has-item.uncommon::after {
|
||||
background-color: #a6e3a1;
|
||||
}
|
||||
|
||||
.inventory-slot.has-item.rare::after {
|
||||
background-color: #89b4fa;
|
||||
}
|
||||
|
||||
.inventory-slot.has-item.epic::after {
|
||||
background-color: #cba6f7;
|
||||
}
|
||||
|
||||
.inventory-slot.has-item.legendary::after {
|
||||
background-color: #f9e2af;
|
||||
box-shadow: 0 0 5px rgba(249, 226, 175, 0.8);
|
||||
animation: pulseDot 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulseDot {
|
||||
0% { transform: scale(1); box-shadow: 0 0 5px rgba(249, 226, 175, 0.5); }
|
||||
50% { transform: scale(1.2); box-shadow: 0 0 8px rgba(249, 226, 175, 0.8); }
|
||||
100% { transform: scale(1); box-shadow: 0 0 5px rgba(249, 226, 175, 0.5); }
|
||||
}
|
||||
|
||||
.inventory-slot.has-item:hover {
|
||||
transform: scale(1.08);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.inventory-slot.has-item:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Selected item overrides rarity glow */
|
||||
.inventory-slot.selected {
|
||||
border-color: var(--accent-color) !important;
|
||||
box-shadow: 0 0 12px var(--accent-color) !important;
|
||||
z-index: 10;
|
||||
animation: selectedPulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes selectedPulse {
|
||||
0% { box-shadow: 0 0 8px var(--accent-color) !important; }
|
||||
50% { box-shadow: 0 0 15px var(--accent-color) !important; }
|
||||
100% { box-shadow: 0 0 8px var(--accent-color) !important; }
|
||||
}
|
||||
|
||||
/* Shine effect when clicked */
|
||||
.inventory-slot.has-item:active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 70%);
|
||||
opacity: 0.6;
|
||||
transform: scale(0);
|
||||
animation: clickRipple 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes clickRipple {
|
||||
0% { transform: scale(0); opacity: 0.6; }
|
||||
100% { transform: scale(1); opacity: 0; }
|
||||
}
|
||||
|
||||
.inventory-item {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Tooltip that appears on hover */
|
||||
.inventory-tooltip {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgba(30, 30, 46, 0.95);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
min-width: 80px;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inventory-slot:hover .inventory-tooltip {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tooltip-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tooltip-rarity {
|
||||
font-size: 10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Rarity colors */
|
||||
.common, .common.equipped-cursor-name, .tooltip-name.common { color: #a6adc8; }
|
||||
.uncommon, .uncommon.equipped-cursor-name, .tooltip-name.uncommon { color: #a6e3a1; }
|
||||
.rare, .rare.equipped-cursor-name, .tooltip-name.rare { color: #89b4fa; }
|
||||
.epic, .epic.equipped-cursor-name, .tooltip-name.epic { color: #cba6f7; }
|
||||
.legendary, .legendary.equipped-cursor-name, .tooltip-name.legendary { color: #f9e2af; }
|
||||
|
||||
/* Legendary glow effect */
|
||||
.inventory-slot.selected .inventory-item.legendary {
|
||||
filter: drop-shadow(0 0 3px #f9e2af);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { filter: drop-shadow(0 0 2px rgba(249, 226, 175, 0.5)); }
|
||||
50% { filter: drop-shadow(0 0 4px rgba(249, 226, 175, 0.8)); }
|
||||
100% { filter: drop-shadow(0 0 2px rgba(249, 226, 175, 0.5)); }
|
||||
}
|
||||
|
||||
.inventory-footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.cursor-count {
|
||||
font-size: 12px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './Inventory.css';
|
||||
|
||||
function Inventory({ cursors, currentCursorIndex, selectCursor }) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedRarity, setSelectedRarity] = useState('all');
|
||||
const [filteredSlots, setFilteredSlots] = useState([]);
|
||||
// Minecraft inventory typically has 27 slots (9x3)
|
||||
const totalSlots = 36; // 9x4 grid
|
||||
|
||||
// This will eventually come from backend with proper data
|
||||
const cursorRarities = {
|
||||
common: 'Common',
|
||||
uncommon: 'Uncommon',
|
||||
rare: 'Rare',
|
||||
epic: 'Epic',
|
||||
legendary: 'Legendary'
|
||||
};
|
||||
|
||||
// Assign mock rarities to cursors for demonstration
|
||||
const getCursorRarity = (index) => {
|
||||
const rarities = Object.keys(cursorRarities);
|
||||
return rarities[index % rarities.length];
|
||||
};
|
||||
|
||||
// Get currently selected cursor
|
||||
const currentCursor = cursors.length > 0 && currentCursorIndex < cursors.length
|
||||
? cursors[currentCursorIndex]
|
||||
: null;
|
||||
|
||||
// Create array of slots for the inventory grid
|
||||
const inventorySlots = Array(totalSlots).fill(null).map((_, slotIndex) => {
|
||||
// Map cursor to slot if available
|
||||
const cursorIndex = slotIndex < cursors.length ? slotIndex : null;
|
||||
const cursor = cursorIndex !== null ? cursors[cursorIndex] : null;
|
||||
const rarity = cursor ? getCursorRarity(cursorIndex) : null;
|
||||
|
||||
return {
|
||||
slotIndex,
|
||||
cursor,
|
||||
cursorIndex,
|
||||
rarity
|
||||
};
|
||||
});
|
||||
|
||||
// Apply filters to inventory slots
|
||||
useEffect(() => {
|
||||
// First, filter the slots that match our criteria
|
||||
let filtered = inventorySlots.filter(slot => {
|
||||
// Filter by search query
|
||||
const matchesSearch = !searchQuery ||
|
||||
(slot.cursor && slot.cursor.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
|
||||
// Filter by rarity
|
||||
const matchesRarity = selectedRarity === 'all' ||
|
||||
(slot.rarity && slot.rarity === selectedRarity);
|
||||
|
||||
return matchesSearch && matchesRarity;
|
||||
});
|
||||
|
||||
// Create a new array for the filtered slots, prioritizing slots with items
|
||||
const filledSlots = filtered.filter(slot => slot.cursor);
|
||||
const emptySlots = Array(totalSlots - filledSlots.length).fill(null)
|
||||
.map((_, index) => ({
|
||||
slotIndex: filledSlots.length + index,
|
||||
cursor: null,
|
||||
cursorIndex: null,
|
||||
rarity: null
|
||||
}));
|
||||
|
||||
// Combine filled slots at the beginning and empty ones at the end
|
||||
const result = [...filledSlots, ...emptySlots];
|
||||
|
||||
setFilteredSlots(result);
|
||||
}, [searchQuery, selectedRarity, inventorySlots]);
|
||||
|
||||
return (
|
||||
<div className="inventory-container">
|
||||
{/* Search and Filter Controls */}
|
||||
|
||||
{/* Equipped cursor display */}
|
||||
{currentCursor && (
|
||||
<div className="equipped-cursor">
|
||||
<div className="equipped-cursor-image">
|
||||
<img
|
||||
src={currentCursor.path}
|
||||
alt={currentCursor.name}
|
||||
className="equipped-image"
|
||||
/>
|
||||
</div>
|
||||
<div className="equipped-cursor-info">
|
||||
<h3>Equipped</h3>
|
||||
<span className={`equipped-cursor-name ${getCursorRarity(currentCursorIndex)}`}>
|
||||
{currentCursor.name}
|
||||
</span>
|
||||
<span className="equipped-cursor-rarity">
|
||||
{cursorRarities[getCursorRarity(currentCursorIndex)]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="inventory-controls">
|
||||
<div className="search-bar">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search cursors..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="rarity-filter">
|
||||
<div className={`rarity-circle ${selectedRarity}`}></div>
|
||||
<select
|
||||
value={selectedRarity}
|
||||
onChange={(e) => setSelectedRarity(e.target.value)}
|
||||
aria-label="Filter by rarity"
|
||||
>
|
||||
<option value="all">All Rarities</option>
|
||||
{Object.keys(cursorRarities).map(rarity => (
|
||||
<option key={rarity} value={rarity}>{cursorRarities[rarity]}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/* Minecraft-like inventory grid */}
|
||||
<div className="cute-inventory">
|
||||
<div className="inventory-grid">
|
||||
{filteredSlots.map((slot) => (
|
||||
<div
|
||||
key={slot.slotIndex}
|
||||
className={`inventory-slot ${slot.cursor ? 'has-item' : ''} ${slot.cursorIndex === currentCursorIndex ? 'selected' : ''} ${slot.rarity || ''}`}
|
||||
onClick={() => slot.cursor && selectCursor(slot.cursorIndex)}
|
||||
>
|
||||
{slot.cursor && (
|
||||
<>
|
||||
<img
|
||||
src={slot.cursor.path}
|
||||
alt={slot.cursor.name}
|
||||
className={`inventory-item ${slot.rarity}`}
|
||||
/>
|
||||
<div className="inventory-tooltip">
|
||||
<span className={`tooltip-name ${slot.rarity}`}>{slot.cursor.name}</span>
|
||||
<span className="tooltip-rarity">{cursorRarities[slot.rarity]}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Inventory;
|
||||
@@ -0,0 +1,75 @@
|
||||
/* Settings Module Styles */
|
||||
.settings-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
background-color: var(--button-bg, #313244);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
color: var(--text-color, #cdd6f4);
|
||||
}
|
||||
|
||||
.settings-group h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--accent-color, #89b4fa);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setting-item label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.setting-item input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
background-color: var(--modal-bg, #181825);
|
||||
border: 1px solid var(--text-color, #cdd6f4);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.setting-item input[type="checkbox"]:checked {
|
||||
background-color: var(--accent-color, #89b4fa);
|
||||
border-color: var(--accent-color, #89b4fa);
|
||||
}
|
||||
|
||||
.setting-item input[type="checkbox"]:checked::after {
|
||||
content: '✓';
|
||||
font-size: 12px;
|
||||
color: var(--bg-color, #1e1e2e);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.settings-group p {
|
||||
margin: 5px 0;
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import './Settings.css';
|
||||
|
||||
function Settings({ settings, setSettings }) {
|
||||
// Handler for checkbox changes
|
||||
const handleSettingChange = (settingName) => {
|
||||
setSettings({
|
||||
...settings,
|
||||
[settingName]: !settings[settingName]
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-container">
|
||||
<h2 className="content-title">Settings</h2>
|
||||
<div className="settings-content">
|
||||
<div className="settings-group">
|
||||
<h3>Application Settings</h3>
|
||||
<div className="setting-item">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.startWithWindows}
|
||||
onChange={() => handleSettingChange('startWithWindows')}
|
||||
/>
|
||||
Start with Windows
|
||||
</label>
|
||||
</div>
|
||||
<div className="setting-item">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.minimizeToTrayOnClose}
|
||||
onChange={() => handleSettingChange('minimizeToTrayOnClose')}
|
||||
/>
|
||||
Minimize to tray on close
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h3>Cursor Settings</h3>
|
||||
<div className="setting-item">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.autoApplyCursorOnSelection}
|
||||
onChange={() => handleSettingChange('autoApplyCursorOnSelection')}
|
||||
/>
|
||||
Auto-apply cursor on selection
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h3>About</h3>
|
||||
<p>Cursor Rush v0.1.0</p>
|
||||
<p>A cross-platform application for customizing your cursor.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 407 B |
@@ -0,0 +1,68 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
@@ -1,111 +0,0 @@
|
||||
// Cursor management functionality
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
use ::image::open as image_open;
|
||||
use ::image::GenericImageView;
|
||||
|
||||
use crate::platform;
|
||||
|
||||
// Define a struct to hold cursor information
|
||||
#[derive(Clone)]
|
||||
pub struct CursorInfo {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub image: Option<::image::RgbaImage>,
|
||||
#[cfg(windows)]
|
||||
pub win_cursor: Option<crate::platform::SyncHCURSOR>,
|
||||
}
|
||||
|
||||
// Function to load all PNG files from the assets directory
|
||||
pub fn load_cursor_files() -> Vec<CursorInfo> {
|
||||
let mut cursors = Vec::new();
|
||||
|
||||
println!("Looking for cursor PNG files in assets directory...");
|
||||
|
||||
// Check if assets directory exists
|
||||
if !std::path::Path::new("assets").exists() {
|
||||
println!("Assets directory not found!");
|
||||
return cursors;
|
||||
}
|
||||
|
||||
for entry in WalkDir::new("assets").into_iter().filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
println!("Found file: {}", path.display());
|
||||
if path.is_file() && path.extension().map_or(false, |ext| ext == "png") {
|
||||
let name = path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
|
||||
println!("Adding cursor: {} at path {}", name, path.display());
|
||||
|
||||
// Try to load the image during initialization
|
||||
let image = match image_open(path) {
|
||||
Ok(img) => {
|
||||
println!("Successfully pre-loaded image for cursor: {}", name);
|
||||
Some(img.to_rgba8())
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to pre-load image for cursor {}: {}", name, e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
cursors.push(CursorInfo {
|
||||
name,
|
||||
path: path.to_path_buf(),
|
||||
image,
|
||||
#[cfg(windows)]
|
||||
win_cursor: None, // Windows cursor will be created on first use
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
println!("Found {} cursor PNG files", cursors.len());
|
||||
|
||||
// Sort cursors by name for consistent ordering
|
||||
cursors.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
cursors
|
||||
}
|
||||
|
||||
// Function to validate a cursor image
|
||||
pub fn validate_cursor_image(path: &Path) -> bool {
|
||||
// Load the image to validate it
|
||||
match image_open(path) {
|
||||
Ok(img) => {
|
||||
let (width, height) = img.dimensions();
|
||||
|
||||
// Ensure reasonable dimensions
|
||||
if width > 128 || height > 128 || width == 0 || height == 0 {
|
||||
eprintln!("Image dimensions not suitable for cursor: {}x{}", width, height);
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to open image {}: {}", path.display(), e);
|
||||
false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the cursor from CursorInfo
|
||||
pub fn apply_cursor_info(cursor_info: &mut CursorInfo) -> bool {
|
||||
// Validate the cursor image if not already cached
|
||||
if cursor_info.image.is_none() && !validate_cursor_image(&cursor_info.path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use platform-specific cursor image update with the cursor info
|
||||
if platform::update_cursor_info(cursor_info) {
|
||||
println!("Successfully updated cursor image: {}", cursor_info.name);
|
||||
return true;
|
||||
} else {
|
||||
eprintln!("Failed to update cursor image: {}", cursor_info.name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// Application messages
|
||||
|
||||
// Messages that can be sent in our application
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
NextCursor,
|
||||
SelectCursor(usize),
|
||||
Quit,
|
||||
TrayEvent, // Used as a no-op message
|
||||
CloseRequested(iced::window::Id), // Message for window close requests with the real window ID
|
||||
ShowWindow, // Message to show the window from tray
|
||||
WindowHidden, // Message indicating window was successfully hidden
|
||||
WindowShown, // Message indicating window was successfully shown
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Application core module
|
||||
// Contains the main application state and logic
|
||||
|
||||
mod state;
|
||||
mod messages;
|
||||
mod cursor;
|
||||
|
||||
pub use state::CursorRush;
|
||||
pub use messages::Message;
|
||||
pub use cursor::{CursorInfo, load_cursor_files, apply_cursor_info};
|
||||
pub use state::SharedState;
|
||||
|
||||
use iced::{Subscription, Task};
|
||||
use once_cell::sync::Lazy;
|
||||
use iced::futures::channel::mpsc;
|
||||
|
||||
// Global channel for tray events
|
||||
pub static TRAY_SENDER: Lazy<std::sync::Mutex<Option<mpsc::UnboundedSender<Message>>>> =
|
||||
Lazy::new(|| std::sync::Mutex::new(None));
|
||||
|
||||
// Boot function to initialize the application
|
||||
pub fn boot() -> (CursorRush, Task<Message>) {
|
||||
state::boot_app()
|
||||
}
|
||||
|
||||
// Update function to handle messages
|
||||
pub fn update(state: &mut CursorRush, message: Message) -> Task<Message> {
|
||||
state::update_app(state, message)
|
||||
}
|
||||
|
||||
// Top-level subscription function to listen for events
|
||||
pub fn subscription(_state: &CursorRush) -> Subscription<Message> {
|
||||
state::app_subscription(_state)
|
||||
}
|
||||
|
||||
// Helper function to hide a window to the tray
|
||||
pub fn hide_window_to_tray(window_id: iced::window::Id) -> Task<Message> {
|
||||
// Use ONLY the iced method for hiding. Platform specific might interfere.
|
||||
let hide_task: Task<()> = iced::window::change_mode(window_id, iced::window::Mode::Hidden);
|
||||
|
||||
hide_task.map(|_: ()| {
|
||||
println!("Window hide task completed (via iced)");
|
||||
// Return a specific message to confirm the window was hidden
|
||||
Message::WindowHidden
|
||||
})
|
||||
}
|
||||
@@ -1,534 +0,0 @@
|
||||
// Application state management
|
||||
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use ::image::{RgbaImage, open as image_open};
|
||||
use iced::{Task, Subscription, window, event};
|
||||
use iced::futures::channel::mpsc;
|
||||
use global_hotkey::{
|
||||
GlobalHotKeyManager,
|
||||
hotkey::HotKey as GlobalHotKey,
|
||||
HotKeyState,
|
||||
};
|
||||
|
||||
// Update function to handle messages
|
||||
pub fn update_app(state: &mut CursorRush, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::NextCursor => {
|
||||
if !state.cursors.is_empty() {
|
||||
state.current_index = (state.current_index + 1) % state.cursors.len();
|
||||
|
||||
// Apply the new cursor
|
||||
if let Some(cursor) = state.cursors.get_mut(state.current_index) {
|
||||
// Use the cached version with the cursor info
|
||||
apply_cursor_info(cursor);
|
||||
|
||||
// Update shared state
|
||||
if let Some(shared) = &state.shared_state {
|
||||
if let Ok(mut shared_state) = shared.lock() {
|
||||
shared_state.current_index = state.current_index;
|
||||
// Use the cached image directly
|
||||
shared_state.current_cursor_image = cursor.image.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
Message::SelectCursor(idx) => {
|
||||
if idx < state.cursors.len() {
|
||||
state.current_index = idx;
|
||||
|
||||
// Apply the selected cursor
|
||||
if let Some(cursor) = state.cursors.get_mut(state.current_index) {
|
||||
// Use the cached version with the cursor info
|
||||
apply_cursor_info(cursor);
|
||||
|
||||
// Update shared state
|
||||
if let Some(shared) = &state.shared_state {
|
||||
if let Ok(mut shared_state) = shared.lock() {
|
||||
shared_state.current_index = state.current_index;
|
||||
// Use the cached image directly
|
||||
shared_state.current_cursor_image = cursor.image.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
Message::Quit => {
|
||||
println!("Update: Handling Quit message");
|
||||
|
||||
// Set running flag to false to signal other threads (like hotkey listener)
|
||||
if let Some(shared) = &state.shared_state {
|
||||
if let Ok(mut shared_state) = shared.lock() {
|
||||
shared_state.running = false;
|
||||
println!("Set running flag to false in shared state");
|
||||
} else {
|
||||
println!("Failed to lock shared state to set running flag");
|
||||
}
|
||||
} else {
|
||||
println!("Shared state not available when handling Quit message");
|
||||
}
|
||||
|
||||
// Unregister hotkeys if possible
|
||||
if let Some(_) = &state.hotkey_manager {
|
||||
println!("Unregistering hotkeys before exit");
|
||||
// We don't have a list of registered hotkeys, so we'll just drop the manager
|
||||
// The hotkeys will be unregistered when the manager is dropped
|
||||
println!("Hotkey manager will be dropped during exit");
|
||||
} else {
|
||||
println!("No hotkey manager available to unregister hotkeys");
|
||||
}
|
||||
|
||||
// Restore the system cursor before exiting
|
||||
platform::restore_cursor();
|
||||
println!("System cursor restored");
|
||||
|
||||
// Cleanly drop the tray icon *before* exiting iced
|
||||
// This removes the icon from the system tray immediately
|
||||
if let Some(tray) = state.tray_icon.take() {
|
||||
println!("Dropping tray icon...");
|
||||
drop(tray);
|
||||
println!("Tray icon explicitly dropped.");
|
||||
} else {
|
||||
println!("No tray icon to drop");
|
||||
}
|
||||
|
||||
// Use iced's exit mechanism for a cleaner shutdown
|
||||
println!("Requesting application exit via iced::exit...");
|
||||
iced::exit()
|
||||
},
|
||||
Message::TrayEvent => {
|
||||
// Sync with shared state
|
||||
if let Some(shared) = &state.shared_state {
|
||||
if let Ok(shared_state) = shared.lock() {
|
||||
state.current_index = shared_state.current_index;
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
Message::CloseRequested(window_id) => {
|
||||
println!("Handling CloseRequested: Hiding window with ID: {:?}", window_id);
|
||||
|
||||
// Store the real window ID for future use
|
||||
// This is critical for showing the window later
|
||||
state.main_window = Some(window_id);
|
||||
println!("Stored window ID: {:?} for future use", window_id);
|
||||
|
||||
// Update shared state first
|
||||
if let Some(shared) = &state.shared_state {
|
||||
match shared.lock() {
|
||||
Ok(mut shared_state) => {
|
||||
if shared_state.window_visible { // Only hide if visible
|
||||
shared_state.window_visible = false;
|
||||
println!("Set window_visible to false in shared state");
|
||||
println!("Hiding window with ID: {:?}, app continues running in tray", window_id);
|
||||
return crate::app::hide_window_to_tray(window_id);
|
||||
} else {
|
||||
println!("Window already hidden, ignoring CloseRequested.");
|
||||
return Task::none(); // Don't try to hide again
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error locking shared state for hiding: {}", e);
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Error: Shared state not available for hiding.");
|
||||
return Task::none();
|
||||
}
|
||||
},
|
||||
Message::WindowHidden => {
|
||||
println!("Window successfully hidden to tray");
|
||||
Task::none()
|
||||
},
|
||||
Message::WindowShown => {
|
||||
println!("Update: Received WindowShown message. Window should now be visible.");
|
||||
Task::none()
|
||||
},
|
||||
Message::ShowWindow => {
|
||||
println!("Handling ShowWindow: Attempting to show window"); // More specific starting log
|
||||
|
||||
if let Some(window_id) = state.main_window {
|
||||
println!("Using stored window ID: {:?}", window_id);
|
||||
|
||||
// It's generally safer to attempt the show operation even if the state *thinks* it's visible.
|
||||
// Let's update the shared state *after* we know the task is being created.
|
||||
|
||||
println!("Creating show/focus tasks for window ID: {:?}", window_id);
|
||||
let show_task = iced::window::change_mode(window_id, iced::window::Mode::Windowed);
|
||||
let focus_task = iced::window::gain_focus(window_id);
|
||||
|
||||
// Update shared state *before* returning the task, but after creating it.
|
||||
if let Some(shared) = &state.shared_state {
|
||||
match shared.lock() {
|
||||
Ok(mut shared_state) => {
|
||||
if !shared_state.window_visible { // Only log if changing state
|
||||
println!("Updating shared state: window_visible = true");
|
||||
shared_state.window_visible = true;
|
||||
} else {
|
||||
println!("Shared state already indicates window is visible.");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error locking shared state during show: {}", e);
|
||||
// Proceed with showing the window anyway, state might be out of sync
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Shared state unavailable during show.");
|
||||
// Proceed with showing the window anyway
|
||||
}
|
||||
|
||||
println!("Returning Task::batch to show and focus window.");
|
||||
// Return the batch task
|
||||
return Task::batch(vec![show_task, focus_task])
|
||||
.map(|_: ()| {
|
||||
// This message confirms the Iced runtime *completed* the tasks.
|
||||
println!("Iced runtime reports show/focus tasks completed successfully.");
|
||||
Message::WindowShown // Send confirmation message
|
||||
});
|
||||
|
||||
} else {
|
||||
eprintln!("Error: Main window ID not found when trying to show. Cannot create tasks.");
|
||||
Task::none() // Cannot proceed without window_id
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
use crate::platform;
|
||||
use crate::ui::tray::setup_tray_icon;
|
||||
use crate::app::{Message, CursorInfo, load_cursor_files, apply_cursor_info, TRAY_SENDER};
|
||||
|
||||
// Shared state between the UI and the tray icon
|
||||
pub struct SharedState {
|
||||
pub current_index: usize,
|
||||
pub cursors: Vec<CursorInfo>,
|
||||
pub current_cursor_image: Option<RgbaImage>,
|
||||
pub running: bool,
|
||||
pub window_visible: bool, // Track if the window is visible
|
||||
}
|
||||
|
||||
// Our application state
|
||||
pub struct CursorRush {
|
||||
pub cursors: Vec<CursorInfo>,
|
||||
pub current_index: usize,
|
||||
pub shared_state: Option<Arc<Mutex<SharedState>>>,
|
||||
pub main_window: Option<iced::window::Id>, // Store the main window ID
|
||||
pub tray_icon: Option<tray_icon::TrayIcon>, // Store the TrayIcon object to keep it alive
|
||||
pub hotkey_manager: Option<GlobalHotKeyManager>, // Store the hotkey manager to keep it alive
|
||||
}
|
||||
|
||||
// Make sure the default impl initializes tray_icon and hotkey_manager to None
|
||||
impl Default for CursorRush {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cursors: Vec::new(),
|
||||
current_index: 0,
|
||||
shared_state: None,
|
||||
main_window: None,
|
||||
tray_icon: None, // Initialize as None
|
||||
hotkey_manager: None, // Initialize as None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Top-level subscription function to listen for events
|
||||
pub fn app_subscription(_state: &CursorRush) -> Subscription<Message> {
|
||||
// Create a vector to hold all subscriptions
|
||||
let mut subs = vec![
|
||||
// Listen for events
|
||||
event::listen().map(|event| {
|
||||
match event {
|
||||
// We no longer need to handle cursor movement events since we're using SetSystemCursor
|
||||
iced::Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => {
|
||||
// Return TrayEvent which is essentially a no-op
|
||||
Message::TrayEvent
|
||||
},
|
||||
// Add a fallback keyboard handler for Ctrl+Shift+F12
|
||||
iced::Event::Keyboard(iced::keyboard::Event::KeyPressed {
|
||||
key,
|
||||
modifiers,
|
||||
..
|
||||
}) => {
|
||||
// Check if Ctrl+Shift+F12 is pressed
|
||||
if key == iced::keyboard::Key::Named(iced::keyboard::key::Named::F12) &&
|
||||
modifiers.control() &&
|
||||
modifiers.shift() {
|
||||
println!("Detected Ctrl+Shift+F12 via iced keyboard event");
|
||||
Message::Quit
|
||||
} else {
|
||||
Message::TrayEvent // No-op for other key events
|
||||
}
|
||||
},
|
||||
// Handle other events
|
||||
_ => Message::TrayEvent, // Use TrayEvent as a no-op message
|
||||
}
|
||||
}),
|
||||
|
||||
// Specifically subscribe to window close requests
|
||||
// This is the key to handling the X button properly
|
||||
window::close_requests().map(|window_id| {
|
||||
println!("Window close requested via X button for window ID: {:?}", window_id);
|
||||
Message::CloseRequested(window_id)
|
||||
}),
|
||||
];
|
||||
|
||||
// Add a subscription for tray events
|
||||
// We'll use a simple channel that the tray icon can send messages to
|
||||
let tray_subscription = Subscription::run(
|
||||
// Stream generator function
|
||||
|| {
|
||||
// Create a channel that the tray icon can send messages to
|
||||
let (tx, rx) = mpsc::unbounded::<Message>();
|
||||
|
||||
// Store the sender in the global state
|
||||
*TRAY_SENDER.lock().unwrap() = Some(tx);
|
||||
|
||||
// Return the receiver as a stream
|
||||
rx
|
||||
}
|
||||
);
|
||||
|
||||
subs.push(tray_subscription);
|
||||
|
||||
// Combine all subscriptions
|
||||
Subscription::batch(subs)
|
||||
}
|
||||
|
||||
// Boot function implementation
|
||||
pub fn boot_app() -> (CursorRush, Task<Message>) {
|
||||
// Load cursor files from assets directory
|
||||
let mut cursors = load_cursor_files();
|
||||
|
||||
if cursors.is_empty() {
|
||||
eprintln!("No cursor PNG files found in assets directory!");
|
||||
return (CursorRush::default(), Task::none());
|
||||
}
|
||||
|
||||
// Load the first cursor image
|
||||
let current_cursor_image = if let Some(cursor) = cursors.first() {
|
||||
match image_open(&cursor.path) {
|
||||
Ok(img) => Some(img.to_rgba8()),
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Hide the system cursor once at startup
|
||||
if !platform::hide_system_cursor() {
|
||||
eprintln!("Failed to hide system cursor at startup!");
|
||||
}
|
||||
|
||||
// Apply the first cursor by default
|
||||
if let Some(cursor) = cursors.first_mut() {
|
||||
apply_cursor_info(cursor);
|
||||
}
|
||||
|
||||
// Create shared state
|
||||
let shared_state = Arc::new(Mutex::new(SharedState {
|
||||
current_index: 0,
|
||||
cursors: cursors.clone(),
|
||||
current_cursor_image,
|
||||
running: true,
|
||||
window_visible: true, // Window is initially visible
|
||||
}));
|
||||
|
||||
// Initialize the application state with loaded cursors
|
||||
let mut state = CursorRush::default();
|
||||
state.cursors = cursors.clone(); // Clone here
|
||||
state.shared_state = Some(shared_state.clone());
|
||||
|
||||
// Set up the tray icon AND store the result in the state
|
||||
state.tray_icon = setup_tray_icon(&state.cursors, shared_state.clone()); // Pass the cloned cursors
|
||||
if state.tray_icon.is_none() {
|
||||
eprintln!("Failed to initialize tray icon, application might not function correctly in background.");
|
||||
}
|
||||
|
||||
// Set up hotkeys and store the manager in the application state
|
||||
state.hotkey_manager = setup_hotkeys(shared_state.clone());
|
||||
if state.hotkey_manager.is_none() {
|
||||
eprintln!("Failed to initialize hotkey manager, hotkeys will not work!");
|
||||
} else {
|
||||
println!("Hotkey manager successfully initialized and stored in application state");
|
||||
}
|
||||
|
||||
(state, Task::none())
|
||||
}
|
||||
|
||||
// Set up global hotkeys
|
||||
fn setup_hotkeys(shared_state: Arc<Mutex<SharedState>>) -> Option<GlobalHotKeyManager> {
|
||||
// Set up global hotkey manager
|
||||
let hotkey_manager = match GlobalHotKeyManager::new() {
|
||||
Ok(manager) => {
|
||||
println!("Successfully created hotkey manager");
|
||||
manager
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create hotkey manager: {}", e);
|
||||
// Continue without hotkey support
|
||||
match GlobalHotKeyManager::new() {
|
||||
Ok(manager) => {
|
||||
println!("Second attempt to create hotkey manager succeeded");
|
||||
manager
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Critical failure: could not create hotkey manager on second attempt: {}", e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Register Ctrl+Shift+F12 as the exit hotkey (uncommon but easy to remember)
|
||||
use global_hotkey::hotkey::{Code, Modifiers};
|
||||
|
||||
let hotkey = GlobalHotKey::new(
|
||||
Some(Modifiers::CONTROL | Modifiers::SHIFT),
|
||||
Code::F12,
|
||||
);
|
||||
|
||||
println!("Attempting to register Ctrl+Shift+F12 as exit hotkey");
|
||||
|
||||
// Register the hotkey with better error handling
|
||||
let registered_hotkey = match hotkey_manager.register(hotkey) {
|
||||
Ok(_) => {
|
||||
println!("Successfully registered Ctrl+Shift+F12 as exit hotkey");
|
||||
hotkey
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to register hotkey: {}", e);
|
||||
// Try registering with a different hotkey as fallback
|
||||
let fallback_hotkey = GlobalHotKey::new(
|
||||
Some(Modifiers::CONTROL | Modifiers::ALT),
|
||||
Code::F12,
|
||||
);
|
||||
|
||||
println!("Attempting to register fallback hotkey Ctrl+Alt+F12");
|
||||
match hotkey_manager.register(fallback_hotkey) {
|
||||
Ok(_) => {
|
||||
println!("Registered fallback hotkey Ctrl+Alt+F12 instead");
|
||||
fallback_hotkey
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to register fallback hotkey: {}", e);
|
||||
|
||||
// Try one more fallback with just F12
|
||||
let last_fallback = GlobalHotKey::new(None, Code::F12);
|
||||
println!("Attempting to register last fallback hotkey F12");
|
||||
match hotkey_manager.register(last_fallback) {
|
||||
Ok(_) => {
|
||||
println!("Registered last fallback hotkey F12");
|
||||
last_fallback
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to register any hotkey: {}", e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
println!("Hotkey registered: {:?}", registered_hotkey);
|
||||
println!("Press Ctrl+Shift+F12 to exit the application");
|
||||
|
||||
// We can't clone the hotkey manager, so we'll use Arc to share it
|
||||
let manager_arc = Arc::new(hotkey_manager);
|
||||
let manager_for_thread = Arc::clone(&manager_arc);
|
||||
|
||||
// Spawn a thread to listen for hotkeys
|
||||
let state_clone = shared_state.clone();
|
||||
thread::spawn(move || {
|
||||
println!("Hotkey listener thread started");
|
||||
let event_receiver = global_hotkey::GlobalHotKeyEvent::receiver();
|
||||
|
||||
// Keep a reference to the manager in this thread to ensure it stays alive
|
||||
let _manager_ref = &manager_for_thread;
|
||||
println!("Hotkey manager reference maintained in thread");
|
||||
|
||||
// Create a channel to signal thread termination
|
||||
let (exit_tx, exit_rx) = std::sync::mpsc::channel();
|
||||
|
||||
// Spawn a thread to monitor the running flag
|
||||
let monitor_state = state_clone.clone();
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
// Check if we should exit
|
||||
if let Ok(state) = monitor_state.lock() {
|
||||
if !state.running {
|
||||
println!("Monitor thread detected running=false, signaling hotkey thread to exit");
|
||||
let _ = exit_tx.send(());
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Sleep briefly to avoid high CPU usage
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
});
|
||||
|
||||
// Main event loop - efficiently wait for events using blocking recv()
|
||||
loop {
|
||||
// Use std::sync::mpsc::TryRecv to check if we should exit without blocking
|
||||
if exit_rx.try_recv().is_ok() {
|
||||
println!("Hotkey listener received exit signal, terminating thread");
|
||||
break;
|
||||
}
|
||||
|
||||
// Use blocking recv() with a timeout to efficiently wait for events
|
||||
// This avoids busy-waiting and reduces CPU usage to near 0% when idle
|
||||
match event_receiver.recv_timeout(Duration::from_millis(500)) {
|
||||
Ok(event) => {
|
||||
println!("Received hotkey event: {:?}", event);
|
||||
if event.state == HotKeyState::Pressed {
|
||||
println!("Exit hotkey pressed, sending Quit message...");
|
||||
// Send Quit message instead of direct exit
|
||||
// Get the global sender
|
||||
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
|
||||
println!("Found global sender, sending Quit message");
|
||||
if let Err(e) = tx.unbounded_send(Message::Quit) {
|
||||
eprintln!("Failed to send Quit message from hotkey thread: {}", e);
|
||||
// Fallback to direct exit if channel fails
|
||||
println!("Using fallback exit mechanism");
|
||||
platform::restore_cursor();
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
println!("Successfully sent Quit message through channel");
|
||||
}
|
||||
} else {
|
||||
eprintln!("Hotkey: Global sender not initialized yet");
|
||||
// Fallback to direct exit if sender not available
|
||||
println!("Global sender not available, using direct exit");
|
||||
platform::restore_cursor();
|
||||
std::process::exit(0);
|
||||
}
|
||||
// Don't exit directly here, let the main thread handle it.
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
// Timeout or channel disconnected, just continue the loop
|
||||
// No need to sleep as recv_timeout already waited
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return the hotkey manager from the Arc so it can be stored in the application state
|
||||
match Arc::try_unwrap(manager_arc) {
|
||||
Ok(manager) => Some(manager),
|
||||
Err(_arc) => {
|
||||
// If we can't unwrap the Arc (which shouldn't happen), extract a clone from it
|
||||
println!("Warning: Could not unwrap Arc<GlobalHotKeyManager>, using a new instance");
|
||||
// Create a new instance as a fallback
|
||||
GlobalHotKeyManager::new().ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Steam integration
|
||||
|
||||
use steamworks::{
|
||||
sys::{SteamAPI_ISteamInventory_TriggerItemDrop, SteamAPI_SteamInventory_v003},
|
||||
AppId, Client, SingleClient,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Steam {
|
||||
client: Client,
|
||||
single: SingleClient,
|
||||
}
|
||||
|
||||
impl Steam {
|
||||
pub fn new() -> Self {
|
||||
let (client, single) = Client::init_app(AppId(1430270)).unwrap();
|
||||
Self { client, single }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn drop_item(&self) {
|
||||
unsafe {
|
||||
let mut r = 0;
|
||||
SteamAPI_ISteamInventory_TriggerItemDrop(SteamAPI_SteamInventory_v003(), &mut r, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Main entry point for the Cursor Rush application
|
||||
|
||||
mod app;
|
||||
mod ui;
|
||||
mod platform;
|
||||
mod integrations;
|
||||
|
||||
fn main() -> iced::Result {
|
||||
// Initialize Steam
|
||||
let _steam = integrations::steam::Steam::new();
|
||||
|
||||
// Run the application
|
||||
iced::application("Cursor Rush", app::update, ui::view_app)
|
||||
.theme(|_| iced::Theme::Dark)
|
||||
.subscription(app::subscription)
|
||||
.centered()
|
||||
// This is the key - we need to tell iced not to exit when the window close button is clicked
|
||||
.exit_on_close_request(false)
|
||||
// run_with expects a function that returns (State, Task), not the tuple directly
|
||||
.run_with(app::boot)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Linux cursor management
|
||||
|
||||
use std::path::Path;
|
||||
use crate::platform::CursorInfo;
|
||||
|
||||
// Hide the system cursor
|
||||
pub fn hide_system_cursor() -> bool {
|
||||
println!("Linux system cursor hiding not yet implemented");
|
||||
false
|
||||
}
|
||||
|
||||
// Update the cursor image using CursorInfo (with caching)
|
||||
pub fn update_cursor_info(_cursor_info: &mut CursorInfo) -> bool {
|
||||
println!("Linux cursor image update with caching not yet implemented");
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Restore the original cursor
|
||||
pub fn restore_cursor() {
|
||||
println!("Linux cursor restoration not yet implemented");
|
||||
}
|
||||
|
||||
// Render the cursor
|
||||
pub fn render_cursor() {
|
||||
// No-op
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// Linux platform implementation module
|
||||
|
||||
mod cursor;
|
||||
mod state;
|
||||
|
||||
// Re-export the public API
|
||||
pub use cursor::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||
@@ -1,4 +0,0 @@
|
||||
// Linux platform state management
|
||||
|
||||
// This file will contain Linux-specific state management
|
||||
// Currently a placeholder for future implementation
|
||||
@@ -1,28 +0,0 @@
|
||||
// macOS cursor management
|
||||
|
||||
use std::path::Path;
|
||||
use crate::platform::CursorInfo;
|
||||
|
||||
// Hide the system cursor
|
||||
pub fn hide_system_cursor() -> bool {
|
||||
println!("macOS system cursor hiding not yet implemented");
|
||||
false
|
||||
}
|
||||
|
||||
// Update the cursor image using CursorInfo (with caching)
|
||||
pub fn update_cursor_info(_cursor_info: &mut CursorInfo) -> bool {
|
||||
println!("macOS cursor image update with caching not yet implemented");
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Restore the original cursor
|
||||
pub fn restore_cursor() {
|
||||
println!("macOS cursor restoration not yet implemented");
|
||||
}
|
||||
|
||||
// Render the cursor
|
||||
pub fn render_cursor() {
|
||||
// No-op
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// macOS platform implementation module
|
||||
|
||||
mod cursor;
|
||||
mod state;
|
||||
|
||||
// Re-export the public API
|
||||
pub use cursor::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||
@@ -1,4 +0,0 @@
|
||||
// macOS platform state management
|
||||
|
||||
// This file will contain macOS-specific state management
|
||||
// Currently a placeholder for future implementation
|
||||
@@ -1,52 +0,0 @@
|
||||
// Platform-specific cursor handling
|
||||
|
||||
// Windows implementation
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
// macOS implementation
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
// Linux implementation
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
|
||||
// Unsupported platforms
|
||||
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
|
||||
mod unsupported;
|
||||
|
||||
// --- Platform-independent types ---
|
||||
|
||||
// Define a cross-platform cursor handle type
|
||||
#[cfg(windows)]
|
||||
pub use windows::types::SyncHCURSOR;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct SyncHCURSOR(pub usize);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
unsafe impl Send for SyncHCURSOR {}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
unsafe impl Sync for SyncHCURSOR {}
|
||||
|
||||
// --- Public Exports ---
|
||||
|
||||
// Export the platform-specific functions
|
||||
#[cfg(windows)]
|
||||
pub use windows::{hide_system_cursor, update_cursor_info, restore_cursor};
|
||||
|
||||
// Re-export CursorInfo for platform-specific implementations
|
||||
pub use crate::app::CursorInfo;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||
|
||||
// Default implementation for other platforms
|
||||
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
|
||||
pub use unsupported::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||
@@ -1,28 +0,0 @@
|
||||
// Unsupported platform cursor management
|
||||
|
||||
use std::path::Path;
|
||||
use crate::platform::CursorInfo;
|
||||
|
||||
// Hide the system cursor
|
||||
pub fn hide_system_cursor() -> bool {
|
||||
println!("System cursor hiding not supported");
|
||||
false
|
||||
}
|
||||
|
||||
// Update the cursor image using CursorInfo (with caching)
|
||||
pub fn update_cursor_info(_cursor_info: &mut CursorInfo) -> bool {
|
||||
println!("Cursor image update with caching not supported");
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Restore the original cursor
|
||||
pub fn restore_cursor() {
|
||||
println!("Cursor restoration not supported");
|
||||
}
|
||||
|
||||
// Render the cursor
|
||||
pub fn render_cursor() {
|
||||
// No-op
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// Unsupported platform implementation module
|
||||
|
||||
mod cursor;
|
||||
mod state;
|
||||
|
||||
// Re-export the public API
|
||||
pub use cursor::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||
@@ -1,4 +0,0 @@
|
||||
// Unsupported platform state management
|
||||
|
||||
// This file will contain fallback state management for unsupported platforms
|
||||
// Currently a placeholder for future implementation
|
||||
@@ -1,8 +0,0 @@
|
||||
// Windows platform implementation module
|
||||
|
||||
pub mod types;
|
||||
mod cursor;
|
||||
mod state;
|
||||
|
||||
// Re-export the public API
|
||||
pub use cursor::{hide_system_cursor, update_cursor_info, restore_cursor};
|
||||
@@ -1,7 +0,0 @@
|
||||
// UI module
|
||||
// Contains UI components and rendering logic
|
||||
|
||||
pub mod view;
|
||||
pub mod tray;
|
||||
|
||||
pub use view::view_app;
|
||||
@@ -1,195 +0,0 @@
|
||||
// System tray functionality
|
||||
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
use ::image::open as image_open;
|
||||
use tray_icon::{
|
||||
TrayIconBuilder,
|
||||
TrayIcon,
|
||||
TrayIconEvent,
|
||||
MouseButton,
|
||||
menu::{Menu, MenuEvent},
|
||||
};
|
||||
|
||||
use crate::app::{Message, CursorInfo, TRAY_SENDER, SharedState};
|
||||
|
||||
// Define menu item IDs
|
||||
const NEXT_CURSOR_ID: &str = "next_cursor";
|
||||
const SHOW_WINDOW_ID: &str = "show_window";
|
||||
const QUIT_ID: &str = "quit";
|
||||
|
||||
// Set up the system tray icon
|
||||
pub fn setup_tray_icon(
|
||||
cursors: &[CursorInfo],
|
||||
shared_state: Arc<Mutex<SharedState>>,
|
||||
) -> Option<TrayIcon> { // Return Option<TrayIcon>
|
||||
if cursors.is_empty() {
|
||||
return None; // Return None if no cursors
|
||||
}
|
||||
|
||||
// Create a menu
|
||||
let menu = Menu::with_items(&[
|
||||
&tray_icon::menu::MenuItem::with_id(NEXT_CURSOR_ID, "Next cursor", true, None),
|
||||
&tray_icon::menu::MenuItem::with_id(SHOW_WINDOW_ID, "Show window", true, None),
|
||||
&tray_icon::menu::MenuItem::with_id(QUIT_ID, "Quit", true, None),
|
||||
]).ok()?; // Use ok()? to convert Result to Option
|
||||
|
||||
|
||||
// Use the first PNG as the tray icon
|
||||
let icon_path = &cursors[0].path;
|
||||
let img = image_open(icon_path).ok()?; // Use ok()?
|
||||
let rgba_img = img.to_rgba8();
|
||||
let (width, height) = rgba_img.dimensions();
|
||||
let icon = tray_icon::Icon::from_rgba(rgba_img.into_raw(), width, height).ok()?; // Use ok()?
|
||||
|
||||
// Build the tray icon
|
||||
println!("Building tray icon...");
|
||||
let tray_result = TrayIconBuilder::new()
|
||||
.with_tooltip("Cursor Rush")
|
||||
.with_icon(icon)
|
||||
.with_menu(Box::new(menu))
|
||||
.with_menu_on_left_click(false) // Disable menu on left click - only show on right click
|
||||
.build();
|
||||
|
||||
println!("Tray icon configuration: menu_on_left_click = false");
|
||||
|
||||
// Store the tray icon itself
|
||||
let tray = match tray_result {
|
||||
Ok(tray_instance) => {
|
||||
println!("Tray icon built successfully");
|
||||
tray_instance // Assign the TrayIcon object
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to build tray icon: {}", e);
|
||||
return None; // Return None on error
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for tray menu events in a separate thread
|
||||
let state_clone = shared_state.clone();
|
||||
println!("Spawning tray menu event listener thread...");
|
||||
thread::spawn(move || {
|
||||
let menu_channel = MenuEvent::receiver();
|
||||
println!("Tray menu event listener waiting for events...");
|
||||
|
||||
while let Ok(event) = menu_channel.recv() {
|
||||
println!("Tray menu event received: ID = {:?}", event.id);
|
||||
let msg_to_send = match event.id.as_ref() {
|
||||
NEXT_CURSOR_ID => {
|
||||
println!("Tray: Next Cursor requested");
|
||||
match state_clone.lock() {
|
||||
Ok(mut state) => {
|
||||
if !state.cursors.is_empty() {
|
||||
state.current_index = (state.current_index + 1) % state.cursors.len();
|
||||
let current_index = state.current_index;
|
||||
if let Some(cursor) = state.cursors.get_mut(current_index) {
|
||||
println!("Tray: Updating cursor image: {}", cursor.name);
|
||||
crate::app::apply_cursor_info(cursor);
|
||||
// Use the cached image directly
|
||||
state.current_cursor_image = cursor.image.clone();
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => eprintln!("Tray: Failed to lock shared state for next cursor: {}", e),
|
||||
}
|
||||
// Send TrayEvent just to trigger UI update if needed
|
||||
Some(Message::TrayEvent)
|
||||
},
|
||||
SHOW_WINDOW_ID => {
|
||||
println!("Tray: Show Window requested");
|
||||
// Send ShowWindow message to main thread
|
||||
Some(Message::ShowWindow)
|
||||
},
|
||||
QUIT_ID => {
|
||||
println!("Tray: Quit requested");
|
||||
// Send Quit message to main thread
|
||||
Some(Message::Quit)
|
||||
},
|
||||
_ => {
|
||||
println!("Tray: Unknown menu event ID: {:?}", event.id);
|
||||
None // Don't send a message for unknown IDs
|
||||
}
|
||||
};
|
||||
|
||||
// Send the determined message to the main UI thread using the global sender
|
||||
if let Some(msg) = msg_to_send {
|
||||
// Get the global sender
|
||||
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
|
||||
if let Err(e) = tx.unbounded_send(msg.clone()) { // Use clone if msg is needed after send
|
||||
eprintln!("Tray: Failed to send {:?} message: {}", msg, e);
|
||||
break; // Stop thread if channel broken
|
||||
}
|
||||
// If Quit message was sent, break the loop to exit the listener thread
|
||||
if matches!(msg, Message::Quit) {
|
||||
println!("Tray: Quit message sent, exiting listener thread");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
eprintln!("Tray: Global sender not initialized yet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Tray menu event listener thread finished");
|
||||
});
|
||||
|
||||
// Set up a direct event handler for tray icon events
|
||||
// This is the preferred way to handle tray icon clicks
|
||||
println!("Setting up tray icon click event handler...");
|
||||
|
||||
// Use set_event_handler to directly handle tray icon events
|
||||
TrayIconEvent::set_event_handler(Some(move |event| {
|
||||
// Only process important events, filter out mouse movements
|
||||
match event {
|
||||
// Handle click events
|
||||
TrayIconEvent::Click { button, .. } => {
|
||||
println!("Tray: Click detected with button: {:?}", button);
|
||||
// Left click should show the window
|
||||
if button == MouseButton::Left {
|
||||
println!("Tray: Left click detected, sending ShowWindow message");
|
||||
// Get the global sender
|
||||
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
|
||||
if let Err(e) = tx.unbounded_send(Message::ShowWindow) {
|
||||
eprintln!("Tray: Failed to send ShowWindow message via handler: {}", e);
|
||||
}
|
||||
} else {
|
||||
eprintln!("Tray: Global sender not initialized yet for click handler");
|
||||
}
|
||||
}
|
||||
},
|
||||
// Handle double-click events
|
||||
TrayIconEvent::DoubleClick { button, .. } => {
|
||||
println!("Tray: Double click detected with button: {:?}", button);
|
||||
// Double left click should also show the window
|
||||
if button == MouseButton::Left {
|
||||
println!("Tray: Double left click detected, sending ShowWindow message");
|
||||
// Get the global sender
|
||||
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
|
||||
if let Err(e) = tx.unbounded_send(Message::ShowWindow) {
|
||||
eprintln!("Tray: Failed to send ShowWindow message via handler on double click: {}", e);
|
||||
}
|
||||
} else {
|
||||
eprintln!("Tray: Global sender not initialized yet for double click handler");
|
||||
}
|
||||
}
|
||||
},
|
||||
// Silently ignore mouse movement events
|
||||
TrayIconEvent::Move { .. } => {
|
||||
// Don't log anything for mouse movements
|
||||
},
|
||||
// Silently ignore mouse enter/leave events
|
||||
TrayIconEvent::Enter { .. } | TrayIconEvent::Leave { .. } => {
|
||||
// Don't log anything for mouse enter/leave
|
||||
},
|
||||
// Log other events for debugging (but not mouse-related ones)
|
||||
_ => {
|
||||
println!("Tray: Event received: {:?}", event);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
println!("Tray setup complete using set_event_handler for clicks."); // Updated log
|
||||
Some(tray) // Return the TrayIcon object
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// UI view components
|
||||
|
||||
use iced::{
|
||||
widget::{button, text, Column, Row, container, image as iced_image},
|
||||
Element, Length,
|
||||
};
|
||||
|
||||
use crate::app::{CursorRush, Message};
|
||||
|
||||
// View function to render the UI
|
||||
pub fn view_app(state: &CursorRush) -> Element<Message> {
|
||||
let mut content = Column::new()
|
||||
.spacing(16)
|
||||
.push(text("Cursor Rush").size(24));
|
||||
|
||||
// Add a button for each cursor
|
||||
if !state.cursors.is_empty() {
|
||||
let current_cursor = &state.cursors[state.current_index];
|
||||
content = content.push(text(format!("Current cursor: {}", current_cursor.name)));
|
||||
|
||||
// Display the current cursor image as a preview
|
||||
let img_handle = iced_image::Handle::from_path(¤t_cursor.path);
|
||||
let cursor_image = iced_image::Image::new(img_handle)
|
||||
.width(Length::Fixed(64.0))
|
||||
.height(Length::Fixed(64.0));
|
||||
|
||||
content = content.push(cursor_image);
|
||||
|
||||
// Create a row of cursor buttons
|
||||
let mut cursor_row = Row::new().spacing(10);
|
||||
|
||||
for (i, cursor) in state.cursors.iter().enumerate() {
|
||||
// Create a small preview image for each cursor
|
||||
let preview_handle = iced_image::Handle::from_path(&cursor.path);
|
||||
let preview_image = iced_image::Image::new(preview_handle)
|
||||
.width(Length::Fixed(32.0))
|
||||
.height(Length::Fixed(32.0));
|
||||
|
||||
let btn = button(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(preview_image)
|
||||
.push(text(&cursor.name).size(16))
|
||||
)
|
||||
.on_press(Message::SelectCursor(i));
|
||||
|
||||
cursor_row = cursor_row.push(btn);
|
||||
}
|
||||
|
||||
content = content
|
||||
.push(cursor_row)
|
||||
.push(button("Next cursor").on_press(Message::NextCursor))
|
||||
.push(button("Quit").on_press(Message::Quit));
|
||||
|
||||
// Add instructions for the hotkey and close button behavior
|
||||
content = content.push(
|
||||
container(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(text("Press Ctrl+Shift+F12 to exit the application").size(16))
|
||||
.push(text("Close button (X) minimizes to system tray").size(16))
|
||||
)
|
||||
.padding(10)
|
||||
);
|
||||
} else {
|
||||
content = content.push(text("No cursors found in assets directory"));
|
||||
}
|
||||
|
||||
// Create the main container
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
.padding(20)
|
||||
.into()
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Application configuration
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Application configuration
|
||||
pub struct Config {
|
||||
pub assets_dir: PathBuf,
|
||||
pub default_cursor: String,
|
||||
pub auto_start: bool,
|
||||
pub minimize_to_tray: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
assets_dir: PathBuf::from("assets"),
|
||||
default_cursor: String::from("default"),
|
||||
auto_start: false,
|
||||
minimize_to_tray: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load configuration from file or use defaults
|
||||
pub fn load_config() -> Config {
|
||||
// TODO: Implement loading from a config file
|
||||
Config::default()
|
||||
}
|
||||
|
||||
// Save configuration to file
|
||||
pub fn save_config(_config: &Config) -> Result<(), String> {
|
||||
// TODO: Implement saving to a config file
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Logging utilities
|
||||
|
||||
// Log levels
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum LogLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
// Global log level (could be made configurable)
|
||||
static mut CURRENT_LOG_LEVEL: LogLevel = LogLevel::Info;
|
||||
|
||||
// Set the global log level
|
||||
pub fn set_log_level(level: LogLevel) {
|
||||
unsafe {
|
||||
CURRENT_LOG_LEVEL = level;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the current log level
|
||||
pub fn get_log_level() -> LogLevel {
|
||||
unsafe {
|
||||
CURRENT_LOG_LEVEL
|
||||
}
|
||||
}
|
||||
|
||||
// Log a message at the specified level
|
||||
pub fn log(level: LogLevel, message: &str) {
|
||||
if level >= unsafe { CURRENT_LOG_LEVEL } {
|
||||
let prefix = match level {
|
||||
LogLevel::Debug => "[DEBUG]",
|
||||
LogLevel::Info => "[INFO]",
|
||||
LogLevel::Warning => "[WARNING]",
|
||||
LogLevel::Error => "[ERROR]",
|
||||
};
|
||||
|
||||
println!("{} {}", prefix, message);
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience functions for different log levels
|
||||
pub fn debug(message: &str) {
|
||||
log(LogLevel::Debug, message);
|
||||
}
|
||||
|
||||
pub fn info(message: &str) {
|
||||
log(LogLevel::Info, message);
|
||||
}
|
||||
|
||||
pub fn warning(message: &str) {
|
||||
log(LogLevel::Warning, message);
|
||||
}
|
||||
|
||||
pub fn error(message: &str) {
|
||||
log(LogLevel::Error, message);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// Utility functions and helpers
|
||||
|
||||
pub mod config;
|
||||
pub mod logging;
|
||||