feat: android!

This commit is contained in:
neoapps-dev 2026-08-11 02:01:38 +03:00
parent d083ad09d4
commit 277c424f85
17 changed files with 226 additions and 33 deletions

@ -1 +1 @@
Subproject commit d59d52410d7374412504cdc94a778679acc91d99
Subproject commit 856d57abb29afdfed03a84f53cd48b4fde602063

BIN
public/Skins/counterr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

2
src-tauri/Cargo.lock generated
View file

@ -1051,6 +1051,7 @@ dependencies = [
"futures-util",
"http 1.4.0",
"image 0.24.9",
"jni",
"libc",
"lzxd",
"once_cell",
@ -1074,6 +1075,7 @@ dependencies = [
"tokio-util",
"url",
"uuid 1.23.0",
"wry",
]
[[package]]

View file

@ -38,6 +38,10 @@ lzxd = "0.2.6"
uuid = { version = "1", features = ["v4"] }
tauri-plugin-deep-link = "2"
[target.'cfg(target_os = "android")'.dependencies]
wry = { version = "0.54", default-features = false }
jni = "0.21"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = "2"
tauri-plugin-drpc = "*"

View file

@ -35,7 +35,7 @@ android {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.emerald.legacy"
minSdk = 26
targetSdk = 36
targetSdk = 28
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
@ -67,6 +67,12 @@ android {
buildFeatures {
buildConfig = true
}
lint {
disable += "ExpiredTargetSdkVersion"
}
packaging {
resources.excludes += "lib/*/jni/**"
}
}
rust {
@ -74,6 +80,7 @@ rust {
}
dependencies {
implementation(project(":diamondruntime"))
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")

View file

@ -1,21 +1,19 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-keep class dev.lcehub.emerald.** { *; }
-keepclassmembers class dev.lcehub.emerald.** { *; }
-dontwarn dev.lcehub.emerald.**
-keep class com.github.luben.zstd.** { *; }
-keepclassmembers class com.github.luben.zstd.** { *; }
-dontwarn com.github.luben.zstd.**
-keep class com.emerald.legacy.TauriActivity {
public <methods>;
public <fields>;
}
-keep class com.emerald.legacy.MainActivity {
public <methods>;
}
# 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
-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

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<!-- AndroidTV support -->
@ -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">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
@ -32,7 +35,8 @@
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
android:grantUriPermissions="true"
tools:replace="android:authorities">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />

View file

@ -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
}
}

View file

@ -2,4 +2,5 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
<files-path name="files" path="." />
</paths>

View file

@ -1,3 +1,5 @@
include ':app'
include ':diamondruntime'
project(':diamondruntime').projectDir = new File(settingsDir, '../../../DiamondRuntime/app/app')
apply from: 'tauri.settings.gradle'

View file

@ -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())
}
}

View file

@ -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())
}
}

View file

@ -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,

View file

@ -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(() => {

View file

@ -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 }) {

View file

@ -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<string> {
@ -149,6 +150,7 @@ export function useGameManager({
setCustomizations,
extraLaunchArgs,
}: GameManagerProps) {
const { isAndroid } = usePlatform();
const [installs, setInstalls] = useState<string[]>([]);
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 {

View file

@ -127,6 +127,10 @@ export class TauriService {
return invoke("open_instance_folder", { instanceId });
}
static async openContainerSettings(instanceId: string): Promise<void> {
return invoke("open_container_settings", { instanceId });
}
static async deleteInstance(instanceId: string): Promise<void> {
return invoke("delete_instance", { instanceId });
}