From 9f733ed39bed5231c34bf72812b2807495bca3af Mon Sep 17 00:00:00 2001 From: JobyGitGud Date: Sat, 17 May 2025 21:07:31 -0500 Subject: [PATCH] Integrate Tauri frontend with categorization system and Steam integration --- .vscode/settings.json | 3 + README.md | 78 ++----- frontend/src-tauri/Cargo.toml | 5 +- frontend/src-tauri/src/integrations/mod.rs | 3 + frontend/src-tauri/src/integrations/steam.rs | 26 +++ frontend/src-tauri/src/lib.rs | 218 ++++++++++++++---- frontend/src-tauri/tauri.conf.json | 16 +- frontend/src/App.jsx | 109 +++++++-- frontend/src/components/Content.jsx | 12 +- frontend/src/components/Header.jsx | 35 ++- .../components/content_modules/Settings.jsx | 30 ++- 11 files changed, 392 insertions(+), 143 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 frontend/src-tauri/src/integrations/mod.rs create mode 100644 frontend/src-tauri/src/integrations/steam.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3b66410 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "git.ignoreLimitWarning": true +} \ No newline at end of file diff --git a/README.md b/README.md index 4e574fe..eb77e07 100644 --- a/README.md +++ b/README.md @@ -16,43 +16,26 @@ A cross-platform application for customizing your cursor with gameified logic. ## 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 @@ -149,27 +132,6 @@ The application includes integration with the Steam API for future gameified fea - Item drop functionality is implemented but not yet active - Steam must be running for this functionality to work -## Project Structure (Tauri Version) - -``` -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 -│ │ └── platform/ # Platform-specific code -│ │ ├── mod.rs # Platform module exports -│ │ ├── windows/ # Windows-specific code -│ │ ├── macos/ # macOS-specific code -│ │ └── linux/ # Linux-specific code -│ └── Cargo.toml # Rust dependencies -└── package.json # npm dependencies -``` - ## Contributing Contributions to Cursor Rush are welcome! Here are some ways you can contribute: diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 2de4348..39137b9 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -14,13 +14,14 @@ rust-version = "1.56" tauri-build = { version = "1.5.2", features = [] } [dependencies] -tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open"] } +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"] } +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!! diff --git a/frontend/src-tauri/src/integrations/mod.rs b/frontend/src-tauri/src/integrations/mod.rs new file mode 100644 index 0000000..d320bd0 --- /dev/null +++ b/frontend/src-tauri/src/integrations/mod.rs @@ -0,0 +1,3 @@ +// External integrations module + +pub mod steam; diff --git a/frontend/src-tauri/src/integrations/steam.rs b/frontend/src-tauri/src/integrations/steam.rs new file mode 100644 index 0000000..876c818 --- /dev/null +++ b/frontend/src-tauri/src/integrations/steam.rs @@ -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); + } +} diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index f019348..8ee9422 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1,10 +1,11 @@ use std::sync::Mutex; use serde::{Serialize, Deserialize}; -use tauri::{Manager, AppHandle}; +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)] @@ -40,26 +41,26 @@ fn get_current_cursor_index(state: tauri::State) -> usize { #[tauri::command] fn select_cursor(index: usize, state: tauri::State, 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()) { @@ -73,29 +74,29 @@ fn select_cursor(index: usize, state: tauri::State, app_handle: 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 { @@ -111,11 +112,11 @@ fn select_cursor(index: usize, state: tauri::State, app_handle: #[tauri::command] fn next_cursor(state: tauri::State, app_handle: AppHandle) -> Result { 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); @@ -126,22 +127,22 @@ fn next_cursor(state: tauri::State, app_handle: AppHandle) -> R // 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 { @@ -161,22 +162,40 @@ fn quit_app(app_handle: AppHandle) { 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 { 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"), @@ -184,26 +203,26 @@ fn load_cursor_files() -> Vec { 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, @@ -212,14 +231,14 @@ fn load_cursor_files() -> Vec { } } } - + 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."); @@ -228,28 +247,28 @@ fn load_cursor_files() -> Vec { 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 @@ -258,34 +277,153 @@ fn load_cursor_files() -> Vec { #[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::(); + 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 + 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 diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index aa3e874..4981592 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -14,7 +14,11 @@ "all": false, "window": { "all": false, - "startDragging": true + "startDragging": true, + "minimize": true, + "hide": true, + "show": true, + "close": true }, "shell": { "all": false, @@ -27,6 +31,12 @@ ] } }, + "systemTray": { + "iconPath": "icons/icon.png", + "iconAsTemplate": false, + "menuOnLeftClick": false, + "title": "Cursor Rush" + }, "bundle": { "active": true, "identifier": "com.cursor-rush.dev", @@ -47,13 +57,15 @@ }, "windows": [ { + "label": "main", "title": "Cursor Rush", "width": 310, "height": 600, "resizable": true, "fullscreen": false, "decorations": false, - "transparent": true + "transparent": true, + "visible": true } ] } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index de12990..00caadf 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { invoke } from "@tauri-apps/api/tauri"; import { appWindow } from "@tauri-apps/api/window"; import "./App.css"; @@ -17,8 +17,37 @@ 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 + }); - // Load cursors on mount + // 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 { @@ -26,7 +55,7 @@ function App() { 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); @@ -47,6 +76,53 @@ function App() { 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 { @@ -60,7 +136,7 @@ function App() { const nextCursor = async () => { if (cursors.length === 0) return; - + try { console.log('Switching to next cursor'); const newIndex = await invoke("next_cursor"); @@ -73,16 +149,6 @@ function App() { } }; - // Function to minimize app to tray - const minimizeToTray = async () => { - try { - console.log('Minimizing app to tray'); - await appWindow.hide(); - } catch (error) { - console.error("Failed to minimize to tray:", error); - } - }; - // Function to exit app const quitApp = async () => { try { @@ -96,22 +162,25 @@ function App() { return (
-
- - - -