aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-16 20:24:53 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-16 20:24:53 +0800
commit853f40dc13e6463bedf30e2471a71bd011be425b (patch)
tree6dce37f1ae9a4617fe6c88b00a25e14e6a45f139 /src-tauri/src
parent743401f15199a116b1777bced843c774c5a59fba (diff)
downloadDropOut-853f40dc13e6463bedf30e2471a71bd011be425b.tar.gz
DropOut-853f40dc13e6463bedf30e2471a71bd011be425b.zip
feat: implement instance management features and enhance game launch process
Added functionality for managing game instances, including creating, deleting, updating, and duplicating instances. Integrated instance selection into the game launch process, allowing users to specify the instance when starting a game. Updated the main application logic to handle instance states and paths, ensuring proper directory management for each instance. Introduced a new module for instance management and updated relevant commands to support instance-specific operations.
Diffstat (limited to 'src-tauri/src')
-rw-r--r--src-tauri/src/core/instance.rs325
-rw-r--r--src-tauri/src/core/mod.rs1
-rw-r--r--src-tauri/src/main.rs260
-rw-r--r--src-tauri/src/utils/mod.rs2
-rw-r--r--src-tauri/src/utils/path.rs22
5 files changed, 526 insertions, 84 deletions
diff --git a/src-tauri/src/core/instance.rs b/src-tauri/src/core/instance.rs
new file mode 100644
index 0000000..90ec34e
--- /dev/null
+++ b/src-tauri/src/core/instance.rs
@@ -0,0 +1,325 @@
+//! Instance/Profile management module.
+//!
+//! This module provides functionality to:
+//! - Create and manage multiple isolated game instances
+//! - Each instance has its own versions, libraries, assets, mods, and saves
+//! - Support for instance switching and isolation
+
+use serde::{Deserialize, Serialize};
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::sync::Mutex;
+use tauri::{AppHandle, Manager};
+
+/// Represents a game instance/profile
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Instance {
+ pub id: String, // 唯一标识符(UUID)
+ pub name: String, // 显示名称
+ pub game_dir: PathBuf, // 游戏目录路径
+ pub version_id: Option<String>, // 当前选择的版本ID
+ pub created_at: i64, // 创建时间戳
+ pub last_played: Option<i64>, // 最后游玩时间
+ pub icon_path: Option<String>, // 图标路径(可选)
+ pub notes: Option<String>, // 备注(可选)
+ pub mod_loader: Option<String>, // 模组加载器类型:"fabric", "forge", "vanilla"
+ pub mod_loader_version: Option<String>, // 模组加载器版本
+}
+
+/// Configuration for all instances
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct InstanceConfig {
+ pub instances: Vec<Instance>,
+ pub active_instance_id: Option<String>, // 当前活动的实例ID
+}
+
+/// State management for instances
+pub struct InstanceState {
+ pub instances: Mutex<InstanceConfig>,
+ pub file_path: PathBuf,
+}
+
+impl InstanceState {
+ /// Create a new InstanceState
+ pub fn new(app_handle: &AppHandle) -> Self {
+ let app_dir = app_handle.path().app_data_dir().unwrap();
+ let file_path = app_dir.join("instances.json");
+
+ let config = if file_path.exists() {
+ let content = fs::read_to_string(&file_path).unwrap_or_default();
+ serde_json::from_str(&content).unwrap_or_else(|_| InstanceConfig::default())
+ } else {
+ InstanceConfig::default()
+ };
+
+ Self {
+ instances: Mutex::new(config),
+ file_path,
+ }
+ }
+
+ /// Save the instance configuration to disk
+ pub fn save(&self) -> Result<(), String> {
+ let config = self.instances.lock().unwrap();
+ let content = serde_json::to_string_pretty(&*config).map_err(|e| e.to_string())?;
+ fs::create_dir_all(self.file_path.parent().unwrap()).map_err(|e| e.to_string())?;
+ fs::write(&self.file_path, content).map_err(|e| e.to_string())?;
+ Ok(())
+ }
+
+ /// Create a new instance
+ pub fn create_instance(
+ &self,
+ name: String,
+ app_handle: &AppHandle,
+ ) -> Result<Instance, String> {
+ let app_dir = app_handle.path().app_data_dir().unwrap();
+ let instance_id = uuid::Uuid::new_v4().to_string();
+ let instance_dir = app_dir.join("instances").join(&instance_id);
+ let game_dir = instance_dir.clone();
+
+ // Create instance directory structure
+ fs::create_dir_all(&instance_dir).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("versions")).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("libraries")).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("assets")).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("mods")).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("config")).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("saves")).map_err(|e| e.to_string())?;
+
+ let instance = Instance {
+ id: instance_id.clone(),
+ name,
+ game_dir,
+ version_id: None,
+ created_at: chrono::Utc::now().timestamp(),
+ last_played: None,
+ icon_path: None,
+ notes: None,
+ mod_loader: Some("vanilla".to_string()),
+ mod_loader_version: None,
+ };
+
+ let mut config = self.instances.lock().unwrap();
+ config.instances.push(instance.clone());
+
+ // If this is the first instance, set it as active
+ if config.active_instance_id.is_none() {
+ config.active_instance_id = Some(instance_id);
+ }
+
+ drop(config);
+ self.save()?;
+
+ Ok(instance)
+ }
+
+ /// Delete an instance
+ pub fn delete_instance(&self, id: &str) -> Result<(), String> {
+ let mut config = self.instances.lock().unwrap();
+
+ // Find the instance
+ let instance_index = config
+ .instances
+ .iter()
+ .position(|i| i.id == id)
+ .ok_or_else(|| format!("Instance {} not found", id))?;
+
+ let instance = config.instances[instance_index].clone();
+
+ // Remove from list
+ config.instances.remove(instance_index);
+
+ // If this was the active instance, clear or set another as active
+ if config.active_instance_id.as_ref() == Some(&id.to_string()) {
+ config.active_instance_id = config.instances.first().map(|i| i.id.clone());
+ }
+
+ drop(config);
+ self.save()?;
+
+ // Delete the instance directory
+ if instance.game_dir.exists() {
+ fs::remove_dir_all(&instance.game_dir)
+ .map_err(|e| format!("Failed to delete instance directory: {}", e))?;
+ }
+
+ Ok(())
+ }
+
+ /// Update an instance
+ pub fn update_instance(&self, instance: Instance) -> Result<(), String> {
+ let mut config = self.instances.lock().unwrap();
+
+ let index = config
+ .instances
+ .iter()
+ .position(|i| i.id == instance.id)
+ .ok_or_else(|| format!("Instance {} not found", instance.id))?;
+
+ config.instances[index] = instance;
+ drop(config);
+ self.save()?;
+
+ Ok(())
+ }
+
+ /// Get an instance by ID
+ pub fn get_instance(&self, id: &str) -> Option<Instance> {
+ let config = self.instances.lock().unwrap();
+ config.instances.iter().find(|i| i.id == id).cloned()
+ }
+
+ /// List all instances
+ pub fn list_instances(&self) -> Vec<Instance> {
+ let config = self.instances.lock().unwrap();
+ config.instances.clone()
+ }
+
+ /// Set the active instance
+ pub fn set_active_instance(&self, id: &str) -> Result<(), String> {
+ let mut config = self.instances.lock().unwrap();
+
+ // Verify the instance exists
+ if !config.instances.iter().any(|i| i.id == id) {
+ return Err(format!("Instance {} not found", id));
+ }
+
+ config.active_instance_id = Some(id.to_string());
+ drop(config);
+ self.save()?;
+
+ Ok(())
+ }
+
+ /// Get the active instance
+ pub fn get_active_instance(&self) -> Option<Instance> {
+ let config = self.instances.lock().unwrap();
+ config
+ .active_instance_id
+ .as_ref()
+ .and_then(|id| config.instances.iter().find(|i| i.id == *id))
+ .cloned()
+ }
+
+ /// Get the game directory for an instance
+ pub fn get_instance_game_dir(&self, id: &str) -> Option<PathBuf> {
+ self.get_instance(id).map(|i| i.game_dir)
+ }
+
+ /// Duplicate an instance
+ pub fn duplicate_instance(
+ &self,
+ id: &str,
+ new_name: String,
+ app_handle: &AppHandle,
+ ) -> Result<Instance, String> {
+ let source_instance = self
+ .get_instance(id)
+ .ok_or_else(|| format!("Instance {} not found", id))?;
+
+ // Create new instance
+ let mut new_instance = self.create_instance(new_name, app_handle)?;
+
+ // Copy instance properties
+ new_instance.version_id = source_instance.version_id.clone();
+ new_instance.mod_loader = source_instance.mod_loader.clone();
+ new_instance.mod_loader_version = source_instance.mod_loader_version.clone();
+ new_instance.notes = source_instance.notes.clone();
+
+ // Copy directory contents
+ if source_instance.game_dir.exists() {
+ copy_dir_all(&source_instance.game_dir, &new_instance.game_dir)
+ .map_err(|e| format!("Failed to copy instance directory: {}", e))?;
+ }
+
+ self.update_instance(new_instance.clone())?;
+
+ Ok(new_instance)
+ }
+}
+
+/// Copy a directory recursively
+fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), std::io::Error> {
+ fs::create_dir_all(dst)?;
+ for entry in fs::read_dir(src)? {
+ let entry = entry?;
+ let ty = entry.file_type()?;
+ if ty.is_dir() {
+ copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?;
+ } else {
+ fs::copy(entry.path(), dst.join(entry.file_name()))?;
+ }
+ }
+ Ok(())
+}
+
+/// Migrate legacy data to instance system
+pub fn migrate_legacy_data(
+ app_handle: &AppHandle,
+ instance_state: &InstanceState,
+) -> Result<(), String> {
+ let app_dir = app_handle.path().app_data_dir().unwrap();
+ let old_versions_dir = app_dir.join("versions");
+ let old_libraries_dir = app_dir.join("libraries");
+ let old_assets_dir = app_dir.join("assets");
+
+ // Check if legacy data exists
+ let has_legacy_data =
+ old_versions_dir.exists() || old_libraries_dir.exists() || old_assets_dir.exists();
+
+ if !has_legacy_data {
+ return Ok(()); // No legacy data to migrate
+ }
+
+ // Check if instances already exist
+ let config = instance_state.instances.lock().unwrap();
+ if !config.instances.is_empty() {
+ drop(config);
+ return Ok(()); // Already have instances, skip migration
+ }
+ drop(config);
+
+ // Create default instance
+ let default_instance = instance_state
+ .create_instance("Default".to_string(), app_handle)
+ .map_err(|e| format!("Failed to create default instance: {}", e))?;
+
+ let new_versions_dir = default_instance.game_dir.join("versions");
+ let new_libraries_dir = default_instance.game_dir.join("libraries");
+ let new_assets_dir = default_instance.game_dir.join("assets");
+
+ // Move legacy data
+ if old_versions_dir.exists() {
+ if new_versions_dir.exists() {
+ // Merge directories
+ copy_dir_all(&old_versions_dir, &new_versions_dir)
+ .map_err(|e| format!("Failed to migrate versions: {}", e))?;
+ } else {
+ fs::rename(&old_versions_dir, &new_versions_dir)
+ .map_err(|e| format!("Failed to migrate versions: {}", e))?;
+ }
+ }
+
+ if old_libraries_dir.exists() {
+ if new_libraries_dir.exists() {
+ copy_dir_all(&old_libraries_dir, &new_libraries_dir)
+ .map_err(|e| format!("Failed to migrate libraries: {}", e))?;
+ } else {
+ fs::rename(&old_libraries_dir, &new_libraries_dir)
+ .map_err(|e| format!("Failed to migrate libraries: {}", e))?;
+ }
+ }
+
+ if old_assets_dir.exists() {
+ if new_assets_dir.exists() {
+ copy_dir_all(&old_assets_dir, &new_assets_dir)
+ .map_err(|e| format!("Failed to migrate assets: {}", e))?;
+ } else {
+ fs::rename(&old_assets_dir, &new_assets_dir)
+ .map_err(|e| format!("Failed to migrate assets: {}", e))?;
+ }
+ }
+
+ Ok(())
+}
diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs
index 7ad6ef9..dcbd47a 100644
--- a/src-tauri/src/core/mod.rs
+++ b/src-tauri/src/core/mod.rs
@@ -6,6 +6,7 @@ pub mod downloader;
pub mod fabric;
pub mod forge;
pub mod game_version;
+pub mod instance;
pub mod java;
pub mod manifest;
pub mod maven;
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 661309a..2871b03 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -68,11 +68,16 @@ async fn start_game(
auth_state: State<'_, core::auth::AccountState>,
config_state: State<'_, core::config::ConfigState>,
assistant_state: State<'_, core::assistant::AssistantState>,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
version_id: String,
) -> Result<String, String> {
emit_log!(
window,
- format!("Starting game launch for version: {}", version_id)
+ format!(
+ "Starting game launch for version: {} in instance: {}",
+ version_id, instance_id
+ )
);
// Check for active account
@@ -93,14 +98,10 @@ async fn start_game(
format!("Memory: {}MB - {}MB", config.min_memory, config.max_memory)
);
- // Get App Data Directory (e.g., ~/.local/share/com.dropout.launcher or similar)
- // The identifier is set in tauri.conf.json.
- // If not accessible, use a specific logic.
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ // Get game directory from instance
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Ensure game directory exists
tokio::fs::create_dir_all(&game_dir)
@@ -169,6 +170,7 @@ async fn start_game(
};
// Check if configured Java is compatible
+ let app_handle = window.app_handle();
let mut java_path_to_use = config.java_path.clone();
if !java_path_to_use.is_empty() && java_path_to_use != "java" {
let is_compatible =
@@ -890,12 +892,15 @@ async fn get_versions(window: Window) -> Result<Vec<core::manifest::Version>, St
/// Check if a version is installed (has client.jar)
#[tauri::command]
-async fn check_version_installed(window: Window, version_id: String) -> Result<bool, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+async fn check_version_installed(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ version_id: String,
+) -> Result<bool, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// For modded versions, check the parent vanilla version
let minecraft_version = if version_id.starts_with("fabric-loader-") {
@@ -929,19 +934,24 @@ async fn check_version_installed(window: Window, version_id: String) -> Result<b
async fn install_version(
window: Window,
config_state: State<'_, core::config::ConfigState>,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
version_id: String,
) -> Result<(), String> {
emit_log!(
window,
- format!("Starting installation for version: {}", version_id)
+ format!(
+ "Starting installation for version: {} in instance: {}",
+ version_id, instance_id
+ )
);
let config = config_state.config.lock().unwrap().clone();
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+
+ // Get game directory from instance
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Ensure game directory exists
tokio::fs::create_dir_all(&game_dir)
@@ -1530,22 +1540,22 @@ async fn get_fabric_loaders_for_version(
#[tauri::command]
async fn install_fabric(
window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
game_version: String,
loader_version: String,
) -> Result<core::fabric::InstalledFabricVersion, String> {
emit_log!(
window,
format!(
- "Installing Fabric {} for Minecraft {}...",
- loader_version, game_version
+ "Installing Fabric {} for Minecraft {} in instance {}...",
+ loader_version, game_version, instance_id
)
);
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
let result = core::fabric::install_fabric(&game_dir, &game_version, &loader_version)
.await
@@ -1564,12 +1574,14 @@ async fn install_fabric(
/// List installed Fabric versions
#[tauri::command]
-async fn list_installed_fabric_versions(window: Window) -> Result<Vec<String>, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+async fn list_installed_fabric_versions(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<Vec<String>, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
core::fabric::list_installed_fabric_versions(&game_dir)
.await
@@ -1579,14 +1591,14 @@ async fn list_installed_fabric_versions(window: Window) -> Result<Vec<String>, S
/// Get Java version requirement for a specific version
#[tauri::command]
async fn get_version_java_version(
- window: Window,
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
version_id: String,
) -> Result<Option<u64>, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Try to load the version JSON to get javaVersion
match core::manifest::load_version(&game_dir, &version_id).await {
@@ -1607,12 +1619,15 @@ struct VersionMetadata {
/// Delete a version (remove version directory)
#[tauri::command]
-async fn delete_version(window: Window, version_id: String) -> Result<(), String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+async fn delete_version(
+ window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ version_id: String,
+) -> Result<(), String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
let version_dir = game_dir.join("versions").join(&version_id);
@@ -1634,14 +1649,14 @@ async fn delete_version(window: Window, version_id: String) -> Result<(), String
/// Get detailed metadata for a specific version
#[tauri::command]
async fn get_version_metadata(
- window: Window,
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
version_id: String,
) -> Result<VersionMetadata, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Initialize metadata
let mut metadata = VersionMetadata {
@@ -1728,12 +1743,14 @@ struct InstalledVersion {
/// List all installed versions from the data directory
/// Simply lists all folders in the versions directory without validation
#[tauri::command]
-async fn list_installed_versions(window: Window) -> Result<Vec<InstalledVersion>, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+async fn list_installed_versions(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<Vec<InstalledVersion>, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
let versions_dir = game_dir.join("versions");
let mut installed = Vec::new();
@@ -1813,15 +1830,15 @@ async fn list_installed_versions(window: Window) -> Result<Vec<InstalledVersion>
/// Check if Fabric is installed for a specific version
#[tauri::command]
async fn is_fabric_installed(
- window: Window,
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
game_version: String,
loader_version: String,
) -> Result<bool, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
Ok(core::fabric::is_fabric_installed(
&game_dir,
@@ -1853,25 +1870,26 @@ async fn get_forge_versions_for_game(
async fn install_forge(
window: Window,
config_state: State<'_, core::config::ConfigState>,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
game_version: String,
forge_version: String,
) -> Result<core::forge::InstalledForgeVersion, String> {
emit_log!(
window,
format!(
- "Installing Forge {} for Minecraft {}...",
- forge_version, game_version
+ "Installing Forge {} for Minecraft {} in instance {}...",
+ forge_version, game_version, instance_id
)
);
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Get Java path from config or detect
let config = config_state.config.lock().unwrap().clone();
+ let app_handle = window.app_handle();
let java_path_str = if !config.java_path.is_empty() && config.java_path != "java" {
config.java_path.clone()
} else {
@@ -2075,6 +2093,85 @@ async fn list_openai_models(
assistant.list_openai_models(&config.assistant).await
}
+// ==================== Instance Management Commands ====================
+
+/// Create a new instance
+#[tauri::command]
+async fn create_instance(
+ window: Window,
+ state: State<'_, core::instance::InstanceState>,
+ name: String,
+) -> Result<core::instance::Instance, String> {
+ let app_handle = window.app_handle();
+ state.create_instance(name, app_handle)
+}
+
+/// Delete an instance
+#[tauri::command]
+async fn delete_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<(), String> {
+ state.delete_instance(&instance_id)
+}
+
+/// Update an instance
+#[tauri::command]
+async fn update_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance: core::instance::Instance,
+) -> Result<(), String> {
+ state.update_instance(instance)
+}
+
+/// Get all instances
+#[tauri::command]
+async fn list_instances(
+ state: State<'_, core::instance::InstanceState>,
+) -> Result<Vec<core::instance::Instance>, String> {
+ Ok(state.list_instances())
+}
+
+/// Get a single instance by ID
+#[tauri::command]
+async fn get_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<core::instance::Instance, String> {
+ state
+ .get_instance(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))
+}
+
+/// Set the active instance
+#[tauri::command]
+async fn set_active_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<(), String> {
+ state.set_active_instance(&instance_id)
+}
+
+/// Get the active instance
+#[tauri::command]
+async fn get_active_instance(
+ state: State<'_, core::instance::InstanceState>,
+) -> Result<Option<core::instance::Instance>, String> {
+ Ok(state.get_active_instance())
+}
+
+/// Duplicate an instance
+#[tauri::command]
+async fn duplicate_instance(
+ window: Window,
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ new_name: String,
+) -> Result<core::instance::Instance, String> {
+ let app_handle = window.app_handle();
+ state.duplicate_instance(&instance_id, new_name, app_handle)
+}
+
#[tauri::command]
async fn assistant_chat_stream(
window: tauri::Window,
@@ -2101,6 +2198,16 @@ fn main() {
let config_state = core::config::ConfigState::new(app.handle());
app.manage(config_state);
+ // Initialize instance state
+ let instance_state = core::instance::InstanceState::new(app.handle());
+
+ // Migrate legacy data if needed
+ if let Err(e) = core::instance::migrate_legacy_data(app.handle(), &instance_state) {
+ eprintln!("[Startup] Warning: Failed to migrate legacy data: {}", e);
+ }
+
+ app.manage(instance_state);
+
// Load saved account on startup
let app_dir = app.path().app_data_dir().unwrap();
let storage = core::account_storage::AccountStorage::new(app_dir);
@@ -2176,7 +2283,16 @@ fn main() {
assistant_chat,
assistant_chat_stream,
list_ollama_models,
- list_openai_models
+ list_openai_models,
+ // Instance management commands
+ create_instance,
+ delete_instance,
+ update_instance,
+ list_instances,
+ get_instance,
+ set_active_instance,
+ get_active_instance,
+ duplicate_instance
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs
index 651d26b..c9ac368 100644
--- a/src-tauri/src/utils/mod.rs
+++ b/src-tauri/src/utils/mod.rs
@@ -1,5 +1,5 @@
-pub mod zip;
pub mod path;
+pub mod zip;
// File system related utility functions
#[allow(dead_code)]
diff --git a/src-tauri/src/utils/path.rs b/src-tauri/src/utils/path.rs
index deaebb5..1db6e5b 100644
--- a/src-tauri/src/utils/path.rs
+++ b/src-tauri/src/utils/path.rs
@@ -59,7 +59,7 @@ pub fn normalize_java_path(java_path: &str) -> Result<PathBuf, String> {
}
}
}
-
+
// If still not found after PATH search, return specific error
if !path.exists() {
return Err(
@@ -80,7 +80,7 @@ pub fn normalize_java_path(java_path: &str) -> Result<PathBuf, String> {
// Canonicalize and strip UNC prefix for clean path
let canonical = std::fs::canonicalize(&path)
.map_err(|e| format!("Failed to resolve Java path '{}': {}", path.display(), e))?;
-
+
Ok(strip_unc_prefix(canonical))
}
@@ -98,7 +98,7 @@ pub fn normalize_java_path(java_path: &str) -> Result<PathBuf, String> {
}
}
}
-
+
// If still not found after PATH search, return specific error
if !path.exists() {
return Err(
@@ -119,7 +119,7 @@ pub fn normalize_java_path(java_path: &str) -> Result<PathBuf, String> {
// Canonicalize to resolve symlinks and get absolute path
let canonical = std::fs::canonicalize(&path)
.map_err(|e| format!("Failed to resolve Java path '{}': {}", path.display(), e))?;
-
+
Ok(strip_unc_prefix(canonical))
}
@@ -196,23 +196,23 @@ mod tests {
fn test_normalize_with_temp_file() {
// Create a temporary file to test with an actual existing path
let temp_dir = std::env::temp_dir();
-
+
#[cfg(target_os = "windows")]
let temp_file = temp_dir.join("test_java_normalize.exe");
#[cfg(not(target_os = "windows"))]
let temp_file = temp_dir.join("test_java_normalize");
-
+
// Create the file
if let Ok(mut file) = fs::File::create(&temp_file) {
let _ = file.write_all(b"#!/bin/sh\necho test");
drop(file);
-
+
// Test normalization
let result = normalize_java_path(temp_file.to_str().unwrap());
-
+
// Clean up
let _ = fs::remove_file(&temp_file);
-
+
// Verify result
assert!(result.is_ok(), "Failed to normalize temp file path");
let normalized = result.unwrap();
@@ -227,12 +227,12 @@ mod tests {
let unc_path = PathBuf::from(r"\\?\C:\Windows\System32\cmd.exe");
let stripped = strip_unc_prefix(unc_path);
assert_eq!(stripped.to_string_lossy(), r"C:\Windows\System32\cmd.exe");
-
+
let normal_path = PathBuf::from(r"C:\Windows\System32\cmd.exe");
let unchanged = strip_unc_prefix(normal_path.clone());
assert_eq!(unchanged, normal_path);
}
-
+
#[cfg(not(target_os = "windows"))]
{
let path = PathBuf::from("/usr/bin/java");