mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-08-20 04:27:29 +00:00
feat: android!
This commit is contained in:
parent
d083ad09d4
commit
277c424f85
|
|
@ -1 +1 @@
|
|||
Subproject commit d59d52410d7374412504cdc94a778679acc91d99
|
||||
Subproject commit 856d57abb29afdfed03a84f53cd48b4fde602063
|
||||
BIN
public/Skins/counterr.png
Normal file
BIN
public/Skins/counterr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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 = "*"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
38
src-tauri/gen/android/app/proguard-rules.pro
vendored
38
src-tauri/gen/android/app/proguard-rules.pro
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
include ':app'
|
||||
include ':diamondruntime'
|
||||
project(':diamondruntime').projectDir = new File(settingsDir, '../../../DiamondRuntime/app/app')
|
||||
|
||||
apply from: 'tauri.settings.gradle'
|
||||
|
|
|
|||
83
src-tauri/src/android_runtime.rs
Normal file
83
src-tauri/src/android_runtime.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue