From 277c424f85dca1a0a1dce2ef1a3cb2d076ebf297 Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Tue, 11 Aug 2026 02:01:38 +0300 Subject: [PATCH] feat: android! --- DiamondRuntime | 2 +- public/Skins/counterr.png | Bin 0 -> 1487 bytes src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 4 + src-tauri/gen/android/app/build.gradle.kts | 9 +- src-tauri/gen/android/app/proguard-rules.pro | 38 ++++---- .../android/app/src/main/AndroidManifest.xml | 10 ++- .../java/com/emerald/legacy/MainActivity.kt | 3 + .../app/src/main/res/xml/file_paths.xml | 1 + src-tauri/gen/android/settings.gradle | 2 + src-tauri/src/android_runtime.rs | 83 ++++++++++++++++++ src-tauri/src/commands/game.rs | 60 ++++++++++++- src-tauri/src/lib.rs | 3 + src/components/views/SettingsView.tsx | 22 +++++ src/components/views/SkinsView.tsx | 6 ++ src/hooks/useGameManager.ts | 10 ++- src/services/TauriService.ts | 4 + 17 files changed, 226 insertions(+), 33 deletions(-) create mode 100644 public/Skins/counterr.png create mode 100644 src-tauri/src/android_runtime.rs diff --git a/DiamondRuntime b/DiamondRuntime index d59d524..856d57a 160000 --- a/DiamondRuntime +++ b/DiamondRuntime @@ -1 +1 @@ -Subproject commit d59d52410d7374412504cdc94a778679acc91d99 +Subproject commit 856d57abb29afdfed03a84f53cd48b4fde602063 diff --git a/public/Skins/counterr.png b/public/Skins/counterr.png new file mode 100644 index 0000000000000000000000000000000000000000..edd7d540ababd22325f48809992519c33fa7905a GIT binary patch literal 1487 zcmV;=1u*)FP)r;ORWeZ zHfbybu?QAIDp732Du|U2EDZSLCEzo}KtKZtf+6tiM$Tl<$JujdcJ7b8@4kJx2M#m) zeKWJ)IXk=ey}NfdYvAF}*VBVPZe;#*^Oxv@$ffVT&#d)hp{W9*AxK3bMluHaIB*Qz zdI2JMT;Pc*j3Pmer^AEv-}U`W^f`9jI(h!Bnc!EMWKsE8#JdThWJTZ{Y-u=i#{%C0 zAo{!_IbJbf6UA;~%|W<$W$cc-2lKZ8Vk7K;t;2T!T5kW?0TA1n5aRESzXNc-I5;j) zu!?3sc7}-VnK9xH;JG6(7W|4Tk@GzJXqneJI<7R|^HE3xdfo_*0WXXv(PN54oH-Zn zb6PpZqL2Y#tQ)z^{6Xkqq>JOdk3Ne*Ovm^fU(@3pAB9xF@qRwU!5FwIv=}cAEyc#u zy?ffz6dhYyLt`3e8->hfv!lQM%*SkXo*KY7TCg;iZ4+3?g-{$?@ch$HrPt1$i3c3| zh%v8od_5ks&!RCN$5BY3c+Tn4GoPb*pSv(v3|orCaTa!Xkbb!JD1G+oGm%3dG2}2N z&*jr!)Fn^SWI=3h97&%>Y1;HIF%%%iO|XF(3tVi$6I0?Xjqo(v8b6g~;3o z4i6rWkz2&qQ?cl?Wj+e2zzt9gn+7WSX^PU`<%y^A!3~&Mj-cxlw>tfD;PB{4crx%+if>;Rs z@$VG>8=y}kT(C11s$kB5?v{-K>ta&|@ZNX9&RD2|IRiGn1D3o6`22uq7KlD#$hlx= zEL6dq0qgDnT~K-d=u!Ihi+6jw0rYV$pl3jbngNVy>oGr{Qr7|zd+y~Aa~lF%@I}FE z`}eZ=RSZDkxZf$9MwFfbxGjbZ0R4PQT?^D)2yDR@0V%#@fRA;ufKOh0t4GFlEDQj% zVV!82!e<8Om8ffhnhSv~sMr$S0MFNo`0Dld(@!6Ko{2u=I+g}dG(Re70LXdHH{SWW zw-%`R5ZHnXa&FQ95JI@^X#lrBjyX?>IYR~@7Xn*wLC%r^cdmV$iGHn!MGW9o<}ILo zv=G91N<8R5T?=GxZ39$b$pB@k2oyjA(1H}BV&HZnB^t_)Kf@%i%RpSl-X#$BD#>e0LN`DEX?8bz^ z7A~B7HVyIly)!*}UIl^Nf^GogwedAlaIX~;v|Y)79d_7ZhcUsYI=)X}41LBKb7*d8 z+=&N1yYVrG?*UjWzYsyLB!C}#`Voj0e+b2(sepggdA%(`6z%;`s92Q}Gb=ugsQxxjOe{$ke{za0Bp4ov+tn zLMW|Z&VcTlo)~b_F}eX$-vMji0w;Y3_!Y;?68!(KLWbxcWuC7w9NRWZ27vgDffrrk zsWqVV+H4WJK{ams!Ut+QU{RI~;7gPtMgiN#rVOZ=z}IWr)*V>IfT7z#l{Ek0O_W8vy3}T>cN3 p<4Jl3w0=L(;-Gl;-H;It_#abhpKk~; + public ; +} +-keep class com.emerald.legacy.MainActivity { + public ; +} -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +-dontwarn com.android.org.conscrypt.SSLParametersImpl +-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml index 7a1ca94..90b8a8c 100644 --- a/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + @@ -9,7 +10,9 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.emerald_legacy_launcher" - android:usesCleartextTraffic="${usesCleartextTraffic}"> + android:extractNativeLibs="true" + android:usesCleartextTraffic="${usesCleartextTraffic}" + tools:replace="android:label,android:theme"> + android:grantUriPermissions="true" + tools:replace="android:authorities"> diff --git a/src-tauri/gen/android/app/src/main/java/com/emerald/legacy/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/com/emerald/legacy/MainActivity.kt index d0b415e..3ca8f28 100644 --- a/src-tauri/gen/android/app/src/main/java/com/emerald/legacy/MainActivity.kt +++ b/src-tauri/gen/android/app/src/main/java/com/emerald/legacy/MainActivity.kt @@ -21,5 +21,8 @@ class MainActivity : TauriActivity() { super.onWebViewCreate(webView) webView.settings.useWideViewPort = true webView.settings.loadWithOverviewMode = true + webView.settings.setSupportZoom(false) + webView.settings.builtInZoomControls = false + webView.settings.displayZoomControls = false } } diff --git a/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml index 782d63b..f2c4376 100644 --- a/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml +++ b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml @@ -2,4 +2,5 @@ + diff --git a/src-tauri/gen/android/settings.gradle b/src-tauri/gen/android/settings.gradle index 3939116..9006af6 100644 --- a/src-tauri/gen/android/settings.gradle +++ b/src-tauri/gen/android/settings.gradle @@ -1,3 +1,5 @@ include ':app' +include ':diamondruntime' +project(':diamondruntime').projectDir = new File(settingsDir, '../../../DiamondRuntime/app/app') apply from: 'tauri.settings.gradle' diff --git a/src-tauri/src/android_runtime.rs b/src-tauri/src/android_runtime.rs new file mode 100644 index 0000000..cd64b6c --- /dev/null +++ b/src-tauri/src/android_runtime.rs @@ -0,0 +1,83 @@ +pub enum BridgeAction { + Play, + OpenContainer, + OpenSettings, +} + +pub fn launch_bridge(instance_path: String, action: BridgeAction) -> Result<(), String> { + #[cfg(target_os = "android")] + { + use jni::objects::{JObject, JValue}; + use jni::sys::jint; + use jni::JNIEnv; + use wry::prelude::{dispatch, find_class}; + fn start_bridge( + env: &mut JNIEnv, + activity: &JObject, + action: &str, + instance_path: &str, + ) -> jni::errors::Result<()> { + let bridge_class = + find_class(env, activity, "dev.lcehub.emerald.LauncherBridgeActivity".to_string())?; + let intent_class = env.find_class("android/content/Intent")?; + let intent = env.new_object( + intent_class, + "(Landroid/content/Context;Ljava/lang/Class;)V", + &[(&activity).into(), (&bridge_class).into()], + )?; + + let extra_action = env.new_string("launcher_action")?; + let action_str = env.new_string(action)?; + env.call_method( + &intent, + "putExtra", + "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", + &[(&extra_action).into(), (&action_str).into()], + )?; + + let extra_path = env.new_string("instance_path")?; + let path_str = env.new_string(instance_path)?; + env.call_method( + &intent, + "putExtra", + "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", + &[(&extra_path).into(), (&path_str).into()], + )?; + + env.call_method( + &intent, + "addFlags", + "(I)Landroid/content/Intent;", + &[JValue::Int(0x10000000 as jint)], + )?; + + env.call_method( + activity, + "startActivity", + "(Landroid/content/Intent;)V", + &[(&intent).into()], + )?; + + Ok(()) + } + + let action_str = match action { + BridgeAction::Play => "play", + BridgeAction::OpenContainer => "open", + BridgeAction::OpenSettings => "settings", + } + .to_string(); + dispatch(move |env, activity, _webview| { + if let Err(e) = start_bridge(env, activity, &action_str, &instance_path) { + eprintln!("[android_bridge] failed to start activity: {e}"); + } + }); + + Ok(()) + } + #[cfg(not(target_os = "android"))] + { + let _ = (instance_path, action); + Err("Only supported on Android".into()) + } +} diff --git a/src-tauri/src/commands/game.rs b/src-tauri/src/commands/game.rs index 6660c4e..45361fe 100644 --- a/src-tauri/src/commands/game.rs +++ b/src-tauri/src/commands/game.rs @@ -32,8 +32,31 @@ pub async fn launch_game( ) -> Result<(), String> { #[cfg(target_os = "android")] { - let _ = (app, state, instance_id, servers, extra_args); - return Err("Launching the game is not implemented on Android. -neo".into()); + let _ = (state, extra_args); + let mut servers = servers; + let working_dir = util::get_instance_working_dir(&app, &instance_id); + if !working_dir.join("Minecraft.Client.exe").exists() { + return Err("Game executable not found in instance folder.".into()); + } + + let config_val = config::load_config_raw(app.clone()); + let lce_online = McServer { name: "LCEOnline Game".into(), ip: "127.0.0.1".into(), port: 61000 }; + if !servers.iter().any(|s| s.ip == lce_online.ip && s.port == lce_online.port) { + servers.push(lce_online); + } + if let Some(ref saved) = config_val.saved_servers { + for s in saved { + if !servers.iter().any(|existing| existing.ip == s.ip && existing.port == s.port) { + servers.push(s.clone()); + } + } + } + ensure_server_list(&working_dir, servers); + + crate::android_runtime::launch_bridge( + working_dir.to_string_lossy().to_string(), + crate::android_runtime::BridgeAction::Play, + ) } #[cfg(not(target_os = "android"))] launch_game_desktop(app, state, instance_id, servers, extra_args).await @@ -312,8 +335,37 @@ pub fn check_game_installed(app: AppHandle, instance_id: String) -> bool { #[allow(non_snake_case)] pub fn open_instance_folder(app: AppHandle, instance_id: String) { let folder = util::get_instance_working_dir(&app, &instance_id); - if folder.exists() { - let _ = app.opener().open_path(folder.to_str().unwrap(), None::<&str>); + #[cfg(target_os = "android")] + { + let _ = std::fs::create_dir_all(&folder); + let _ = crate::android_runtime::launch_bridge( + folder.to_string_lossy().to_string(), + crate::android_runtime::BridgeAction::OpenContainer, + ); + } + #[cfg(not(target_os = "android"))] + { + if folder.exists() { + let _ = app.opener().open_path(folder.to_str().unwrap(), None::<&str>); + } + } +} + +#[tauri::command] +#[allow(non_snake_case)] +pub fn open_container_settings(app: AppHandle, instance_id: String) -> Result<(), String> { + #[cfg(target_os = "android")] + { + let folder = util::get_instance_working_dir(&app, &instance_id); + crate::android_runtime::launch_bridge( + folder.to_string_lossy().to_string(), + crate::android_runtime::BridgeAction::OpenSettings, + ) + } + #[cfg(not(target_os = "android"))] + { + let _ = (app, instance_id); + Err("Only supported on Android".into()) } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 357f5cf..e742f25 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,8 @@ mod playtime; mod platform; mod networking; mod workshop_server; +#[cfg(target_os = "android")] +mod android_runtime; mod commands; use std::collections::HashMap; use std::sync::Arc; @@ -81,6 +83,7 @@ pub fn run() { config_cmds::load_config, download::download_and_install, game::open_instance_folder, + game::open_container_settings, download::cancel_download, runners::get_available_runners, config_cmds::get_external_palettes, diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index 5230b11..842f84b 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -43,6 +43,7 @@ const SettingsView = memo(function SettingsView() { setLaunchEnvVars, skipIntro, setSkipIntro, + profile, } = useConfig(); const { currentTrack, skipTrack, tracks, playPressSound, playBackSound } = useAudio(); @@ -323,6 +324,26 @@ const SettingsView = memo(function SettingsView() { setFocusIndex(0); }, }); + if (isAndroid && profile) { + items.push({ + id: "container_settings", + label: "Container Settings", + type: "button", + onClick: () => { + playPressSound(); + TauriService.openContainerSettings(profile).catch(console.error); + }, + }); + items.push({ + id: "open_container", + label: "Open Container", + type: "button", + onClick: () => { + playPressSound(); + TauriService.openInstanceFolder(profile).catch(console.error); + }, + }); + } for (const action of pluginSettingsActions) { items.push({ id: action.id, @@ -582,6 +603,7 @@ const SettingsView = memo(function SettingsView() { launchPrefix, launchEnvVars, skipIntro, + profile, ]); useEffect(() => { diff --git a/src/components/views/SkinsView.tsx b/src/components/views/SkinsView.tsx index 5b12c8b..0dfb3d0 100644 --- a/src/components/views/SkinsView.tsx +++ b/src/components/views/SkinsView.tsx @@ -66,6 +66,12 @@ const DEFAULT_SKINS: SavedSkin[] = [ }, { id: "amy", name: "Amy", url: "/Skins/amy.png", isSlim: true }, //neo: she's the best btw { id: "huckle", name: "Huckle", url: "/Skins/huckle.png", isSlim: true }, + { + id: "counterract", + name: "CounterrAct", + url: "/Skins/counterr.png", + isSlim: true, + }, ]; const HeadPreview = memo(function HeadPreview({ src }: { src: string }) { diff --git a/src/hooks/useGameManager.ts b/src/hooks/useGameManager.ts index 7185811..e40ad6c 100644 --- a/src/hooks/useGameManager.ts +++ b/src/hooks/useGameManager.ts @@ -9,6 +9,7 @@ import { } from "react"; import { TauriService, type CustomEdition } from "../services/TauriService"; import { getCurrentWindow } from "@tauri-apps/api/window"; +import { usePlatform } from "./usePlatform"; import type { Edition } from "../types/edition"; async function imageUrlToBase64(url: string): Promise { @@ -149,6 +150,7 @@ export function useGameManager({ setCustomizations, extraLaunchArgs, }: GameManagerProps) { + const { isAndroid } = usePlatform(); const [installs, setInstalls] = useState([]); const [isGameRunning, setIsGameRunning] = useState(false); const [downloadProgress, setDownloadProgress] = useState< @@ -401,7 +403,7 @@ export function useGameManager({ gameLogRef.current = true; setError(null); setGameLog(log); - getCurrentWindow().unminimize(); + if (!isAndroid) getCurrentWindow().unminimize(); }); return () => { unlistenDownload.then((u) => u()); @@ -410,7 +412,7 @@ export function useGameManager({ unlistenRetry.then((u) => u()); unlistenGameLog.then((u) => u()); }; - }, [customEditions, checkInstalls]); + }, [customEditions, checkInstalls, isAndroid]); const downloadRunner = useCallback( async (name: string, url: string) => { @@ -508,7 +510,7 @@ export function useGameManager({ setError(null); setIsGameRunning(true); try { - getCurrentWindow().minimize(); + if (!isAndroid) getCurrentWindow().minimize(); const currentEdition = editions.find((e) => e.instanceId === profile); await TauriService.launchGame( profile, @@ -537,7 +539,7 @@ export function useGameManager({ } finally { setIsGameRunning(false); } - }, [isGameRunning, profile, extraLaunchArgs]); + }, [isGameRunning, profile, extraLaunchArgs, isAndroid]); const stopGame = useCallback(async () => { try { diff --git a/src/services/TauriService.ts b/src/services/TauriService.ts index c50640d..c13ad33 100644 --- a/src/services/TauriService.ts +++ b/src/services/TauriService.ts @@ -127,6 +127,10 @@ export class TauriService { return invoke("open_instance_folder", { instanceId }); } + static async openContainerSettings(instanceId: string): Promise { + return invoke("open_container_settings", { instanceId }); + } + static async deleteInstance(instanceId: string): Promise { return invoke("delete_instance", { instanceId }); }