diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 64ae2b334..1eced404b 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -150,3 +150,4 @@ jobs: target/universal-apple-darwin/release/bundle/dmg/Modrinth App_*.dmg* target/release/bundle/nsis/Modrinth App_*-setup.exe* target/release/bundle/nsis/Modrinth App_*-setup.nsis.zip* + target/release/Modrinth App.exe diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 11cc6600e..0a493eb29 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -255,7 +255,16 @@ initialize_state() }) const handleClose = async () => { - await saveWindowState(StateFlags.ALL) + let isPortable = false + try { + isPortable = !!(await invoke('is_portable_mode')) + console.log('Portable mode:', isPortable) + } catch (err) { + console.warn('Failed to check portable mode:', err) + } + if (!isPortable) { + await saveWindowState(StateFlags.ALL) + } await getCurrentWindow().close() } diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 09723d39f..4ec08f4ff 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -7,6 +7,7 @@ use native_dialog::{DialogBuilder, MessageLevel}; use std::env; use tauri::{Listener, Manager}; use theseus::prelude::*; +use theseus::DirectoryInfo; mod api; mod error; @@ -141,9 +142,16 @@ fn restart_app(app: tauri::AppHandle) { app.restart(); } +#[tauri::command] +fn is_portable_mode() -> bool { + theseus::DirectoryInfo::is_portable_mode() +} + // if Tauri app is called with arguments, then those arguments will be treated as commands // ie: deep links or filepaths for .mrpacks fn main() { + // Set up portable environment as early as possible (before any other initialization) + DirectoryInfo::setup_portable_env(); /* tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show ERROR > WARN > INFO > DEBUG > TRACE @@ -187,13 +195,18 @@ fn main() { .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_deep_link::init()) - .plugin(tauri_plugin_opener::init()) - .plugin( + .plugin(tauri_plugin_opener::init()); + + // Only add window state plugin if not portable + if !theseus::DirectoryInfo::is_portable_mode() { + builder = builder.plugin( tauri_plugin_window_state::Builder::default() .with_filename("app-window-state.json") .build(), - ) - .setup(|app| { + ); + } + + builder = builder.setup(|app| { #[cfg(target_os = "macos")] { let payload = macos::deep_link::get_or_init_payload(app); @@ -267,6 +280,7 @@ fn main() { toggle_decorations, show_window, restart_app, + is_portable_mode, ]); tracing::info!("Initializing app..."); diff --git a/apps/docs/src/content/docs/guide/portable-mode.md b/apps/docs/src/content/docs/guide/portable-mode.md new file mode 100644 index 000000000..8f97d1ec0 --- /dev/null +++ b/apps/docs/src/content/docs/guide/portable-mode.md @@ -0,0 +1,163 @@ +--- +title: Portable Mode +description: Learn how to use Modrinth App in portable mode for maximum flexibility and convenience. +--- + +The Modrinth App supports **portable mode**, which allows you to run the application from any location without requiring installation to system directories. In portable mode, all application data is stored alongside the executable, making it perfect for USB drives, shared computers, or when you want a completely self-contained setup. + +## What is Portable Mode? + +Portable mode changes how the Modrinth App stores its data. Instead of using system directories like: + +- `%APPDATA%\ModrinthApp` (Windows) +- `~/Library/Application Support/ModrinthApp` (macOS) +- `~/.local/share/ModrinthApp` (Linux) + +All data is stored in a `ModrinthAppData` folder right next to the executable, making the entire installation truly portable. + +## Benefits of Portable Mode + +- **No Installation Required**: Run directly from any location +- **System Independence**: No registry entries or system directory modifications +- **Easy Backup**: Simply copy the entire folder to backup everything +- **Multi-System**: Use the same setup across different computers +- **Clean Removal**: Delete the folder to completely remove all traces +- **Isolation**: Perfect for testing without affecting main installations + +## Enabling Portable Mode + +To enable portable mode, simply create an empty file named `portable.txt` in the same directory as your Modrinth App executable, then launch the app. The app will automatically detect the file and enable portable mode. All app data will be stored in a `ModrinthAppData` folder next to the executable. + +``` +YourFolder/ +├── Modrinth App.exe (or modrinth-app on Linux/macOS) +├── portable.txt ← Create this file +└── ModrinthAppData/ ← Created automatically on first run +``` + +## Directory Structure + +When portable mode is enabled, your directory structure will look like this: + +``` +YourPortableFolder/ +├── Modrinth App.exe # The application executable +├── portable.txt # Enables portable mode (Method 1) +└── ModrinthAppData/ # All app data stored here + ├── profiles/ # Minecraft instances and profiles + │ ├── vanilla/ + │ ├── modded/ + │ └── ... + ├── caches/ # Downloaded files cache + │ ├── mods/ + │ ├── resource_packs/ + │ └── ... + ├── launcher_logs/ # Application logs + ├── meta/ # Metadata files + └── app.db # Application database +``` + +## Creating a Portable Installation + +### From Source (Developers) + +If you're building from source: + +1. Build the application: + + ```bash + pnpm app:build + ``` + +2. Copy the executable from `target/release/` to your portable folder + +3. Create the portable indicator: + ```bash + # Create portable.txt file + echo "Portable mode enabled" > portable.txt + ``` + +### From Release Binary + +1. Download the Modrinth App executable from [modrinth.com/app](https://modrinth.com/app) +2. Place it in your desired portable folder +3. Create an empty `portable.txt` file next to the executable +4. Launch the app + +## Use Cases + +### USB Drive Setup + +Perfect for carrying your complete Minecraft setup on a USB drive: + +``` +USB_Drive/ +├── ModrinthApp/ +│ ├── Modrinth App.exe +│ ├── portable.txt +│ └── ModrinthAppData/ +└── other_files/ +``` + +### Shared Computer + +Use your personal setup on shared computers without affecting other users or requiring installation permissions. + +### Testing Environment + +Test different modrinth configurations or app versions without affecting your main installation. + +### System Migration + +Easily move your entire Minecraft setup to a new computer by copying the portable folder. + +## Switching Between Modes + +### Converting Regular Installation to Portable + +1. Create a portable folder with the executable and `portable.txt` +2. Copy your existing data from the system directory to `ModrinthAppData/` +3. Launch the portable version + +### Converting Portable to Regular Installation + +1. Install the app normally +2. Copy data from `ModrinthAppData/` to the system directory +3. Remove the `portable.txt` file + +## Technical Notes + +- **Detection**: Portable mode is enabled only by the presence of `portable.txt` next to the executable +- **Config Directory**: The app sets the `THESEUS_CONFIG_DIR` environment variable automatically when portable mode is detected, ensuring all components use the correct data directory +- **Performance**: Portable mode has identical performance to regular installations +- **Platform Support**: Available on Windows, macOS, and Linux +- **First Launch**: The `ModrinthAppData` directory is created automatically on first run +- **File Permissions**: Ensure the portable folder has write permissions + +## Troubleshooting + +### App Not Detecting Portable Mode + +- Verify `portable.txt` is in the same directory as the executable +- Check file permissions on the portable folder + +### Data Not Saving + +- Ensure the portable folder has write permissions +- Check that antivirus software isn't blocking file creation +- Verify there's sufficient disk space + +### Performance Issues + +- Portable mode shouldn't affect performance +- If using a USB drive, ensure it has adequate read/write speeds +- Consider using USB 3.0+ for better performance + +## Security Considerations + +- **Portable Storage**: Be mindful of where you store portable installations +- **Data Encryption**: Consider encrypting USB drives with sensitive data +- **Access Control**: Set appropriate file permissions on shared systems +- **Backup**: Regularly backup your portable installation + +Portable mode makes the Modrinth App incredibly flexible and convenient for various use cases while maintaining all the functionality of a standard installation. diff --git a/packages/app-lib/src/lib.rs b/packages/app-lib/src/lib.rs index 258e72423..70aa81765 100644 --- a/packages/app-lib/src/lib.rs +++ b/packages/app-lib/src/lib.rs @@ -15,7 +15,7 @@ mod error; mod event; mod launcher; mod logger; -mod state; +pub mod state; pub use api::*; pub use error::*; @@ -25,3 +25,4 @@ pub use event::{ }; pub use logger::start_logger; pub use state::State; +pub use state::dirs::DirectoryInfo; diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index 1f577f7c4..d8e61b61f 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -5,6 +5,7 @@ use crate::state::{JavaVersion, Profile, Settings}; use crate::util::fetch::IoSemaphore; use dashmap::DashSet; use std::path::{Path, PathBuf}; +use std::env; use std::sync::Arc; use tokio::fs; @@ -20,9 +21,67 @@ pub struct DirectoryInfo { } impl DirectoryInfo { + /// Returns true if portable mode is enabled (portable.txt next to executable) + pub fn is_portable_mode() -> bool { + Self::portable_data_dir().is_some() + } + + /// Call this as early as possible in main() to ensure all dependencies use the correct portable directory. + /// + /// Example usage (at the very top of main.rs): + /// theseus::DirectoryInfo::setup_portable_env(); + pub fn setup_portable_env() { + if let Some(portable_dir) = Self::portable_data_dir() { + Self::set_portable_env(&portable_dir); + } + } + /// Returns the path to the directory containing the running executable, if possible. + fn exe_dir() -> Option { + std::env::current_exe().ok().and_then(|p| p.parent().map(|p| p.to_path_buf())) + } + + /// Checks if portable mode is enabled (portable.txt) and returns the portable data dir if so. + fn portable_data_dir() -> Option { + // portable.txt file next to executable + if let Some(exe_dir) = Self::exe_dir() { + let portable_txt = exe_dir.join("portable.txt"); + if portable_txt.exists() { + return Some(exe_dir.join("ModrinthAppData")); + } + } + None + } + + /// Sets the process environment variable for config dir to the portable dir if in portable mode. + fn set_portable_env(portable_dir: &Path) { + // Always set THESEUS_CONFIG_DIR for portable mode + unsafe { + env::set_var("THESEUS_CONFIG_DIR", portable_dir); + } + #[cfg(target_os = "windows")] + unsafe { + env::set_var("APPDATA", portable_dir); + } + #[cfg(target_os = "macos")] + unsafe { + // macOS typically uses HOME/Library/Application Support, but we can override XDG_DATA_HOME for some libs + env::set_var("XDG_DATA_HOME", portable_dir); + } + #[cfg(target_os = "linux")] + unsafe { + env::set_var("XDG_DATA_HOME", portable_dir); + } + } + // Get the settings directory // init() is not needed for this function pub fn get_initial_settings_dir() -> Option { + // If portable mode, set env and use portable dir + if let Some(portable_dir) = Self::portable_data_dir() { + Self::set_portable_env(&portable_dir); + return Some(portable_dir); + } + // Otherwise, use THESEUS_CONFIG_DIR if set Self::env_path("THESEUS_CONFIG_DIR") .or_else(|| Some(dirs::data_dir()?.join("ModrinthApp"))) } @@ -168,7 +227,7 @@ impl DirectoryInfo { /// Get path from environment variable #[inline] fn env_path(name: &str) -> Option { - std::env::var_os(name).map(PathBuf::from) + env::var_os(name).map(PathBuf::from) } #[tracing::instrument(skip(settings, exec, io_semaphore))] diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index ab7a5e3e9..c1502f8aa 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -7,7 +7,7 @@ use crate::state::fs_watcher::FileWatcher; use sqlx::SqlitePool; // Submodules -mod dirs; +pub mod dirs; pub use self::dirs::*; mod profiles;