aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-18 13:34:52 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-18 13:34:52 +0800
commit02520ca62ac5e508e8748b2445171be64f459b6c (patch)
treea15a95be5f2f93385df36d6336f53b3f08d07a44
parent53df697ccf90cd13efc985c195dade48920cc0fa (diff)
downloadDropOut-02520ca62ac5e508e8748b2445171be64f459b6c.tar.gz
DropOut-02520ca62ac5e508e8748b2445171be64f459b6c.zip
fix(ci): improve pre-commit fmt hook configuration
- Add pass_filenames: false to fmt hook - Add -- separator for cargo fmt args - Manually format code with cargo fmt
-rw-r--r--.pre-commit-config.yaml7
-rw-r--r--src-tauri/src/core/config.rs36
-rw-r--r--src-tauri/src/core/java.rs15
-rw-r--r--src-tauri/src/core/rules.rs71
-rw-r--r--src-tauri/src/main.rs230
-rw-r--r--ui/src/components/SettingsView.svelte121
-rw-r--r--ui/src/stores/settings.svelte.ts9
-rw-r--r--ui/src/types/index.ts13
8 files changed, 409 insertions, 93 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7e37cac..7fd9d5e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -29,11 +29,12 @@ repos:
rev: v1.2.1
hooks:
- id: fmt
- args: [ --manifest-path src-tauri/Cargo.toml ]
+ args: [ --manifest-path, src-tauri/Cargo.toml, -- ]
files: ^src-tauri/.*\.rs$
+ pass_filenames: false
- id: cargo-check
- args: [ --manifest-path src-tauri/Cargo.toml ]
+ args: [ --manifest-path, src-tauri/Cargo.toml ]
files: ^src-tauri/.*\.rs$
- id: clippy
- args: [ --manifest-path src-tauri/Cargo.toml ]
+ args: [ --manifest-path, src-tauri/Cargo.toml ]
files: ^src-tauri/.*\.rs$
diff --git a/src-tauri/src/core/config.rs b/src-tauri/src/core/config.rs
index 4c4acad..e4b9381 100644
--- a/src-tauri/src/core/config.rs
+++ b/src-tauri/src/core/config.rs
@@ -42,6 +42,34 @@ impl Default for AssistantConfig {
}
}
+/// Feature-gated arguments configuration
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct FeatureFlags {
+ /// Demo user: enables demo-related arguments when rules require it
+ pub demo_user: bool,
+ /// Quick Play: enable quick play arguments
+ pub quick_play_enabled: bool,
+ /// Quick Play singleplayer world path (if provided)
+ pub quick_play_path: Option<String>,
+ /// Quick Play singleplayer flag
+ pub quick_play_singleplayer: bool,
+ /// Quick Play multiplayer server address (optional)
+ pub quick_play_multiplayer_server: Option<String>,
+}
+
+impl Default for FeatureFlags {
+ fn default() -> Self {
+ Self {
+ demo_user: false,
+ quick_play_enabled: false,
+ quick_play_path: None,
+ quick_play_singleplayer: true,
+ quick_play_multiplayer_server: None,
+ }
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct LauncherConfig {
@@ -59,6 +87,11 @@ pub struct LauncherConfig {
pub log_upload_service: String, // "paste.rs" or "pastebin.com"
pub pastebin_api_key: Option<String>,
pub assistant: AssistantConfig,
+ // Storage management
+ pub use_shared_caches: bool, // Use global shared versions/libraries/assets
+ pub keep_legacy_per_instance_storage: bool, // Keep old per-instance caches (no migration)
+ // Feature-gated argument flags
+ pub feature_flags: FeatureFlags,
}
impl Default for LauncherConfig {
@@ -78,6 +111,9 @@ impl Default for LauncherConfig {
log_upload_service: "paste.rs".to_string(),
pastebin_api_key: None,
assistant: AssistantConfig::default(),
+ use_shared_caches: false,
+ keep_legacy_per_instance_storage: true,
+ feature_flags: FeatureFlags::default(),
}
}
}
diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs
index d3e1bb9..2e3c8a7 100644
--- a/src-tauri/src/core/java.rs
+++ b/src-tauri/src/core/java.rs
@@ -855,22 +855,19 @@ fn parse_java_version(version: &str) -> u32 {
// - New format: 17.0.1, 11.0.5+10 (Java 11+)
// - Format with build: 21.0.3+13-Ubuntu-0ubuntu0.24.04.1
// - Format with underscores: 1.8.0_411
-
+
// First, strip build metadata (everything after '+')
let version_only = version.split('+').next().unwrap_or(version);
-
+
// Remove trailing junk (like "-Ubuntu-0ubuntu0.24.04.1")
- let version_only = version_only
- .split('-')
- .next()
- .unwrap_or(version_only);
-
+ let version_only = version_only.split('-').next().unwrap_or(version_only);
+
// Replace underscores with dots (1.8.0_411 -> 1.8.0.411)
let normalized = version_only.replace('_', ".");
-
+
// Split by dots
let parts: Vec<&str> = normalized.split('.').collect();
-
+
if let Some(first) = parts.first() {
if *first == "1" {
// Old format: 1.8.0 -> major is 8
diff --git a/src-tauri/src/core/rules.rs b/src-tauri/src/core/rules.rs
index 10a40b6..781515a 100644
--- a/src-tauri/src/core/rules.rs
+++ b/src-tauri/src/core/rules.rs
@@ -1,7 +1,8 @@
+use crate::core::config::FeatureFlags;
use crate::core::game_version::Rule;
use std::env;
-pub fn is_library_allowed(rules: &Option<Vec<Rule>>) -> bool {
+pub fn is_library_allowed(rules: &Option<Vec<Rule>>, features: Option<&FeatureFlags>) -> bool {
// If no rules, it's allowed by default
let Some(rules) = rules else {
return true;
@@ -39,19 +40,54 @@ pub fn is_library_allowed(rules: &Option<Vec<Rule>>) -> bool {
let mut allowed = false;
for rule in rules {
- if rule_matches(rule) {
+ if rule_matches(rule, features) {
allowed = rule.action == "allow";
}
}
allowed
}
-fn rule_matches(rule: &Rule) -> bool {
- // Feature-based rules (e.g., is_demo_user, has_quick_plays_support, is_quick_play_singleplayer)
- // are not implemented in this launcher, so we return false for any rule that has features.
- // This prevents adding arguments like --demo, --quickPlayPath, --quickPlaySingleplayer, etc.
- if rule.features.is_some() {
- return false;
+fn rule_matches(rule: &Rule, features: Option<&FeatureFlags>) -> bool {
+ // Feature-based rules: apply only if all listed features evaluate to true
+ if let Some(f) = &rule.features {
+ if let Some(map) = f.as_object() {
+ // If no feature flags provided, we cannot satisfy feature rules
+ let ctx = match features {
+ Some(ff) => ff,
+ None => return false,
+ };
+
+ for (key, val) in map.iter() {
+ let required = val.as_bool().unwrap_or(false);
+ // Map known features
+ let actual = match key.as_str() {
+ "is_demo_user" => ctx.demo_user,
+ "has_quick_plays_support" => ctx.quick_play_enabled,
+ "is_quick_play_singleplayer" => {
+ ctx.quick_play_enabled && ctx.quick_play_singleplayer
+ }
+ "is_quick_play_multiplayer" => {
+ ctx.quick_play_enabled
+ && ctx
+ .quick_play_multiplayer_server
+ .as_ref()
+ .map(|s| !s.is_empty())
+ .unwrap_or(false)
+ }
+ _ => false,
+ };
+ if required && !actual {
+ return false;
+ }
+ if !required && actual {
+ // If rule specifies feature must be false, but it's true, do not match
+ return false;
+ }
+ }
+ } else {
+ // Malformed features object
+ return false;
+ }
}
match &rule.os {
@@ -65,28 +101,35 @@ fn rule_matches(rule: &Rule) -> bool {
"windows" => env::consts::OS == "windows",
_ => false, // Unknown OS name in rule
};
-
+
if !os_match {
return false;
}
}
-
+
// Check architecture if specified
if let Some(arch) = &os_rule.arch {
let current_arch = env::consts::ARCH;
- if arch != current_arch && arch != "x86_64" {
- // "x86" is sometimes used for x86_64, but we only match exact arch
+ // Strict match: only exact architecture or known compatibility mapping
+ let compatible = match (arch.as_str(), current_arch) {
+ ("x86_64", "x86_64") => true,
+ ("x86", "x86") => true,
+ ("aarch64", "aarch64") => true,
+ // Treat "x86" not as matching x86_64 (be strict)
+ _ => arch == current_arch,
+ };
+ if !compatible {
return false;
}
}
-
+
// Check version if specified (for OS version compatibility)
if let Some(_version) = &os_rule.version {
// Version checking would require parsing OS version strings
// For now, we accept all versions (conservative approach)
// In the future, parse version and compare
}
-
+
true
}
}
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 4f9071f..6a230c9 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -296,7 +296,12 @@ async fn start_game(
.as_ref()
.ok_or("Version has no downloads information")?;
let client_jar = &downloads.client;
- let mut client_path = game_dir.join("versions");
+ // Use shared caches for versions if enabled
+ let mut client_path = if config.use_shared_caches {
+ app_handle.path().app_data_dir().unwrap().join("versions")
+ } else {
+ game_dir.join("versions")
+ };
client_path.push(&minecraft_version);
client_path.push(format!("{}.jar", minecraft_version));
@@ -309,11 +314,16 @@ async fn start_game(
// --- Libraries ---
println!("Processing libraries...");
- let libraries_dir = game_dir.join("libraries");
+ // Use shared caches for libraries if enabled
+ let libraries_dir = if config.use_shared_caches {
+ app_handle.path().app_data_dir().unwrap().join("libraries")
+ } else {
+ game_dir.join("libraries")
+ };
let mut native_libs_paths = Vec::new(); // Store paths to native jars for extraction
for lib in &version_details.libraries {
- if core::rules::is_library_allowed(&lib.rules) {
+ if core::rules::is_library_allowed(&lib.rules, Some(&config.feature_flags)) {
// 1. Standard Library - check for explicit downloads first
if let Some(downloads) = &lib.downloads {
if let Some(artifact) = &downloads.artifact {
@@ -336,39 +346,53 @@ async fn start_game(
// 2. Native Library (classifiers)
// e.g. "natives-linux": { ... }
if let Some(classifiers) = &downloads.classifiers {
- // Determine the key based on OS
- // Linux usually "natives-linux", Windows "natives-windows", Mac "natives-osx" (or macos)
- let os_key = if cfg!(target_os = "linux") {
- "natives-linux"
+ // Determine candidate keys based on OS and architecture
+ let arch = std::env::consts::ARCH;
+ let mut candidates: Vec<String> = Vec::new();
+ if cfg!(target_os = "linux") {
+ candidates.push("natives-linux".to_string());
+ candidates.push(format!("natives-linux-{}", arch));
+ if arch == "aarch64" {
+ candidates.push("natives-linux-arm64".to_string());
+ }
} else if cfg!(target_os = "windows") {
- "natives-windows"
+ candidates.push("natives-windows".to_string());
+ candidates.push(format!("natives-windows-{}", arch));
} else if cfg!(target_os = "macos") {
- "natives-osx" // or natives-macos? check json
- } else {
- ""
- };
-
- if let Some(native_artifact_value) = classifiers.get(os_key) {
- // Parse it as DownloadArtifact
- if let Ok(native_artifact) =
- serde_json::from_value::<core::game_version::DownloadArtifact>(
- native_artifact_value.clone(),
- )
- {
- let path_str = native_artifact.path.clone().unwrap(); // Natives usually have path
- let mut native_path = libraries_dir.clone();
- native_path.push(&path_str);
-
- download_tasks.push(core::downloader::DownloadTask {
- url: native_artifact.url,
- path: native_path.clone(),
- sha1: native_artifact.sha1,
- sha256: None,
- });
-
- native_libs_paths.push(native_path);
+ candidates.push("natives-osx".to_string());
+ candidates.push("natives-macos".to_string());
+ candidates.push(format!("natives-macos-{}", arch));
+ }
+
+ // Pick the first available classifier key
+ let mut chosen: Option<core::game_version::DownloadArtifact> = None;
+ for key in candidates {
+ if let Some(native_artifact_value) = classifiers.get(&key) {
+ if let Ok(artifact) =
+ serde_json::from_value::<core::game_version::DownloadArtifact>(
+ native_artifact_value.clone(),
+ )
+ {
+ chosen = Some(artifact);
+ break;
+ }
}
}
+
+ if let Some(native_artifact) = chosen {
+ let path_str = native_artifact.path.clone().unwrap(); // Natives usually have path
+ let mut native_path = libraries_dir.clone();
+ native_path.push(&path_str);
+
+ download_tasks.push(core::downloader::DownloadTask {
+ url: native_artifact.url,
+ path: native_path.clone(),
+ sha1: native_artifact.sha1,
+ sha256: None,
+ });
+
+ native_libs_paths.push(native_path);
+ }
}
} else {
// 3. Library without explicit downloads (mod loader libraries)
@@ -392,7 +416,12 @@ async fn start_game(
// --- Assets ---
println!("Fetching asset index...");
- let assets_dir = game_dir.join("assets");
+ // Use shared caches for assets if enabled
+ let assets_dir = if config.use_shared_caches {
+ app_handle.path().app_data_dir().unwrap().join("assets")
+ } else {
+ game_dir.join("assets")
+ };
let objects_dir = assets_dir.join("objects");
let indexes_dir = assets_dir.join("indexes");
@@ -523,7 +552,7 @@ async fn start_game(
// Add libraries
for lib in &version_details.libraries {
- if core::rules::is_library_allowed(&lib.rules) {
+ if core::rules::is_library_allowed(&lib.rules, Some(&config.feature_flags)) {
if let Some(downloads) = &lib.downloads {
// Standard library with explicit downloads
if let Some(artifact) = &downloads.artifact {
@@ -556,7 +585,13 @@ async fn start_game(
// First add arguments from version.json if available
if let Some(args_obj) = &version_details.arguments {
if let Some(jvm_args) = &args_obj.jvm {
- parse_jvm_arguments(jvm_args, &mut args, &natives_path, &classpath);
+ parse_jvm_arguments(
+ jvm_args,
+ &mut args,
+ &natives_path,
+ &classpath,
+ &config.feature_flags,
+ );
}
}
@@ -588,8 +623,18 @@ async fn start_game(
replacements.insert("${assets_index_name}", asset_index.id.clone());
replacements.insert("${auth_uuid}", account.uuid());
replacements.insert("${auth_access_token}", account.access_token());
- replacements.insert("${user_type}", "mojang".to_string());
- replacements.insert("${version_type}", "release".to_string());
+ // Set user_type dynamically: "msa" for Microsoft accounts, "legacy" for offline
+ let user_type = match &account {
+ core::auth::Account::Microsoft(_) => "msa",
+ core::auth::Account::Offline(_) => "legacy",
+ };
+ replacements.insert("${user_type}", user_type.to_string());
+ // Use version_type from version JSON if available, fallback to "release"
+ let version_type_str = version_details
+ .version_type
+ .clone()
+ .unwrap_or_else(|| "release".to_string());
+ replacements.insert("${version_type}", version_type_str);
replacements.insert("${user_properties}", "{}".to_string()); // Correctly pass empty JSON object for user properties
if let Some(minecraft_arguments) = &version_details.minecraft_arguments {
@@ -622,7 +667,10 @@ async fn start_game(
if let Ok(rules) = serde_json::from_value::<Vec<core::game_version::Rule>>(
rules_val.clone(),
) {
- core::rules::is_library_allowed(&Some(rules))
+ core::rules::is_library_allowed(
+ &Some(rules),
+ Some(&config.feature_flags),
+ )
} else {
true // Parse error, assume allow? or disallow.
}
@@ -815,6 +863,7 @@ fn parse_jvm_arguments(
args: &mut Vec<String>,
natives_path: &str,
classpath: &str,
+ feature_flags: &core::config::FeatureFlags,
) {
let mut replacements = std::collections::HashMap::new();
replacements.insert("${natives_directory}", natives_path.to_string());
@@ -840,7 +889,7 @@ fn parse_jvm_arguments(
if let Ok(rules) =
serde_json::from_value::<Vec<core::game_version::Rule>>(rules_val.clone())
{
- core::rules::is_library_allowed(&Some(rules))
+ core::rules::is_library_allowed(&Some(rules), Some(feature_flags))
} else {
false
}
@@ -1049,7 +1098,17 @@ async fn install_version(
.as_ref()
.ok_or("Version has no downloads information")?;
let client_jar = &downloads.client;
- let mut client_path = game_dir.join("versions");
+ // Use shared caches for versions if enabled
+ let mut client_path = if config.use_shared_caches {
+ window
+ .app_handle()
+ .path()
+ .app_data_dir()
+ .unwrap()
+ .join("versions")
+ } else {
+ game_dir.join("versions")
+ };
client_path.push(&minecraft_version);
client_path.push(format!("{}.jar", minecraft_version));
@@ -1061,10 +1120,20 @@ async fn install_version(
});
// --- Libraries ---
- let libraries_dir = game_dir.join("libraries");
+ // Use shared caches for libraries if enabled
+ let libraries_dir = if config.use_shared_caches {
+ window
+ .app_handle()
+ .path()
+ .app_data_dir()
+ .unwrap()
+ .join("libraries")
+ } else {
+ game_dir.join("libraries")
+ };
for lib in &version_details.libraries {
- if core::rules::is_library_allowed(&lib.rules) {
+ if core::rules::is_library_allowed(&lib.rules, Some(&config.feature_flags)) {
if let Some(downloads) = &lib.downloads {
if let Some(artifact) = &downloads.artifact {
let path_str = artifact
@@ -1085,34 +1154,51 @@ async fn install_version(
// Native Library (classifiers)
if let Some(classifiers) = &downloads.classifiers {
- let os_key = if cfg!(target_os = "linux") {
- "natives-linux"
+ // Determine candidate keys based on OS and architecture
+ let arch = std::env::consts::ARCH;
+ let mut candidates: Vec<String> = Vec::new();
+ if cfg!(target_os = "linux") {
+ candidates.push("natives-linux".to_string());
+ candidates.push(format!("natives-linux-{}", arch));
+ if arch == "aarch64" {
+ candidates.push("natives-linux-arm64".to_string());
+ }
} else if cfg!(target_os = "windows") {
- "natives-windows"
+ candidates.push("natives-windows".to_string());
+ candidates.push(format!("natives-windows-{}", arch));
} else if cfg!(target_os = "macos") {
- "natives-osx"
- } else {
- ""
- };
-
- if let Some(native_artifact_value) = classifiers.get(os_key) {
- if let Ok(native_artifact) =
- serde_json::from_value::<core::game_version::DownloadArtifact>(
- native_artifact_value.clone(),
- )
- {
- let path_str = native_artifact.path.clone().unwrap();
- let mut native_path = libraries_dir.clone();
- native_path.push(&path_str);
-
- download_tasks.push(core::downloader::DownloadTask {
- url: native_artifact.url,
- path: native_path.clone(),
- sha1: native_artifact.sha1,
- sha256: None,
- });
+ candidates.push("natives-osx".to_string());
+ candidates.push("natives-macos".to_string());
+ candidates.push(format!("natives-macos-{}", arch));
+ }
+
+ // Pick the first available classifier key
+ let mut chosen: Option<core::game_version::DownloadArtifact> = None;
+ for key in candidates {
+ if let Some(native_artifact_value) = classifiers.get(&key) {
+ if let Ok(artifact) =
+ serde_json::from_value::<core::game_version::DownloadArtifact>(
+ native_artifact_value.clone(),
+ )
+ {
+ chosen = Some(artifact);
+ break;
+ }
}
}
+
+ if let Some(native_artifact) = chosen {
+ let path_str = native_artifact.path.clone().unwrap();
+ let mut native_path = libraries_dir.clone();
+ native_path.push(&path_str);
+
+ download_tasks.push(core::downloader::DownloadTask {
+ url: native_artifact.url,
+ path: native_path.clone(),
+ sha1: native_artifact.sha1,
+ sha256: None,
+ });
+ }
}
} else {
// Library without explicit downloads (mod loader libraries)
@@ -1134,7 +1220,17 @@ async fn install_version(
}
// --- Assets ---
- let assets_dir = game_dir.join("assets");
+ // Use shared caches for assets if enabled
+ let assets_dir = if config.use_shared_caches {
+ window
+ .app_handle()
+ .path()
+ .app_data_dir()
+ .unwrap()
+ .join("assets")
+ } else {
+ game_dir.join("assets")
+ };
let objects_dir = assets_dir.join("objects");
let indexes_dir = assets_dir.join("indexes");
diff --git a/ui/src/components/SettingsView.svelte b/ui/src/components/SettingsView.svelte
index 4de18b3..0e89e25 100644
--- a/ui/src/components/SettingsView.svelte
+++ b/ui/src/components/SettingsView.svelte
@@ -123,6 +123,15 @@
settingsState.settings.custom_background_path = undefined;
settingsState.saveSettings();
}
+
+ async function runMigrationToSharedCaches() {
+ try {
+ await (await import("@tauri-apps/api/core")).invoke("migrate_shared_caches");
+ settingsState.loadSettings();
+ } catch (e) {
+ console.error("Migration failed:", e);
+ }
+ }
</script>
<div class="h-full flex flex-col p-6 overflow-hidden">
@@ -398,6 +407,118 @@
</div>
</div>
+ <!-- Storage & Caches -->
+ <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
+ <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">Storage & Version Caches</h3>
+ <div class="space-y-4">
+ <div class="flex items-center justify-between">
+ <div>
+ <h4 class="text-sm font-medium text-white/90" id="shared-caches-label">Use Shared Caches</h4>
+ <p class="text-xs text-white/40 mt-1">Store versions/libraries/assets in a global cache shared by all instances.</p>
+ </div>
+ <button
+ aria-labelledby="shared-caches-label"
+ onclick={() => { settingsState.settings.use_shared_caches = !settingsState.settings.use_shared_caches; settingsState.saveSettings(); }}
+ class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.use_shared_caches ? 'bg-indigo-500' : 'bg-white/10'}"
+ >
+ <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.use_shared_caches ? 'translate-x-5' : 'translate-x-0'}"></div>
+ </button>
+ </div>
+
+ <div class="flex items-center justify-between">
+ <div>
+ <h4 class="text-sm font-medium text-white/90" id="legacy-storage-label">Keep Legacy Per-Instance Storage</h4>
+ <p class="text-xs text-white/40 mt-1">Do not migrate existing instance caches; keep current layout.</p>
+ </div>
+ <button
+ aria-labelledby="legacy-storage-label"
+ onclick={() => { settingsState.settings.keep_legacy_per_instance_storage = !settingsState.settings.keep_legacy_per_instance_storage; settingsState.saveSettings(); }}
+ class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.keep_legacy_per_instance_storage ? 'bg-indigo-500' : 'bg-white/10'}"
+ >
+ <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.keep_legacy_per_instance_storage ? 'translate-x-5' : 'translate-x-0'}"></div>
+ </button>
+ </div>
+
+ <div class="flex items-center justify-between pt-2 border-t border-white/10">
+ <div>
+ <h4 class="text-sm font-medium text-white/90">Run Migration</h4>
+ <p class="text-xs text-white/40 mt-1">Hard-link or copy existing per-instance caches into the shared cache.</p>
+ </div>
+ <button onclick={runMigrationToSharedCaches} class="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-sm">Migrate Now</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Feature Flags -->
+ <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
+ <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">Feature Flags (Launcher Arguments)</h3>
+ <div class="space-y-4">
+ <div class="flex items-center justify-between">
+ <div>
+ <h4 class="text-sm font-medium text-white/90" id="demo-user-label">Demo User</h4>
+ <p class="text-xs text-white/40 mt-1">Enable demo-related arguments when rules require them.</p>
+ </div>
+ <button
+ aria-labelledby="demo-user-label"
+ onclick={() => { settingsState.settings.feature_flags.demo_user = !settingsState.settings.feature_flags.demo_user; settingsState.saveSettings(); }}
+ class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.feature_flags.demo_user ? 'bg-indigo-500' : 'bg-white/10'}"
+ >
+ <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.feature_flags.demo_user ? 'translate-x-5' : 'translate-x-0'}"></div>
+ </button>
+ </div>
+
+ <div class="flex items-center justify-between">
+ <div>
+ <h4 class="text-sm font-medium text-white/90" id="quick-play-label">Quick Play</h4>
+ <p class="text-xs text-white/40 mt-1">Enable quick play singleplayer/multiplayer arguments.</p>
+ </div>
+ <button
+ aria-labelledby="quick-play-label"
+ onclick={() => { settingsState.settings.feature_flags.quick_play_enabled = !settingsState.settings.feature_flags.quick_play_enabled; settingsState.saveSettings(); }}
+ class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.feature_flags.quick_play_enabled ? 'bg-indigo-500' : 'bg-white/10'}"
+ >
+ <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.feature_flags.quick_play_enabled ? 'translate-x-5' : 'translate-x-0'}"></div>
+ </button>
+ </div>
+
+ {#if settingsState.settings.feature_flags.quick_play_enabled}
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 pl-2 border-l-2 border-white/10">
+ <div>
+ <label class="block text-sm font-medium text-white/70 mb-2">Singleplayer World Path</label>
+ <input
+ type="text"
+ bind:value={settingsState.settings.feature_flags.quick_play_path}
+ placeholder="/path/to/saves/MyWorld"
+ class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors"
+ />
+ </div>
+ <div class="flex items-center justify-between">
+ <div>
+ <h4 class="text-sm font-medium text-white/90" id="qp-singleplayer-label">Prefer Singleplayer</h4>
+ <p class="text-xs text-white/40 mt-1">If enabled, use singleplayer quick play path.</p>
+ </div>
+ <button
+ aria-labelledby="qp-singleplayer-label"
+ onclick={() => { settingsState.settings.feature_flags.quick_play_singleplayer = !settingsState.settings.feature_flags.quick_play_singleplayer; settingsState.saveSettings(); }}
+ class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.feature_flags.quick_play_singleplayer ? 'bg-indigo-500' : 'bg-white/10'}"
+ >
+ <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.feature_flags.quick_play_singleplayer ? 'translate-x-5' : 'translate-x-0'}"></div>
+ </button>
+ </div>
+ <div>
+ <label class="block text-sm font-medium text-white/70 mb-2">Multiplayer Server Address</label>
+ <input
+ type="text"
+ bind:value={settingsState.settings.feature_flags.quick_play_multiplayer_server}
+ placeholder="example.org:25565"
+ class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors"
+ />
+ </div>
+ </div>
+ {/if}
+ </div>
+ </div>
+
<!-- Debug / Logs -->
<div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
<h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">
diff --git a/ui/src/stores/settings.svelte.ts b/ui/src/stores/settings.svelte.ts
index 8a90736..5d20050 100644
--- a/ui/src/stores/settings.svelte.ts
+++ b/ui/src/stores/settings.svelte.ts
@@ -42,6 +42,15 @@ export class SettingsState {
tts_enabled: false,
tts_provider: "disabled",
},
+ use_shared_caches: false,
+ keep_legacy_per_instance_storage: true,
+ feature_flags: {
+ demo_user: false,
+ quick_play_enabled: false,
+ quick_play_path: undefined,
+ quick_play_singleplayer: true,
+ quick_play_multiplayer_server: undefined,
+ },
});
// Convert background path to proper asset URL
diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts
index a5b336e..858ee43 100644
--- a/ui/src/types/index.ts
+++ b/ui/src/types/index.ts
@@ -68,6 +68,19 @@ export interface LauncherConfig {
log_upload_service: "paste.rs" | "pastebin.com";
pastebin_api_key?: string;
assistant: AssistantConfig;
+ // Storage management
+ use_shared_caches: boolean;
+ keep_legacy_per_instance_storage: boolean;
+ // Feature-gated argument flags
+ feature_flags: FeatureFlags;
+}
+
+export interface FeatureFlags {
+ demo_user: boolean;
+ quick_play_enabled: boolean;
+ quick_play_path?: string;
+ quick_play_singleplayer: boolean;
+ quick_play_multiplayer_server?: string;
}
export interface JavaInstallation {