Merge remote-tracking branch 'upstream/main'
1
.github/workflows/build.yml
vendored
@ -60,6 +60,7 @@ jobs:
|
||||
keyStorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
buildToolsVersion: 33.0.0
|
||||
continue-on-error: true
|
||||
|
||||
- name: Rename Signed APK
|
||||
run: |
|
||||
|
4
.gitignore
vendored
@ -6,3 +6,7 @@
|
||||
/app/debug
|
||||
/app/release
|
||||
/local.properties
|
||||
/.vs
|
||||
/.kotlin
|
||||
/app/debug
|
||||
/app/release
|
||||
|
@ -1,8 +1,9 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.kotlinAndroid)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.serialization)
|
||||
}
|
||||
android.buildFeatures.buildConfig true
|
||||
|
||||
android {
|
||||
namespace 'io.github.chinosk.gakumas.localify'
|
||||
@ -14,7 +15,7 @@ android {
|
||||
minSdk 29
|
||||
targetSdk 34
|
||||
versionCode 2
|
||||
versionName "Dev"
|
||||
versionName "v1.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@ -35,38 +36,38 @@ android {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
buildConfigField "boolean", "ENABLE_LOG", "true"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '11'
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
compose true
|
||||
prefab true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.5.1'
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path file('src/main/cpp/CMakeLists.txt')
|
||||
version '3.22.1'
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes.add('/META-INF/{AL2.0,LGPL2.1}')
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
pickFirsts += ['**/libxdl.so', '**/libshadowhook.so']
|
||||
pickFirsts += "**/libxdl.so"
|
||||
pickFirsts += "**/libshadowhook.so"
|
||||
}
|
||||
resources {
|
||||
excludes += "**/META-INF/{AL2.0,LGPL2.1}"
|
||||
excludes += "kotlin/**"
|
||||
excludes += "**.bin"
|
||||
}
|
||||
dataBinding {
|
||||
enable true
|
||||
}
|
||||
|
||||
applicationVariants.configureEach { variant ->
|
||||
@ -80,17 +81,40 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.material)
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.2'
|
||||
implementation platform('androidx.compose:compose-bom:2024.06.00')
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
def composeBom = platform(libs.androidx.compose.bom)
|
||||
implementation(composeBom)
|
||||
androidTestImplementation(composeBom)
|
||||
implementation(libs.androidx.runtime)
|
||||
implementation(libs.androidx.material)
|
||||
implementation(libs.androidx.foundation)
|
||||
implementation(libs.androidx.foundation.layout)
|
||||
implementation(libs.androidx.animation)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
implementation(libs.accompanist.pager)
|
||||
implementation(libs.accompanist.pager.indicators)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
|
||||
implementation 'io.github.hexhacking:xdl:2.1.1'
|
||||
implementation 'com.bytedance.android:shadowhook:1.0.9'
|
||||
compileOnly 'de.robv.android.xposed:api:82'
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.20"
|
||||
implementation 'com.google.code.gson:gson:2.11.0'
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.svg)
|
||||
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.logging.interceptor)
|
||||
|
||||
implementation(libs.xdl)
|
||||
implementation(libs.shadowhook)
|
||||
compileOnly(libs.xposed.api)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
}
|
5
app/proguard-rules.pro
vendored
@ -19,3 +19,8 @@
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep class io.github.chinosk.gakumas.localify.GakumasHookMain {
|
||||
<init>();
|
||||
native <methods>;
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@ -10,6 +12,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.GakumasLocalify"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
|
||||
<meta-data
|
||||
@ -32,7 +35,6 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.GakumasLocalify">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@ -40,6 +42,23 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".TranslucentActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.GakumasLocalify.NoDisplay">
|
||||
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
29
app/src/main/assets/about_contributors_en.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"plugin_repo": "https://github.com/chinosk6/gakuen-imas-localify",
|
||||
"main_contributors": [
|
||||
{
|
||||
"name": "chinosk (Plugin code)",
|
||||
"links": [
|
||||
{"name": "Github", "link": "https://github.com/chinosk6"},
|
||||
{"name": "Bilibili", "link": "https://space.bilibili.com/287061163"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DarwinTree (Translation Workflow)",
|
||||
"links": [
|
||||
{"name": "Github", "link": "https://github.com/darwintree"},
|
||||
{"name": "Bilibili", "link": "https://space.bilibili.com/6069705"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "And all other translators",
|
||||
"links": [
|
||||
{"name": "Github", "link": "https://github.com/chinosk6/GakumasTranslationData/graphs/contributors"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"contrib_img": {
|
||||
"plugin": "https://contrib.rocks/image?repo=chinosk6/gakuen-imas-localify",
|
||||
"translation": "https://contrib.rocks/image?repo=chinosk6/GakumasTranslationData"
|
||||
}
|
||||
}
|
29
app/src/main/assets/about_contributors_zh_cn.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"plugin_repo": "https://github.com/chinosk6/gakuen-imas-localify",
|
||||
"main_contributors": [
|
||||
{
|
||||
"name": "chinosk(插件本体)",
|
||||
"links": [
|
||||
{"name": "Github", "link": "https://github.com/chinosk6"},
|
||||
{"name": "Bilibili", "link": "https://space.bilibili.com/287061163"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DarwinTree(译文工作流)",
|
||||
"links": [
|
||||
{"name": "Github", "link": "https://github.com/darwintree"},
|
||||
{"name": "Bilibili", "link": "https://space.bilibili.com/6069705"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "以及其他所有翻译贡献者",
|
||||
"links": [
|
||||
{"name": "Github", "link": "https://github.com/chinosk6/GakumasTranslationData/graphs/contributors"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"contrib_img": {
|
||||
"plugin": "https://contrib.rocks/image?repo=chinosk6/gakuen-imas-localify",
|
||||
"translation": "https://contrib.rocks/image?repo=chinosk6/GakumasTranslationData"
|
||||
}
|
||||
}
|
@ -24,3 +24,16 @@
|
||||
|
||||
#define WM_KEYDOWN 0
|
||||
#define WM_KEYUP 1
|
||||
|
||||
#define BTN_A 96
|
||||
#define BTN_B 97
|
||||
#define BTN_X 99
|
||||
#define BTN_Y 100
|
||||
#define BTN_LB 102
|
||||
#define BTN_RB 103
|
||||
#define BTN_THUMBL 106
|
||||
#define BTN_THUMBR 107
|
||||
#define BTN_SELECT 109
|
||||
#define BTN_START 108
|
||||
#define BTN_SHARE 130
|
||||
#define BTN_XBOX 110
|
@ -87,7 +87,7 @@ namespace GakumasLocal::HookMain {
|
||||
}
|
||||
|
||||
bool IsNativeObjectAlive(void* obj) {
|
||||
static UnityResolve::Method* IsNativeObjectAliveMtd = NULL;
|
||||
static UnityResolve::Method* IsNativeObjectAliveMtd = nullptr;
|
||||
if (!IsNativeObjectAliveMtd) IsNativeObjectAliveMtd = Il2cppUtils::GetMethod("UnityEngine.CoreModule.dll", "UnityEngine",
|
||||
"Object", "IsNativeObjectAlive");
|
||||
return IsNativeObjectAliveMtd->Invoke<bool>(obj);
|
||||
@ -109,18 +109,18 @@ namespace GakumasLocal::HookMain {
|
||||
return GetResolution->Invoke<Il2cppUtils::Resolution_t>();
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, Unity_set_fieldOfView, (UnityResolve::UnityType::Camera* _this, float value)) {
|
||||
DEFINE_HOOK(void, Unity_set_fieldOfView, (UnityResolve::UnityType::Camera* self, float value)) {
|
||||
if (Config::enableFreeCamera) {
|
||||
if (_this == mainCameraCache) {
|
||||
if (self == mainCameraCache) {
|
||||
value = GKCamera::baseCamera.fov;
|
||||
}
|
||||
}
|
||||
Unity_set_fieldOfView_Orig(_this, value);
|
||||
Unity_set_fieldOfView_Orig(self, value);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(float, Unity_get_fieldOfView, (UnityResolve::UnityType::Camera* _this)) {
|
||||
DEFINE_HOOK(float, Unity_get_fieldOfView, (UnityResolve::UnityType::Camera* self)) {
|
||||
if (Config::enableFreeCamera) {
|
||||
if (_this == mainCameraCache) {
|
||||
if (self == mainCameraCache) {
|
||||
static auto get_orthographic = reinterpret_cast<bool (*)(void*)>(Il2cppUtils::il2cpp_resolve_icall(
|
||||
"UnityEngine.Camera::get_orthographic()"
|
||||
));
|
||||
@ -133,30 +133,30 @@ namespace GakumasLocal::HookMain {
|
||||
// set_orthographic(i, false);
|
||||
Unity_set_fieldOfView_Orig(i, GKCamera::baseCamera.fov);
|
||||
}
|
||||
Unity_set_fieldOfView_Orig(_this, GKCamera::baseCamera.fov);
|
||||
Unity_set_fieldOfView_Orig(self, GKCamera::baseCamera.fov);
|
||||
|
||||
// Log::DebugFmt("main - get_orthographic: %d", get_orthographic(_this));
|
||||
// Log::DebugFmt("main - get_orthographic: %d", get_orthographic(self));
|
||||
return GKCamera::baseCamera.fov;
|
||||
}
|
||||
}
|
||||
return Unity_get_fieldOfView_Orig(_this);
|
||||
return Unity_get_fieldOfView_Orig(self);
|
||||
}
|
||||
|
||||
UnityResolve::UnityType::Transform* cacheTrans = NULL;
|
||||
UnityResolve::UnityType::Transform* cacheTrans = nullptr;
|
||||
UnityResolve::UnityType::Quaternion cacheRotation{};
|
||||
UnityResolve::UnityType::Vector3 cachePosition{};
|
||||
UnityResolve::UnityType::Vector3 cacheForward{};
|
||||
UnityResolve::UnityType::Vector3 cacheLookAt{};
|
||||
|
||||
DEFINE_HOOK(void, Unity_set_rotation_Injected, (UnityResolve::UnityType::Transform* _this, UnityResolve::UnityType::Quaternion* value)) {
|
||||
DEFINE_HOOK(void, Unity_set_rotation_Injected, (UnityResolve::UnityType::Transform* self, UnityResolve::UnityType::Quaternion* value)) {
|
||||
if (Config::enableFreeCamera) {
|
||||
static auto lookat_injected = reinterpret_cast<void (*)(void*_this,
|
||||
static auto lookat_injected = reinterpret_cast<void (*)(void*self,
|
||||
UnityResolve::UnityType::Vector3* worldPosition, UnityResolve::UnityType::Vector3* worldUp)>(
|
||||
Il2cppUtils::il2cpp_resolve_icall(
|
||||
"UnityEngine.Transform::Internal_LookAt_Injected(UnityEngine.Vector3&,UnityEngine.Vector3&)"));
|
||||
static auto worldUp = UnityResolve::UnityType::Vector3(0, 1, 0);
|
||||
|
||||
if (cameraTransformCache == _this) {
|
||||
if (cameraTransformCache == self) {
|
||||
const auto cameraMode = GKCamera::GetCameraMode();
|
||||
if (cameraMode == GKCamera::CameraMode::FIRST_PERSON) {
|
||||
if (cacheTrans && IsNativeObjectAlive(cacheTrans)) {
|
||||
@ -167,7 +167,7 @@ namespace GakumasLocal::HookMain {
|
||||
static GakumasLocal::Misc::FixedSizeQueue<float> recordsY(60);
|
||||
const auto newY = GKCamera::CheckNewY(cacheLookAt, true, recordsY);
|
||||
UnityResolve::UnityType::Vector3 newCacheLookAt{cacheLookAt.x, newY, cacheLookAt.z};
|
||||
lookat_injected(_this, &newCacheLookAt, &worldUp);
|
||||
lookat_injected(self, &newCacheLookAt, &worldUp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -175,25 +175,25 @@ namespace GakumasLocal::HookMain {
|
||||
else if (cameraMode == GKCamera::CameraMode::FOLLOW) {
|
||||
auto newLookAtPos = GKCamera::CalcFollowModeLookAt(cachePosition,
|
||||
GKCamera::followPosOffset, true);
|
||||
lookat_injected(_this, &newLookAtPos, &worldUp);
|
||||
lookat_injected(self, &newLookAtPos, &worldUp);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
auto& origCameraLookat = GKCamera::baseCamera.lookAt;
|
||||
lookat_injected(_this, &origCameraLookat, &worldUp);
|
||||
lookat_injected(self, &origCameraLookat, &worldUp);
|
||||
// Log::DebugFmt("fov: %f, target: %f", Unity_get_fieldOfView_Orig(mainCameraCache), GKCamera::baseCamera.fov);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Unity_set_rotation_Injected_Orig(_this, value);
|
||||
return Unity_set_rotation_Injected_Orig(self, value);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, Unity_set_position_Injected, (UnityResolve::UnityType::Transform* _this, UnityResolve::UnityType::Vector3* data)) {
|
||||
DEFINE_HOOK(void, Unity_set_position_Injected, (UnityResolve::UnityType::Transform* self, UnityResolve::UnityType::Vector3* data)) {
|
||||
if (Config::enableFreeCamera) {
|
||||
CheckAndUpdateMainCamera();
|
||||
|
||||
if (cameraTransformCache == _this) {
|
||||
if (cameraTransformCache == self) {
|
||||
const auto cameraMode = GKCamera::GetCameraMode();
|
||||
if (cameraMode == GKCamera::CameraMode::FIRST_PERSON) {
|
||||
if (cacheTrans && IsNativeObjectAlive(cacheTrans)) {
|
||||
@ -218,16 +218,16 @@ namespace GakumasLocal::HookMain {
|
||||
}
|
||||
}
|
||||
|
||||
return Unity_set_position_Injected_Orig(_this, data);
|
||||
return Unity_set_position_Injected_Orig(self, data);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void*, InternalSetOrientationAsync, (void* _this, int type, void* c, void* tc, void* mtd)) {
|
||||
DEFINE_HOOK(void*, InternalSetOrientationAsync, (void* self, int type, void* c, void* tc, void* mtd)) {
|
||||
switch (Config::gameOrientation) {
|
||||
case 1: type = 0x2; break; // FixedPortrait
|
||||
case 2: type = 0x3; break; // FixedLandscape
|
||||
default: break;
|
||||
}
|
||||
return InternalSetOrientationAsync_Orig(_this, type, c, tc, mtd);
|
||||
return InternalSetOrientationAsync_Orig(self, type, c, tc, mtd);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, EndCameraRendering, (void* ctx, void* camera, void* method)) {
|
||||
@ -248,16 +248,16 @@ namespace GakumasLocal::HookMain {
|
||||
|
||||
std::unordered_map<void*, std::string> loadHistory{};
|
||||
|
||||
DEFINE_HOOK(void*, AssetBundle_LoadAssetAsync, (void* _this, Il2cppString* name, void* type)) {
|
||||
DEFINE_HOOK(void*, AssetBundle_LoadAssetAsync, (void* self, Il2cppString* name, void* type)) {
|
||||
// Log::InfoFmt("AssetBundle_LoadAssetAsync: %s, type: %s", name->ToString().c_str());
|
||||
auto ret = AssetBundle_LoadAssetAsync_Orig(_this, name, type);
|
||||
auto ret = AssetBundle_LoadAssetAsync_Orig(self, name, type);
|
||||
loadHistory.emplace(ret, name->ToString());
|
||||
return ret;
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void*, AssetBundleRequest_GetResult, (void* _this)) {
|
||||
auto result = AssetBundleRequest_GetResult_Orig(_this);
|
||||
if (const auto iter = loadHistory.find(_this); iter != loadHistory.end()) {
|
||||
DEFINE_HOOK(void*, AssetBundleRequest_GetResult, (void* self)) {
|
||||
auto result = AssetBundleRequest_GetResult_Orig(self);
|
||||
if (const auto iter = loadHistory.find(self); iter != loadHistory.end()) {
|
||||
const auto name = iter->second;
|
||||
loadHistory.erase(iter);
|
||||
|
||||
@ -275,25 +275,25 @@ namespace GakumasLocal::HookMain {
|
||||
return ret;
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, I18nHelper_SetUpI18n, (void* _this, Il2cppString* lang, Il2cppString* localizationText, int keyComparison)) {
|
||||
DEFINE_HOOK(void, I18nHelper_SetUpI18n, (void* self, Il2cppString* lang, Il2cppString* localizationText, int keyComparison)) {
|
||||
// Log::InfoFmt("SetUpI18n lang: %s, key: %d text: %s", lang->ToString().c_str(), keyComparison, localizationText->ToString().c_str());
|
||||
// TODO 此处为 dump 原文 csv
|
||||
I18nHelper_SetUpI18n_Orig(_this, lang, localizationText, keyComparison);
|
||||
I18nHelper_SetUpI18n_Orig(self, lang, localizationText, keyComparison);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, I18nHelper_SetValue, (void* _this, Il2cppString* key, Il2cppString* value)) {
|
||||
DEFINE_HOOK(void, I18nHelper_SetValue, (void* self, Il2cppString* key, Il2cppString* value)) {
|
||||
// Log::InfoFmt("I18nHelper_SetValue: %s - %s", key->ToString().c_str(), value->ToString().c_str());
|
||||
std::string local;
|
||||
if (Local::GetI18n(key->ToString(), &local)) {
|
||||
I18nHelper_SetValue_Orig(_this, key, UnityResolve::UnityType::String::New(local));
|
||||
I18nHelper_SetValue_Orig(self, key, UnityResolve::UnityType::String::New(local));
|
||||
return;
|
||||
}
|
||||
Local::DumpI18nItem(key->ToString(), value->ToString());
|
||||
if (Config::textTest) {
|
||||
I18nHelper_SetValue_Orig(_this, key, Il2cppString::New("[I18]" + value->ToString()));
|
||||
I18nHelper_SetValue_Orig(self, key, Il2cppString::New("[I18]" + value->ToString()));
|
||||
}
|
||||
else {
|
||||
I18nHelper_SetValue_Orig(_this, key, value);
|
||||
I18nHelper_SetValue_Orig(self, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,12 +315,12 @@ namespace GakumasLocal::HookMain {
|
||||
updatedFontPtrs.emplace(fontAsset);
|
||||
UpdateFontAssetData->Invoke<void>(fontAsset);
|
||||
}
|
||||
set_font->Invoke<void>(TMP_Text_this, fontAsset);
|
||||
set_font->Invoke<void>(TMP_Textself, fontAsset);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, TMP_Text_PopulateTextBackingArray, (void* _this, UnityResolve::UnityType::String* text, int start, int length)) {
|
||||
DEFINE_HOOK(void, TMP_Text_PopulateTextBackingArray, (void* self, UnityResolve::UnityType::String* text, int start, int length)) {
|
||||
if (!text) {
|
||||
return TMP_Text_PopulateTextBackingArray_Orig(_this, text, start, length);
|
||||
return TMP_Text_PopulateTextBackingArray_Orig(self, text, start, length);
|
||||
}
|
||||
|
||||
static auto Substring = Il2cppUtils::GetMethod("mscorlib.dll", "System", "String", "Substring",
|
||||
@ -330,48 +330,48 @@ namespace GakumasLocal::HookMain {
|
||||
std::string transText;
|
||||
if (Local::GetGenericText(origText, &transText)) {
|
||||
const auto newText = UnityResolve::UnityType::String::New(transText);
|
||||
return TMP_Text_PopulateTextBackingArray_Orig(_this, newText, 0, newText->length);
|
||||
return TMP_Text_PopulateTextBackingArray_Orig(self, newText, 0, newText->length);
|
||||
}
|
||||
|
||||
if (Config::textTest) {
|
||||
TMP_Text_PopulateTextBackingArray_Orig(_this, UnityResolve::UnityType::String::New("[TP]" + text->ToString()), start, length + 4);
|
||||
TMP_Text_PopulateTextBackingArray_Orig(self, UnityResolve::UnityType::String::New("[TP]" + text->ToString()), start, length + 4);
|
||||
}
|
||||
else {
|
||||
TMP_Text_PopulateTextBackingArray_Orig(_this, text, start, length);
|
||||
TMP_Text_PopulateTextBackingArray_Orig(self, text, start, length);
|
||||
}
|
||||
UpdateFont(_this);
|
||||
UpdateFont(self);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, TextMeshProUGUI_Awake, (void* _this, void* method)) {
|
||||
// Log::InfoFmt("TextMeshProUGUI_Awake at %p, _this at %p", TextMeshProUGUI_Awake_Orig, _this);
|
||||
DEFINE_HOOK(void, TextMeshProUGUI_Awake, (void* self, void* method)) {
|
||||
// Log::InfoFmt("TextMeshProUGUI_Awake at %p, self at %p", TextMeshProUGUI_Awake_Orig, self);
|
||||
|
||||
const auto TMP_Text_klass = Il2cppUtils::GetClass("Unity.TextMeshPro.dll",
|
||||
"TMPro", "TMP_Text");
|
||||
const auto get_Text_method = TMP_Text_klass->Get<UnityResolve::Method>("get_text");
|
||||
const auto set_Text_method = TMP_Text_klass->Get<UnityResolve::Method>("set_text");
|
||||
const auto currText = get_Text_method->Invoke<UnityResolve::UnityType::String*>(_this);
|
||||
const auto currText = get_Text_method->Invoke<UnityResolve::UnityType::String*>(self);
|
||||
if (currText) {
|
||||
//Log::InfoFmt("TextMeshProUGUI_Awake: %s", currText->ToString().c_str());
|
||||
std::string transText;
|
||||
if (Local::GetGenericText(currText->ToString(), &transText)) {
|
||||
if (Config::textTest) {
|
||||
set_Text_method->Invoke<void>(_this, UnityResolve::UnityType::String::New("[TA]" + transText));
|
||||
set_Text_method->Invoke<void>(self, UnityResolve::UnityType::String::New("[TA]" + transText));
|
||||
}
|
||||
else {
|
||||
set_Text_method->Invoke<void>(_this, UnityResolve::UnityType::String::New(transText));
|
||||
set_Text_method->Invoke<void>(self, UnityResolve::UnityType::String::New(transText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set_font->Invoke<void>(_this, font);
|
||||
UpdateFont(_this);
|
||||
TextMeshProUGUI_Awake_Orig(_this, method);
|
||||
// set_font->Invoke<void>(self, font);
|
||||
UpdateFont(self);
|
||||
TextMeshProUGUI_Awake_Orig(self, method);
|
||||
}
|
||||
|
||||
// TODO 文本未hook完整
|
||||
DEFINE_HOOK(void, TextField_set_value, (void* _this, Il2cppString* value)) {
|
||||
DEFINE_HOOK(void, TextField_set_value, (void* self, Il2cppString* value)) {
|
||||
Log::DebugFmt("TextField_set_value: %s", value->ToString().c_str());
|
||||
TextField_set_value_Orig(_this, value);
|
||||
TextField_set_value_Orig(self, value);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(Il2cppString*, OctoCaching_GetResourceFileName, (void* data, void* method)) {
|
||||
@ -381,7 +381,7 @@ namespace GakumasLocal::HookMain {
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, OctoResourceLoader_LoadFromCacheOrDownload,
|
||||
(void* _this, Il2cppString* resourceName, void* onComplete, void* onProgress, void* method)) {
|
||||
(void* self, Il2cppString* resourceName, void* onComplete, void* onProgress, void* method)) {
|
||||
|
||||
Log::DebugFmt("OctoResourceLoader_LoadFromCacheOrDownload: %s\n", resourceName->ToString().c_str());
|
||||
|
||||
@ -394,24 +394,24 @@ namespace GakumasLocal::HookMain {
|
||||
const auto onComplete_invoke = reinterpret_cast<void (*)(void*, Il2cppString*, void*)>(
|
||||
onComplete_invoke_mtd->methodPointer
|
||||
);
|
||||
onComplete_invoke(onComplete, UnityResolve::UnityType::String::New(replaceStr), NULL);
|
||||
onComplete_invoke(onComplete, UnityResolve::UnityType::String::New(replaceStr), nullptr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return OctoResourceLoader_LoadFromCacheOrDownload_Orig(_this, resourceName, onComplete, onProgress, method);
|
||||
return OctoResourceLoader_LoadFromCacheOrDownload_Orig(self, resourceName, onComplete, onProgress, method);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, OnDownloadProgress_Invoke, (void* _this, Il2cppString* name, uint64_t receivedLength, uint64_t contentLength)) {
|
||||
DEFINE_HOOK(void, OnDownloadProgress_Invoke, (void* self, Il2cppString* name, uint64_t receivedLength, uint64_t contentLength)) {
|
||||
Log::DebugFmt("OnDownloadProgress_Invoke: %s, %lu/%lu", name->ToString().c_str(), receivedLength, contentLength);
|
||||
OnDownloadProgress_Invoke_Orig(_this, name, receivedLength, contentLength);
|
||||
OnDownloadProgress_Invoke_Orig(self, name, receivedLength, contentLength);
|
||||
}
|
||||
|
||||
// UnHooked
|
||||
DEFINE_HOOK(UnityResolve::UnityType::String*, UI_I18n_GetOrDefault, (void* _this,
|
||||
DEFINE_HOOK(UnityResolve::UnityType::String*, UI_I18n_GetOrDefault, (void* self,
|
||||
UnityResolve::UnityType::String* key, UnityResolve::UnityType::String* defaultKey, void* method)) {
|
||||
|
||||
auto ret = UI_I18n_GetOrDefault_Orig(_this, key, defaultKey, method);
|
||||
auto ret = UI_I18n_GetOrDefault_Orig(self, key, defaultKey, method);
|
||||
|
||||
// Log::DebugFmt("UI_I18n_GetOrDefault: key: %s, default: %s, result: %s", key->ToString().c_str(), defaultKey->ToString().c_str(), ret->ToString().c_str());
|
||||
|
||||
@ -419,16 +419,16 @@ namespace GakumasLocal::HookMain {
|
||||
// return UnityResolve::UnityType::String::New("[I18]" + ret->ToString());
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, PictureBookLiveThumbnailView_SetData, (void* _this, void* liveData, bool isUnlocked, bool isNew)) {
|
||||
DEFINE_HOOK(void, PictureBookLiveThumbnailView_SetData, (void* self, void* liveData, bool isUnlocked, bool isNew, void* ct, void* mtd)) {
|
||||
// Log::DebugFmt("PictureBookLiveThumbnailView_SetData: isUnlocked: %d, isNew: %d", isUnlocked, isNew);
|
||||
if (Config::unlockAllLive) {
|
||||
if (Config::dbgMode && Config::unlockAllLive) {
|
||||
isUnlocked = true;
|
||||
}
|
||||
PictureBookLiveThumbnailView_SetData_Orig(_this, liveData, isUnlocked, isNew);
|
||||
PictureBookLiveThumbnailView_SetData_Orig(self, liveData, isUnlocked, isNew, ct, mtd);
|
||||
}
|
||||
|
||||
bool needRestoreHides = false;
|
||||
DEFINE_HOOK(void*, PictureBookLiveSelectScreenPresenter_MoveLiveScene, (void* _this, void* produceLive,
|
||||
DEFINE_HOOK(void*, PictureBookLiveSelectScreenPresenter_MoveLiveScene, (void* self, void* produceLive,
|
||||
Il2cppString* characterId, Il2cppString* costumeId, Il2cppString* costumeHeadId)) {
|
||||
needRestoreHides = false;
|
||||
Log::InfoFmt("MoveLiveScene: characterId: %s, costumeId: %s, costumeHeadId: %s,",
|
||||
@ -439,18 +439,18 @@ namespace GakumasLocal::HookMain {
|
||||
characterId: shro, costumeId: shro-cstm-0006, costumeHeadId: costume_head_shro-cstm-0006,
|
||||
*/
|
||||
|
||||
if (Config::enableLiveCustomeDress) {
|
||||
if (Config::dbgMode && Config::enableLiveCustomeDress) {
|
||||
// 修改 LiveFixedData_GetCharacter 可以更改 Loading 角色和演唱者名字,而不变更实际登台人
|
||||
return PictureBookLiveSelectScreenPresenter_MoveLiveScene_Orig(_this, produceLive, characterId,
|
||||
return PictureBookLiveSelectScreenPresenter_MoveLiveScene_Orig(self, produceLive, characterId,
|
||||
Config::liveCustomeCostumeId.empty() ? costumeId : Il2cppString::New(Config::liveCustomeCostumeId),
|
||||
Config::liveCustomeHeadId.empty() ? costumeHeadId : Il2cppString::New(Config::liveCustomeHeadId));
|
||||
}
|
||||
|
||||
return PictureBookLiveSelectScreenPresenter_MoveLiveScene_Orig(_this, produceLive, characterId, costumeId, costumeHeadId);
|
||||
return PictureBookLiveSelectScreenPresenter_MoveLiveScene_Orig(self, produceLive, characterId, costumeId, costumeHeadId);
|
||||
}
|
||||
|
||||
// std::string lastMusicId;
|
||||
DEFINE_HOOK(void, PictureBookLiveSelectScreenPresenter_OnSelectMusic, (void* _this, void* itemModel, bool isFirst, void* mtd)) {
|
||||
DEFINE_HOOK(void, PictureBookLiveSelectScreenPresenter_OnSelectMusic, (void* self, void* itemModel, void* ct, void* mtd)) {
|
||||
/* // 修改角色后,Live 结束返回时, itemModel 为 null
|
||||
Log::DebugFmt("OnSelectMusic itemModel at %p", itemModel);
|
||||
|
||||
@ -472,7 +472,7 @@ namespace GakumasLocal::HookMain {
|
||||
auto newItemModel = PictureBookLiveSelectMusicListItemModel_klass->New<void*>();
|
||||
PictureBookLiveSelectMusicListItemModel_ctor->Invoke<void>(newItemModel, music, false);
|
||||
|
||||
return PictureBookLiveSelectScreenPresenter_OnSelectMusic_Orig(_this, newItemModel, isFirst, mtd);
|
||||
return PictureBookLiveSelectScreenPresenter_OnSelectMusic_Orig(self, newItemModel, isFirst, mtd);
|
||||
}
|
||||
|
||||
if (itemModel) {
|
||||
@ -481,23 +481,23 @@ namespace GakumasLocal::HookMain {
|
||||
lastMusicId = musicId->ToString();
|
||||
}*/
|
||||
if (!itemModel) return;
|
||||
return PictureBookLiveSelectScreenPresenter_OnSelectMusic_Orig(_this, itemModel, isFirst, mtd);
|
||||
return PictureBookLiveSelectScreenPresenter_OnSelectMusic_Orig(self, itemModel, ct, mtd);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(bool, VLDOF_IsActive, (void* _this)) {
|
||||
DEFINE_HOOK(bool, VLDOF_IsActive, (void* self)) {
|
||||
if (Config::enableFreeCamera) return false;
|
||||
return VLDOF_IsActive_Orig(_this);
|
||||
return VLDOF_IsActive_Orig(self);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, CampusQualityManager_set_TargetFrameRate, (void* _this, float value)) {
|
||||
DEFINE_HOOK(void, CampusQualityManager_set_TargetFrameRate, (void* self, float value)) {
|
||||
// Log::InfoFmt("CampusQualityManager_set_TargetFrameRate: %f", value);
|
||||
const auto configFps = Config::targetFrameRate;
|
||||
CampusQualityManager_set_TargetFrameRate_Orig(_this, configFps == 0 ? value : (float)configFps);
|
||||
CampusQualityManager_set_TargetFrameRate_Orig(self, configFps == 0 ? value : (float)configFps);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, CampusQualityManager_ApplySetting, (void* _this, int qualitySettingsLevel, int maxBufferPixel, float renderScale, int volumeIndex)) {
|
||||
DEFINE_HOOK(void, CampusQualityManager_ApplySetting, (void* self, int qualitySettingsLevel, int maxBufferPixel, float renderScale, int volumeIndex)) {
|
||||
if (Config::targetFrameRate != 0) {
|
||||
CampusQualityManager_set_TargetFrameRate_Orig(_this, Config::targetFrameRate);
|
||||
CampusQualityManager_set_TargetFrameRate_Orig(self, Config::targetFrameRate);
|
||||
}
|
||||
if (Config::useCustomeGraphicSettings) {
|
||||
static auto SetReflectionQuality = Il2cppUtils::GetMethod("campus-submodule.Runtime.dll", "Campus.Common",
|
||||
@ -516,8 +516,8 @@ namespace GakumasLocal::HookMain {
|
||||
if (Config::lodQualityLevel >= values.size()) Config::lodQualityLevel = values.size() - 1;
|
||||
if (Config::reflectionQualityLevel >= values.size()) Config::reflectionQualityLevel = values.size() - 1;
|
||||
|
||||
SetLODQuality->Invoke<void>(_this, values[Config::lodQualityLevel]);
|
||||
SetReflectionQuality->Invoke<void>(_this, values[Config::reflectionQualityLevel]);
|
||||
SetLODQuality->Invoke<void>(self, values[Config::lodQualityLevel]);
|
||||
SetReflectionQuality->Invoke<void>(self, values[Config::reflectionQualityLevel]);
|
||||
|
||||
qualitySettingsLevel = Config::qualitySettingsLevel;
|
||||
maxBufferPixel = Config::maxBufferPixel;
|
||||
@ -528,7 +528,7 @@ namespace GakumasLocal::HookMain {
|
||||
qualitySettingsLevel , renderScale);
|
||||
}
|
||||
|
||||
CampusQualityManager_ApplySetting_Orig(_this, qualitySettingsLevel, maxBufferPixel, renderScale, volumeIndex);
|
||||
CampusQualityManager_ApplySetting_Orig(self, qualitySettingsLevel, maxBufferPixel, renderScale, volumeIndex);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, UIManager_UpdateRenderTarget, (UnityResolve::UnityType::Vector2 ratio, void* mtd)) {
|
||||
@ -537,10 +537,10 @@ namespace GakumasLocal::HookMain {
|
||||
return UIManager_UpdateRenderTarget_Orig(ratio, mtd);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, VLSRPCameraController_UpdateRenderTarget, (void* _this, int width, int height, bool forceAlpha, void* method)) {
|
||||
DEFINE_HOOK(void, VLSRPCameraController_UpdateRenderTarget, (void* self, int width, int height, bool forceAlpha, void* method)) {
|
||||
// const auto resolution = GetResolution();
|
||||
// Log::DebugFmt("VLSRPCameraController_UpdateRenderTarget: %d, %d", width, height);
|
||||
return VLSRPCameraController_UpdateRenderTarget_Orig(_this, width, height, forceAlpha, method);
|
||||
return VLSRPCameraController_UpdateRenderTarget_Orig(self, width, height, forceAlpha, method);
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void*, VLUtility_GetLimitedResolution, (int32_t screenWidth, int32_t screenHeight,
|
||||
@ -555,8 +555,8 @@ namespace GakumasLocal::HookMain {
|
||||
}
|
||||
|
||||
|
||||
DEFINE_HOOK(void, CampusActorModelParts_OnRegisterBone, (void* _this, Il2cppString** name, UnityResolve::UnityType::Transform* bone)) {
|
||||
CampusActorModelParts_OnRegisterBone_Orig(_this, name, bone);
|
||||
DEFINE_HOOK(void, CampusActorModelParts_OnRegisterBone, (void* self, Il2cppString** name, UnityResolve::UnityType::Transform* bone)) {
|
||||
CampusActorModelParts_OnRegisterBone_Orig(self, name, bone);
|
||||
// Log::DebugFmt("CampusActorModelParts_OnRegisterBone: %s, %p", (*name)->ToString().c_str(), bone);
|
||||
}
|
||||
|
||||
@ -578,6 +578,7 @@ namespace GakumasLocal::HookMain {
|
||||
}
|
||||
|
||||
std::vector<std::string> namesVec{};
|
||||
namesVec.reserve(names.size());
|
||||
for (auto i :names) {
|
||||
namesVec.push_back(i->ToString());
|
||||
}
|
||||
@ -616,7 +617,7 @@ namespace GakumasLocal::HookMain {
|
||||
}
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, CampusActorController_LateUpdate, (void* _this, void* mtd)) {
|
||||
DEFINE_HOOK(void, CampusActorController_LateUpdate, (void* self, void* mtd)) {
|
||||
static auto CampusActorController_klass = Il2cppUtils::GetClass("campus-submodule.Runtime.dll",
|
||||
"Campus.Common", "CampusActorController");
|
||||
static auto rootBody_field = CampusActorController_klass->Get<UnityResolve::Field>("_rootBody");
|
||||
@ -625,10 +626,10 @@ namespace GakumasLocal::HookMain {
|
||||
if (!Config::enableFreeCamera || (GKCamera::GetCameraMode() == GKCamera::CameraMode::FREE)) {
|
||||
if (needRestoreHides) {
|
||||
needRestoreHides = false;
|
||||
HideHead(NULL, false);
|
||||
HideHead(NULL, true);
|
||||
HideHead(nullptr, false);
|
||||
HideHead(nullptr, true);
|
||||
}
|
||||
return CampusActorController_LateUpdate_Orig(_this, mtd);
|
||||
return CampusActorController_LateUpdate_Orig(self, mtd);
|
||||
}
|
||||
|
||||
static auto GetHumanBodyBoneTransform_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(parentKlass, "GetHumanBodyBoneTransform", 1);
|
||||
@ -639,13 +640,13 @@ namespace GakumasLocal::HookMain {
|
||||
static auto get_Index = get_index_mtd ? reinterpret_cast<int (*)(void*)>(
|
||||
get_index_mtd->methodPointer) : [](void*){return 0;};
|
||||
|
||||
const auto currIndex = get_Index(_this);
|
||||
const auto currIndex = get_Index(self);
|
||||
if (currIndex == GKCamera::followCharaIndex) {
|
||||
static auto initPartsSuccess = InitBodyParts();
|
||||
static auto headBodyId = initPartsSuccess ? GKCamera::bodyPartsEnum.GetValueByName("Head") : 0xA;
|
||||
const auto isFirstPerson = GKCamera::GetCameraMode() == GKCamera::CameraMode::FIRST_PERSON;
|
||||
|
||||
auto targetTrans = GetHumanBodyBoneTransform(_this,
|
||||
auto targetTrans = GetHumanBodyBoneTransform(self,
|
||||
isFirstPerson ? headBodyId : GKCamera::bodyPartsEnum.GetCurrent().second);
|
||||
|
||||
if (targetTrans) {
|
||||
@ -655,7 +656,7 @@ namespace GakumasLocal::HookMain {
|
||||
cacheForward = cacheTrans->GetForward();
|
||||
cacheLookAt = cacheTrans->GetPosition() + cacheTrans->GetForward() * 3;
|
||||
|
||||
auto rootBody = Il2cppUtils::ClassGetFieldValue<UnityResolve::UnityType::Transform*>(_this, rootBody_field);
|
||||
auto rootBody = Il2cppUtils::ClassGetFieldValue<UnityResolve::UnityType::Transform*>(self, rootBody_field);
|
||||
auto rootModel = rootBody->GetParent();
|
||||
auto rootModelChildCount = rootModel->GetChildCount();
|
||||
for (int i = 0; i < rootModelChildCount; i++) {
|
||||
@ -677,12 +678,12 @@ namespace GakumasLocal::HookMain {
|
||||
}
|
||||
}
|
||||
else {
|
||||
cacheTrans = NULL;
|
||||
cacheTrans = nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CampusActorController_LateUpdate_Orig(_this, mtd);
|
||||
CampusActorController_LateUpdate_Orig(self, mtd);
|
||||
}
|
||||
|
||||
void UpdateSwingBreastBonesData(void* initializeData) {
|
||||
@ -785,9 +786,9 @@ namespace GakumasLocal::HookMain {
|
||||
// Log::DebugFmt("\n");
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void, CampusActorAnimation_Setup, (void* _this, void* rootTrans, void* initializeData)) {
|
||||
DEFINE_HOOK(void, CampusActorAnimation_Setup, (void* self, void* rootTrans, void* initializeData)) {
|
||||
UpdateSwingBreastBonesData(initializeData);
|
||||
return CampusActorAnimation_Setup_Orig(_this, rootTrans, initializeData);
|
||||
return CampusActorAnimation_Setup_Orig(self, rootTrans, initializeData);
|
||||
}
|
||||
|
||||
void StartInjectFunctions() {
|
||||
@ -833,14 +834,14 @@ namespace GakumasLocal::HookMain {
|
||||
|
||||
ADD_HOOK(PictureBookLiveThumbnailView_SetData,
|
||||
Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame.PictureBook",
|
||||
"PictureBookLiveThumbnailView", "SetData"));
|
||||
"PictureBookLiveThumbnailView", "SetDataAsync", {"*", "*", "*", "*"}));
|
||||
|
||||
ADD_HOOK(PictureBookLiveSelectScreenPresenter_MoveLiveScene,
|
||||
Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame",
|
||||
"PictureBookLiveSelectScreenPresenter", "MoveLiveScene"));
|
||||
ADD_HOOK(PictureBookLiveSelectScreenPresenter_OnSelectMusic,
|
||||
Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame",
|
||||
"PictureBookLiveSelectScreenPresenter", "OnSelectMusic"));
|
||||
"PictureBookLiveSelectScreenPresenter", "OnSelectMusicAsync"));
|
||||
|
||||
ADD_HOOK(VLDOF_IsActive,
|
||||
Il2cppUtils::GetMethodPointer("Unity.RenderPipelines.Universal.Runtime.dll", "VL.Rendering",
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <queue>
|
||||
|
||||
extern JavaVM* g_javaVM;
|
||||
extern jclass g_gakumasHookMainClass;
|
||||
@ -24,9 +25,13 @@ extern jmethodID showToastMethodId;
|
||||
|
||||
|
||||
namespace GakumasLocal::Log {
|
||||
namespace {
|
||||
std::queue<std::string> showingToasts{};
|
||||
}
|
||||
|
||||
std::string StringFormat(const char* fmt, ...) {
|
||||
GetParamStringResult(result);
|
||||
return result.c_str();
|
||||
return result;
|
||||
}
|
||||
|
||||
void Log(int prio, const char* msg) {
|
||||
@ -70,8 +75,8 @@ namespace GakumasLocal::Log {
|
||||
__android_log_write(prio, "GakumasLog", result.c_str());
|
||||
}
|
||||
|
||||
void ShowToast(const std::string& text) {
|
||||
DebugFmt("Toast: %s", text.c_str());
|
||||
void ShowToastJNI(const char* text) {
|
||||
DebugFmt("Toast: %s", text);
|
||||
|
||||
std::thread([text](){
|
||||
auto env = Misc::GetJNIEnv();
|
||||
@ -89,15 +94,50 @@ namespace GakumasLocal::Log {
|
||||
g_javaVM->DetachCurrentThread();
|
||||
return;
|
||||
}
|
||||
jstring param = env->NewStringUTF(text.c_str());
|
||||
jstring param = env->NewStringUTF(text);
|
||||
env->CallStaticVoidMethod(kotlinClass, methodId, param);
|
||||
|
||||
g_javaVM->DetachCurrentThread();
|
||||
}).detach();
|
||||
}
|
||||
|
||||
|
||||
void ShowToast(const std::string& text) {
|
||||
showingToasts.push(text);
|
||||
}
|
||||
|
||||
void ShowToast(const char* text) {
|
||||
DebugFmt("Toast: %s", text);
|
||||
return ShowToast(std::string(text));
|
||||
}
|
||||
|
||||
void ShowToastFmt(const char* fmt, ...) {
|
||||
GetParamStringResult(result);
|
||||
ShowToast(result);
|
||||
}
|
||||
|
||||
std::string GetQueuedToast() {
|
||||
if (showingToasts.empty()) {
|
||||
return "";
|
||||
}
|
||||
const auto ret = showingToasts.front();
|
||||
showingToasts.pop();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ToastLoop(JNIEnv *env, jclass clazz) {
|
||||
const auto toastString = GetQueuedToast();
|
||||
if (toastString.empty()) return;
|
||||
|
||||
static auto _showToastMethodId = env->GetStaticMethodID(clazz, "showToast", "(Ljava/lang/String;)V");
|
||||
|
||||
if (env && clazz && _showToastMethodId) {
|
||||
jstring param = env->NewStringUTF(toastString.c_str());
|
||||
env->CallStaticVoidMethod(clazz, _showToastMethodId, param);
|
||||
env->DeleteLocalRef(param);
|
||||
}
|
||||
else {
|
||||
_showToastMethodId = env->GetStaticMethodID(clazz, "showToast", "(Ljava/lang/String;)V");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
#define GAKUMAS_LOCALIFY_LOG_H
|
||||
|
||||
#include <string>
|
||||
#include <jni.h>
|
||||
|
||||
namespace GakumasLocal::Log {
|
||||
std::string StringFormat(const char* fmt, ...);
|
||||
@ -16,6 +17,8 @@ namespace GakumasLocal::Log {
|
||||
|
||||
void ShowToast(const char* text);
|
||||
void ShowToastFmt(const char* fmt, ...);
|
||||
|
||||
void ToastLoop(JNIEnv *env, jclass clazz);
|
||||
}
|
||||
|
||||
#endif //GAKUMAS_LOCALIFY_LOG_H
|
||||
|
@ -62,14 +62,14 @@ namespace BaseCamera {
|
||||
return lookAt;
|
||||
}
|
||||
|
||||
void Camera::set_lon_move(float vertanglePlus, LonMoveHState moveState) { // 前后移动
|
||||
void Camera::set_lon_move(float vertanglePlus, LonMoveHState moveState, float multiplier) { // 前后移动
|
||||
auto radian = (verticalAngle + vertanglePlus) * M_PI / 180;
|
||||
auto radianH = (double)horizontalAngle * M_PI / 180;
|
||||
|
||||
auto f_step = cos(radian) * moveStep * cos(radianH) / smoothLevel; // ↑↓
|
||||
auto l_step = sin(radian) * moveStep * cos(radianH) / smoothLevel; // ←→
|
||||
auto f_step = cos(radian) * moveStep * cos(radianH) / smoothLevel * multiplier; // ↑↓
|
||||
auto l_step = sin(radian) * moveStep * cos(radianH) / smoothLevel * multiplier; // ←→
|
||||
// auto h_step = tan(radianH) * sqrt(pow(f_step, 2) + pow(l_step, 2));
|
||||
auto h_step = sin(radianH) * moveStep / smoothLevel;
|
||||
auto h_step = sin(radianH) * moveStep / smoothLevel * multiplier;
|
||||
|
||||
switch (moveState)
|
||||
{
|
||||
|
@ -30,7 +30,7 @@ namespace BaseCamera {
|
||||
void setPos(float x, float y, float z);
|
||||
void setLookAt(float x, float y, float z);
|
||||
|
||||
void set_lon_move(float vertanglePlus, LonMoveHState moveState = LonMoveHState::LonMoveLeftAndRight);
|
||||
void set_lon_move(float vertanglePlus, LonMoveHState moveState = LonMoveHState::LonMoveLeftAndRight, float multiplier = 1.0f);
|
||||
void updateVertLook();
|
||||
void setHoriLook(float vertangle);
|
||||
|
||||
|
@ -16,6 +16,9 @@ namespace GKCamera {
|
||||
UnityResolve::UnityType::Vector2 followLookAtOffset{0, 0};
|
||||
float offsetMoveStep = 0.008;
|
||||
int followCharaIndex = 0;
|
||||
float l_sensitivity = 0.5f;
|
||||
float r_sensitivity = 0.5f;
|
||||
bool showToast = true;
|
||||
GakumasLocal::Misc::CSEnum bodyPartsEnum("Head", 0xa);
|
||||
|
||||
// bool rMousePressFlg = false;
|
||||
@ -59,16 +62,16 @@ namespace GKCamera {
|
||||
}
|
||||
|
||||
}
|
||||
void camera_back() { // 后退
|
||||
void camera_back(float multiplier = 1.0f) { // 后退
|
||||
switch (cameraMode) {
|
||||
case CameraMode::FREE: {
|
||||
baseCamera.set_lon_move(180, LonMoveHState::LonMoveBack);
|
||||
baseCamera.set_lon_move(180, LonMoveHState::LonMoveBack, multiplier);
|
||||
} break;
|
||||
case CameraMode::FIRST_PERSON: {
|
||||
firstPersonPosOffset.z -= offsetMoveStep;
|
||||
firstPersonPosOffset.z -= offsetMoveStep * multiplier;
|
||||
} break;
|
||||
case CameraMode::FOLLOW: {
|
||||
followPosOffset.z += offsetMoveStep;
|
||||
followPosOffset.z += offsetMoveStep * multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -86,24 +89,24 @@ namespace GKCamera {
|
||||
}
|
||||
|
||||
}
|
||||
void camera_right() { // 向右
|
||||
void camera_right(float multiplier = 1.0f) { // 向右
|
||||
switch (cameraMode) {
|
||||
case CameraMode::FREE: {
|
||||
baseCamera.set_lon_move(-90);
|
||||
baseCamera.set_lon_move(-90, LonMoveLeftAndRight, multiplier);
|
||||
} break;
|
||||
case CameraMode::FOLLOW: {
|
||||
// followPosOffset.x -= 0.8;
|
||||
followLookAtOffset.x -= offsetMoveStep;
|
||||
followLookAtOffset.x -= offsetMoveStep * multiplier;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void camera_down() { // 向下
|
||||
void camera_down(float multiplier = 1.0f) { // 向下
|
||||
switch (cameraMode) {
|
||||
case CameraMode::FREE: {
|
||||
float preStep = BaseCamera::moveStep / BaseCamera::smoothLevel;
|
||||
float preStep = BaseCamera::moveStep / BaseCamera::smoothLevel * multiplier;
|
||||
|
||||
for (int i = 0; i < BaseCamera::smoothLevel; i++) {
|
||||
baseCamera.pos.y -= preStep;
|
||||
@ -112,19 +115,19 @@ namespace GKCamera {
|
||||
}
|
||||
} break;
|
||||
case CameraMode::FIRST_PERSON: {
|
||||
firstPersonPosOffset.y -= offsetMoveStep;
|
||||
firstPersonPosOffset.y -= offsetMoveStep * multiplier;
|
||||
} break;
|
||||
case CameraMode::FOLLOW: {
|
||||
// followPosOffset.y -= offsetMoveStep;
|
||||
followLookAtOffset.y -= offsetMoveStep;
|
||||
followLookAtOffset.y -= offsetMoveStep * multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void camera_up() { // 向上
|
||||
void camera_up(float multiplier = 1.0f) { // 向上
|
||||
switch (cameraMode) {
|
||||
case CameraMode::FREE: {
|
||||
float preStep = BaseCamera::moveStep / BaseCamera::smoothLevel;
|
||||
float preStep = BaseCamera::moveStep / BaseCamera::smoothLevel * multiplier;
|
||||
|
||||
for (int i = 0; i < BaseCamera::smoothLevel; i++) {
|
||||
baseCamera.pos.y += preStep;
|
||||
@ -133,11 +136,11 @@ namespace GKCamera {
|
||||
}
|
||||
} break;
|
||||
case CameraMode::FIRST_PERSON: {
|
||||
firstPersonPosOffset.y += offsetMoveStep;
|
||||
firstPersonPosOffset.y += offsetMoveStep * multiplier;
|
||||
} break;
|
||||
case CameraMode::FOLLOW: {
|
||||
// followPosOffset.y += offsetMoveStep;
|
||||
followLookAtOffset.y += offsetMoveStep;
|
||||
followLookAtOffset.y += offsetMoveStep * multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -249,6 +252,142 @@ namespace GKCamera {
|
||||
}
|
||||
}
|
||||
|
||||
void ShowToast(const char *text) {
|
||||
if (showToast) {
|
||||
GakumasLocal::Log::ShowToast(text);
|
||||
}
|
||||
}
|
||||
|
||||
void JLThumbRight(float value) {
|
||||
camera_right(value * l_sensitivity * baseCamera.fov / 60);
|
||||
}
|
||||
|
||||
void JLThumbDown(float value) {
|
||||
camera_back(value * l_sensitivity * baseCamera.fov / 60);
|
||||
}
|
||||
|
||||
void JRThumbRight(float value) {
|
||||
cameraLookat_right(value * r_sensitivity * baseCamera.fov / 60);
|
||||
ChangeLiveFollowCameraOffsetX(-1 * value * r_sensitivity * baseCamera.fov / 60);
|
||||
}
|
||||
|
||||
void JRThumbDown(float value) {
|
||||
cameraLookat_down(value * r_sensitivity * baseCamera.fov / 60);
|
||||
ChangeLiveFollowCameraOffsetY(-0.1 * value * r_sensitivity * baseCamera.fov / 60);
|
||||
}
|
||||
|
||||
void JDadUp(){
|
||||
reset_camera();
|
||||
ShowToast("Reset Camera");
|
||||
}
|
||||
|
||||
void JDadDown(){
|
||||
ShowToast("Notification off, click again to turn it on.");
|
||||
showToast = !showToast;
|
||||
}
|
||||
|
||||
void JDadLeft(){
|
||||
l_sensitivity = 1.0f;
|
||||
ShowToast("Reset Movement Sensitivity");
|
||||
}
|
||||
|
||||
void JDadRight(){
|
||||
r_sensitivity = 1.0f;
|
||||
ShowToast("Reset Camera Sensitivity");
|
||||
}
|
||||
|
||||
void JAKeyDown() {
|
||||
if (cameraMode == CameraMode::FOLLOW) {
|
||||
const auto currPart = bodyPartsEnum.Next();
|
||||
if (showToast) {
|
||||
GakumasLocal::Log::ShowToastFmt("Look at: %s (0x%x)", currPart.first.c_str(),
|
||||
currPart.second);
|
||||
}
|
||||
} else {
|
||||
r_sensitivity *= 0.8f;
|
||||
}
|
||||
}
|
||||
|
||||
void JBKeyDown() {
|
||||
if (cameraMode == CameraMode::FOLLOW) {
|
||||
const auto currPart = bodyPartsEnum.Last();
|
||||
if (showToast) {
|
||||
GakumasLocal::Log::ShowToastFmt("Look at: %s (0x%x)", currPart.first.c_str(),
|
||||
currPart.second);
|
||||
}
|
||||
} else {
|
||||
r_sensitivity *= 1.2f;
|
||||
}
|
||||
}
|
||||
|
||||
void JXKeyDown() {
|
||||
if (cameraMode == CameraMode::FOLLOW) {
|
||||
OnLeftDown();
|
||||
if (showToast) {
|
||||
GakumasLocal::Log::ShowToastFmt("Look at position: %d", followCharaIndex);
|
||||
}
|
||||
} else {
|
||||
l_sensitivity *= 0.8f;
|
||||
}
|
||||
}
|
||||
|
||||
void JYKeyDown() {
|
||||
if (cameraMode == CameraMode::FOLLOW) {
|
||||
OnRightDown();
|
||||
if (showToast) {
|
||||
GakumasLocal::Log::ShowToastFmt("Look at position: %d", followCharaIndex);
|
||||
}
|
||||
} else {
|
||||
l_sensitivity *= 1.2f;
|
||||
}
|
||||
}
|
||||
|
||||
void JSelectKeyDown() {
|
||||
switch (cameraMode) {
|
||||
case CameraMode::FREE: {
|
||||
cameraMode = CameraMode::FOLLOW;
|
||||
ShowToast("Follow Mode");
|
||||
} break;
|
||||
case CameraMode::FOLLOW: {
|
||||
cameraMode = CameraMode::FIRST_PERSON;
|
||||
ShowToast("First-person Mode");
|
||||
} break;
|
||||
case CameraMode::FIRST_PERSON: {
|
||||
cameraMode = CameraMode::FREE;
|
||||
ShowToast("Free Mode");
|
||||
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void JStartKeyDown() {
|
||||
switch (cameraMode) {
|
||||
case CameraMode::FIRST_PERSON: {
|
||||
if (firstPersonRoll == FirstPersonRoll::ENABLE_ROLL) {
|
||||
firstPersonRoll = FirstPersonRoll::DISABLE_ROLL;
|
||||
ShowToast("Camera Horizontal Fixed");
|
||||
}
|
||||
else {
|
||||
firstPersonRoll = FirstPersonRoll::ENABLE_ROLL;
|
||||
ShowToast("Camera Horizontal Rollable");
|
||||
}
|
||||
} break;
|
||||
|
||||
case CameraMode::FOLLOW: {
|
||||
if (followModeY == FollowModeY::APPLY_Y) {
|
||||
followModeY = FollowModeY::SMOOTH_Y;
|
||||
ShowToast("Smooth Lift");
|
||||
}
|
||||
else {
|
||||
followModeY = FollowModeY::APPLY_Y;
|
||||
ShowToast("Instant Lift");
|
||||
}
|
||||
} break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
UnityResolve::UnityType::Vector3 CalcPositionFromLookAt(const UnityResolve::UnityType::Vector3& target,
|
||||
const UnityResolve::UnityType::Vector3& offset) {
|
||||
// offset: z 远近, y 高低, x角度
|
||||
@ -350,13 +489,49 @@ namespace GKCamera {
|
||||
bool k = false;
|
||||
bool j = false;
|
||||
bool l = false;
|
||||
float thumb_l_right = 0.0f;
|
||||
float thumb_l_down = 0.0f;
|
||||
bool thumb_l_button = false;
|
||||
float thumb_r_right = 0.0f;
|
||||
float thumb_r_down = 0.0f;
|
||||
bool thumb_r_button = false;
|
||||
bool dpad_up = false;
|
||||
bool dpad_down = false;
|
||||
bool dpad_left = false;
|
||||
bool dpad_right = false;
|
||||
bool a_button = false;
|
||||
bool b_button = false;
|
||||
bool x_button = false;
|
||||
bool y_button = false;
|
||||
bool lb_button = false;
|
||||
float lt_button = 0.0f;
|
||||
bool rb_button = false;
|
||||
float rt_button = 0.0f;
|
||||
bool select_button = false;
|
||||
bool start_button = false;
|
||||
bool share_button = false;
|
||||
bool xbox_button = false;
|
||||
bool threadRunning = false;
|
||||
|
||||
void resetAll() {
|
||||
auto p = reinterpret_cast<bool*>(this);
|
||||
const auto numMembers = sizeof(*this) / sizeof(bool);
|
||||
for (size_t idx = 0; idx < numMembers; ++idx) {
|
||||
p[idx] = false;
|
||||
// 获取当前对象的指针并转换为 unsigned char* 类型
|
||||
unsigned char* p = reinterpret_cast<unsigned char*>(this);
|
||||
|
||||
// 遍历对象的每个字节
|
||||
for (size_t offset = 0; offset < sizeof(*this); ) {
|
||||
if (offset + sizeof(bool) <= sizeof(*this) && reinterpret_cast<bool*>(p + offset) == reinterpret_cast<bool*>(this) + offset / sizeof(bool)) {
|
||||
// 如果当前偏移量适用于 bool 类型,则将其设置为 false
|
||||
*reinterpret_cast<bool*>(p + offset) = false;
|
||||
offset += sizeof(bool);
|
||||
} else if (offset + sizeof(float) <= sizeof(*this) && reinterpret_cast<float*>(p + offset) == reinterpret_cast<float*>(this) + offset / sizeof(float)) {
|
||||
// 如果当前偏移量适用于 float 类型,则将其设置为 0.0
|
||||
*reinterpret_cast<float*>(p + offset) = 0.0f;
|
||||
offset += sizeof(float);
|
||||
} else {
|
||||
// 处理未定义的情况(例如混合类型数组或其他类型成员)
|
||||
// 可以根据实际情况调整逻辑或添加更多类型检查
|
||||
offset += 1; // 跳过一个字节
|
||||
}
|
||||
}
|
||||
}
|
||||
} cameraMoveState;
|
||||
@ -385,6 +560,32 @@ namespace GKCamera {
|
||||
if (cameraMoveState.k) ChangeLiveFollowCameraOffsetY(-offsetMoveStep);
|
||||
if (cameraMoveState.j) ChangeLiveFollowCameraOffsetX(0.8);
|
||||
if (cameraMoveState.l) ChangeLiveFollowCameraOffsetX(-0.8);
|
||||
// 手柄操作响应
|
||||
// 左摇杆
|
||||
if (std::abs(cameraMoveState.thumb_l_right) > 0.1f)
|
||||
JLThumbRight(cameraMoveState.thumb_l_right);
|
||||
if (std::abs(cameraMoveState.thumb_l_down) > 0.1f)
|
||||
JLThumbDown(cameraMoveState.thumb_l_down);
|
||||
// 右摇杆
|
||||
if (std::abs(cameraMoveState.thumb_r_right) > 0.1f)
|
||||
JRThumbRight(cameraMoveState.thumb_r_right);
|
||||
if (std::abs(cameraMoveState.thumb_r_down) > 0.1f)
|
||||
JRThumbDown(cameraMoveState.thumb_r_down);
|
||||
// 左扳机
|
||||
if (std::abs(cameraMoveState.lt_button) > 0.1f)
|
||||
camera_down(cameraMoveState.lt_button * l_sensitivity * baseCamera.fov / 60);
|
||||
// 右扳机
|
||||
if (std::abs(cameraMoveState.rt_button) > 0.1f)
|
||||
camera_up(cameraMoveState.rt_button * l_sensitivity * baseCamera.fov / 60);
|
||||
// 左肩键
|
||||
if (cameraMoveState.lb_button) changeCameraFOV(0.5f * r_sensitivity);
|
||||
// 右肩键
|
||||
if (cameraMoveState.rb_button) changeCameraFOV(-0.5f * r_sensitivity);
|
||||
// 十字键
|
||||
if (cameraMoveState.dpad_up) JDadUp();
|
||||
// if (cameraMoveState.dpad_down) JDadDown();
|
||||
if (cameraMoveState.dpad_left) JDadLeft();
|
||||
if (cameraMoveState.dpad_right) JDadRight();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}).detach();
|
||||
@ -446,11 +647,88 @@ namespace GKCamera {
|
||||
} break;
|
||||
case KEY_F: if (message == WM_KEYDOWN) SwitchCameraMode(); break;
|
||||
case KEY_V: if (message == WM_KEYDOWN) SwitchCameraSubMode(); break;
|
||||
// 手柄操作响应
|
||||
case BTN_A:
|
||||
cameraMoveState.a_button = message == WM_KEYDOWN;
|
||||
if (message == WM_KEYDOWN) JAKeyDown();
|
||||
break;
|
||||
case BTN_B:
|
||||
cameraMoveState.b_button = message == WM_KEYDOWN;
|
||||
if (message == WM_KEYDOWN) JBKeyDown();
|
||||
break;
|
||||
case BTN_X:
|
||||
cameraMoveState.x_button = message == WM_KEYDOWN;
|
||||
if (message == WM_KEYDOWN) JXKeyDown();
|
||||
break;
|
||||
case BTN_Y:
|
||||
cameraMoveState.y_button = message == WM_KEYDOWN;
|
||||
if (message == WM_KEYDOWN) JYKeyDown();
|
||||
break;
|
||||
case BTN_LB:
|
||||
cameraMoveState.lb_button = message == WM_KEYDOWN;
|
||||
break;
|
||||
case BTN_RB:
|
||||
cameraMoveState.rb_button = message == WM_KEYDOWN;
|
||||
break;
|
||||
case BTN_THUMBL:
|
||||
cameraMoveState.thumb_l_button = message == WM_KEYDOWN;
|
||||
break;
|
||||
case BTN_THUMBR:
|
||||
cameraMoveState.thumb_r_button = message == WM_KEYDOWN;
|
||||
break;
|
||||
case BTN_SELECT:
|
||||
cameraMoveState.select_button = message == WM_KEYDOWN;
|
||||
if (message == WM_KEYDOWN) JSelectKeyDown();
|
||||
break;
|
||||
case BTN_START:
|
||||
cameraMoveState.start_button = message == WM_KEYDOWN;
|
||||
if (message == WM_KEYDOWN) JStartKeyDown();
|
||||
break;
|
||||
case BTN_SHARE:
|
||||
cameraMoveState.share_button = message == WM_KEYDOWN;
|
||||
break;
|
||||
case BTN_XBOX:
|
||||
cameraMoveState.xbox_button = message == WM_KEYDOWN;
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
on_cam_rawinput_joystick(JoystickEvent event) {
|
||||
int message = event.getMessage();
|
||||
float leftStickX = event.getLeftStickX();
|
||||
float leftStickY = event.getLeftStickY();
|
||||
float rightStickX = event.getRightStickX();
|
||||
float rightStickY = event.getRightStickY();
|
||||
float leftTrigger = event.getLeftTrigger();
|
||||
float rightTrigger = event.getRightTrigger();
|
||||
float hatX = event.getHatX();
|
||||
float hatY = event.getHatY();
|
||||
|
||||
cameraMoveState.thumb_l_right = (std::abs(leftStickX) > 0.1f) ? leftStickX : 0;
|
||||
cameraMoveState.thumb_l_down = (std::abs(leftStickY) > 0.1f) ? leftStickY : 0;
|
||||
cameraMoveState.thumb_r_right = (std::abs(rightStickX) > 0.1f) ? rightStickX : 0;
|
||||
cameraMoveState.thumb_r_down = (std::abs(rightStickY) > 0.1f) ? rightStickY : 0;
|
||||
cameraMoveState.lt_button = (std::abs(leftTrigger) > 0.1f) ? leftTrigger : 0;
|
||||
cameraMoveState.rt_button = (std::abs(rightTrigger) > 0.1f) ? rightTrigger : 0;
|
||||
cameraMoveState.dpad_up = hatY == -1.0f;
|
||||
cameraMoveState.dpad_down = hatY == 1.0f;
|
||||
cameraMoveState.dpad_left = hatX == -1.0f;
|
||||
cameraMoveState.dpad_right = hatX == 1.0f;
|
||||
|
||||
if (cameraMoveState.dpad_down) {
|
||||
JDadDown();
|
||||
}
|
||||
|
||||
// GakumasLocal::Log::InfoFmt(
|
||||
// "Motion event: action=%d, leftStickX=%.2f, leftStickY=%.2f, rightStickX=%.2f, rightStickY=%.2f, leftTrigger=%.2f, rightTrigger=%.2f, hatX=%.2f, hatY=%.2f",
|
||||
// message, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger,
|
||||
// rightTrigger, hatX, hatY);
|
||||
}
|
||||
|
||||
void initCameraSettings() {
|
||||
reset_camera();
|
||||
cameraRawInputThread();
|
||||
|
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "baseCamera.hpp"
|
||||
#include "Joystick/JoystickEvent.h"
|
||||
|
||||
namespace GKCamera {
|
||||
enum class CameraMode {
|
||||
@ -44,5 +45,6 @@ namespace GKCamera {
|
||||
const bool recordY = false);
|
||||
|
||||
void on_cam_rawinput_keyboard(int message, int key);
|
||||
void on_cam_rawinput_joystick(JoystickEvent event);
|
||||
void initCameraSettings();
|
||||
}
|
||||
|
67
app/src/main/cpp/deps/Joystick/JoystickEvent.h
Normal file
@ -0,0 +1,67 @@
|
||||
//
|
||||
// Created by RanKaeder on 2024/6/18.
|
||||
//
|
||||
|
||||
#ifndef GAKUMAS_LOCALIFY_JOYSTICKEVENT_H
|
||||
#define GAKUMAS_LOCALIFY_JOYSTICKEVENT_H
|
||||
|
||||
class JoystickEvent {
|
||||
public:
|
||||
JoystickEvent(int message, float leftStickX, float leftStickY, float rightStickX,
|
||||
float rightStickY, float leftTrigger, float rightTrigger,
|
||||
float hatX, float hatY)
|
||||
: message(message), leftStickX(leftStickX), leftStickY(leftStickY),
|
||||
rightStickX(rightStickX), rightStickY(rightStickY), leftTrigger(leftTrigger),
|
||||
rightTrigger(rightTrigger), hatX(hatX), hatY(hatY) {
|
||||
}
|
||||
|
||||
// Getter 方法
|
||||
int getMessage() const {
|
||||
return message;
|
||||
}
|
||||
|
||||
float getLeftStickX() const {
|
||||
return leftStickX;
|
||||
}
|
||||
|
||||
float getLeftStickY() const {
|
||||
return leftStickY;
|
||||
}
|
||||
|
||||
float getRightStickX() const {
|
||||
return rightStickX;
|
||||
}
|
||||
|
||||
float getRightStickY() const {
|
||||
return rightStickY;
|
||||
}
|
||||
|
||||
float getLeftTrigger() const {
|
||||
return leftTrigger;
|
||||
}
|
||||
|
||||
float getRightTrigger() const {
|
||||
return rightTrigger;
|
||||
}
|
||||
|
||||
float getHatX() const {
|
||||
return hatX;
|
||||
}
|
||||
|
||||
float getHatY() const {
|
||||
return hatY;
|
||||
}
|
||||
|
||||
private:
|
||||
int message;
|
||||
float leftStickX;
|
||||
float leftStickY;
|
||||
float rightStickX;
|
||||
float rightStickY;
|
||||
float leftTrigger;
|
||||
float rightTrigger;
|
||||
float hatX;
|
||||
float hatY;
|
||||
};
|
||||
|
||||
#endif //GAKUMAS_LOCALIFY_JOYSTICKEVENT_H
|
@ -9,6 +9,7 @@
|
||||
#include "xdl.h"
|
||||
#include "GakumasLocalify/camera/camera.hpp"
|
||||
#include "GakumasLocalify/config/Config.hpp"
|
||||
#include "Joystick/JoystickEvent.h"
|
||||
|
||||
JavaVM* g_javaVM = nullptr;
|
||||
jclass g_gakumasHookMainClass = nullptr;
|
||||
@ -87,6 +88,22 @@ Java_io_github_chinosk_gakumas_localify_GakumasHookMain_keyboardEvent(JNIEnv *en
|
||||
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_io_github_chinosk_gakumas_localify_GakumasHookMain_joystickEvent(JNIEnv *env, jclass clazz,
|
||||
jint action,
|
||||
jfloat leftStickX,
|
||||
jfloat leftStickY,
|
||||
jfloat rightStickX,
|
||||
jfloat rightStickY,
|
||||
jfloat leftTrigger,
|
||||
jfloat rightTrigger,
|
||||
jfloat hatX,
|
||||
jfloat hatY) {
|
||||
JoystickEvent event(action, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger, hatX, hatY);
|
||||
GKCamera::on_cam_rawinput_joystick(event);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_io_github_chinosk_gakumas_localify_GakumasHookMain_loadConfig(JNIEnv *env, jclass clazz,
|
||||
@ -95,3 +112,10 @@ Java_io_github_chinosk_gakumas_localify_GakumasHookMain_loadConfig(JNIEnv *env,
|
||||
const std::string configJson = configJsonStrChars;
|
||||
GakumasLocal::Config::LoadConfig(configJson);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_io_github_chinosk_gakumas_localify_GakumasHookMain_pluginCallbackLooper(JNIEnv *env,
|
||||
jclass clazz) {
|
||||
GakumasLocal::Log::ToastLoop(env, clazz);
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package io.github.chinosk.gakumas.localify
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.json
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfig
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfigSerializer
|
||||
import kotlinx.serialization.SerializationException
|
||||
import java.io.File
|
||||
|
||||
|
||||
interface IHasConfigItems {
|
||||
var config: GakumasConfig
|
||||
var programConfig: ProgramConfig
|
||||
|
||||
fun saveConfig() {} // do nothing
|
||||
}
|
||||
|
||||
interface IConfigurableActivity<T : Activity> : IHasConfigItems
|
||||
|
||||
|
||||
fun <T> T.getConfigContent(): String where T : Activity {
|
||||
val configFile = File(filesDir, "gkms-config.json")
|
||||
return if (configFile.exists()) {
|
||||
configFile.readText()
|
||||
} else {
|
||||
Toast.makeText(this, "检测到第一次启动,初始化配置文件...", Toast.LENGTH_SHORT).show()
|
||||
configFile.writeText("{}")
|
||||
"{}"
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> T.getProgramConfigContent(
|
||||
excludes: List<String> = emptyList(),
|
||||
origProgramConfig: ProgramConfig? = null
|
||||
): String where T : Activity {
|
||||
val configFile = File(filesDir, "localify-config.json")
|
||||
if (excludes.isEmpty()) {
|
||||
return if (configFile.exists()) {
|
||||
configFile.readText()
|
||||
} else {
|
||||
"{}"
|
||||
}
|
||||
} else {
|
||||
return if (origProgramConfig == null) {
|
||||
if (configFile.exists()) {
|
||||
val parsedConfig = json.decodeFromString<ProgramConfig>(configFile.readText())
|
||||
json.encodeToString(ProgramConfigSerializer(excludes), parsedConfig)
|
||||
} else {
|
||||
"{}"
|
||||
}
|
||||
} else {
|
||||
json.encodeToString(ProgramConfigSerializer(excludes), origProgramConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> T.loadConfig() where T : Activity, T : IHasConfigItems {
|
||||
val configStr = getConfigContent()
|
||||
config = try {
|
||||
json.decodeFromString<GakumasConfig>(configStr)
|
||||
} catch (e: SerializationException) {
|
||||
Toast.makeText(this, "配置文件异常: $e", Toast.LENGTH_SHORT).show()
|
||||
GakumasConfig()
|
||||
}
|
||||
saveConfig()
|
||||
|
||||
val programConfigStr = getProgramConfigContent()
|
||||
programConfig = try {
|
||||
json.decodeFromString<ProgramConfig>(programConfigStr)
|
||||
} catch (e: SerializationException) {
|
||||
ProgramConfig()
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
|
||||
val intent = Intent().apply {
|
||||
setClassName(
|
||||
"com.bandainamcoent.idolmaster_gakuen",
|
||||
"com.google.firebase.MessagingUnityPlayerActivity"
|
||||
)
|
||||
putExtra("gkmsData", getConfigContent())
|
||||
putExtra(
|
||||
"localData",
|
||||
getProgramConfigContent(listOf("transRemoteZipUrl", "p"), programConfig)
|
||||
)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
|
||||
val updateFile = File(filesDir, "update_trans.zip")
|
||||
if (updateFile.exists()) {
|
||||
val dirUri = FileProvider.getUriForFile(
|
||||
this,
|
||||
"io.github.chinosk.gakumas.localify.fileprovider",
|
||||
File(updateFile.absolutePath)
|
||||
)
|
||||
intent.setDataAndType(dirUri, "resource/file")
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
@ -1,11 +1,18 @@
|
||||
package io.github.chinosk.gakumas.localify
|
||||
|
||||
import android.view.KeyEvent
|
||||
import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfig
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModel
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModelFactory
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
|
||||
interface ConfigListener {
|
||||
fun onClickStartGame()
|
||||
fun onEnabledChanged(value: Boolean)
|
||||
fun onForceExportResourceChanged(value: Boolean)
|
||||
fun onTextTestChanged(value: Boolean)
|
||||
@ -45,47 +52,75 @@ interface ConfigListener {
|
||||
fun onBUseArmCorrectionChanged(value: Boolean)
|
||||
fun onBUseScaleChanged(value: Boolean)
|
||||
fun onBClickPresetChanged(index: Int)
|
||||
fun onPCheckBuiltInAssetsChanged(value: Boolean)
|
||||
fun onPUseRemoteAssetsChanged(value: Boolean)
|
||||
fun onPCleanLocalAssetsChanged(value: Boolean)
|
||||
fun onPDelRemoteAfterUpdateChanged(value: Boolean)
|
||||
fun onPTransRemoteZipUrlChanged(s: CharSequence, start: Int, before: Int, count: Int)
|
||||
fun mainPageAssetsViewDataUpdate(downloadAbleState: Boolean? = null,
|
||||
downloadProgressState: Float? = null,
|
||||
localResourceVersionState: String? = null,
|
||||
errorString: String? = null)
|
||||
}
|
||||
|
||||
class UserConfigViewModelFactory(private val initialValue: GakumasConfig) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(UserConfigViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return UserConfigViewModel(initialValue) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
|
||||
class UserConfigViewModel(initValue: GakumasConfig) : ViewModel() {
|
||||
val configState = MutableStateFlow(initValue)
|
||||
val config: StateFlow<GakumasConfig> = configState.asStateFlow()
|
||||
}
|
||||
|
||||
|
||||
interface ConfigUpdateListener: ConfigListener {
|
||||
var binding: ActivityMainBinding
|
||||
interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
|
||||
var factory: UserConfigViewModelFactory
|
||||
var viewModel: UserConfigViewModel
|
||||
|
||||
var programConfigFactory: ProgramConfigViewModelFactory
|
||||
var programConfigViewModel: ProgramConfigViewModel
|
||||
|
||||
fun pushKeyEvent(event: KeyEvent): Boolean
|
||||
fun getConfigContent(): String
|
||||
fun checkConfigAndUpdateView()
|
||||
fun saveConfig()
|
||||
fun checkConfigAndUpdateView() {} // do nothing
|
||||
// fun saveConfig()
|
||||
fun saveProgramConfig()
|
||||
|
||||
|
||||
override fun onEnabledChanged(value: Boolean) {
|
||||
binding.config!!.enabled = value
|
||||
config.enabled = value
|
||||
saveConfig()
|
||||
pushKeyEvent(KeyEvent(1145, 29))
|
||||
}
|
||||
|
||||
override fun onForceExportResourceChanged(value: Boolean) {
|
||||
binding.config!!.forceExportResource = value
|
||||
config.forceExportResource = value
|
||||
saveConfig()
|
||||
pushKeyEvent(KeyEvent(1145, 30))
|
||||
}
|
||||
|
||||
override fun onTextTestChanged(value: Boolean) {
|
||||
binding.config!!.textTest = value
|
||||
config.textTest = value
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onDumpTextChanged(value: Boolean) {
|
||||
binding.config!!.dumpText = value
|
||||
config.dumpText = value
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onEnableFreeCameraChanged(value: Boolean) {
|
||||
binding.config!!.enableFreeCamera = value
|
||||
config.enableFreeCamera = value
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onUnlockAllLiveChanged(value: Boolean) {
|
||||
binding.config!!.unlockAllLive = value
|
||||
config.unlockAllLive = value
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
@ -98,7 +133,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
} else {
|
||||
valueStr.toInt()
|
||||
}
|
||||
binding.config!!.targetFrameRate = value
|
||||
config.targetFrameRate = value
|
||||
saveConfig()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -107,22 +142,22 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onLiveCustomeDressChanged(value: Boolean) {
|
||||
binding.config!!.enableLiveCustomeDress = value
|
||||
config.enableLiveCustomeDress = value
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onLiveCustomeCostumeIdChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.liveCustomeCostumeId = s.toString()
|
||||
config.liveCustomeCostumeId = s.toString()
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onUseCustomeGraphicSettingsChanged(value: Boolean) {
|
||||
binding.config!!.useCustomeGraphicSettings = value
|
||||
config.useCustomeGraphicSettings = value
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onRenderScaleChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.renderScale = try {
|
||||
config.renderScale = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -132,7 +167,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onQualitySettingsLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.qualitySettingsLevel = try {
|
||||
config.qualitySettingsLevel = try {
|
||||
s.toString().toInt()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -142,7 +177,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onVolumeIndexChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.volumeIndex = try {
|
||||
config.volumeIndex = try {
|
||||
s.toString().toInt()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -152,7 +187,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onMaxBufferPixelChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.maxBufferPixel = try {
|
||||
config.maxBufferPixel = try {
|
||||
s.toString().toInt()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -162,12 +197,12 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onLiveCustomeHeadIdChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.liveCustomeHeadId = s.toString()
|
||||
config.liveCustomeHeadId = s.toString()
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onReflectionQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.reflectionQualityLevel = try {
|
||||
config.reflectionQualityLevel = try {
|
||||
val value = s.toString().toInt()
|
||||
if (value > 5) 5 else value
|
||||
}
|
||||
@ -178,7 +213,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onLodQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.lodQualityLevel = try {
|
||||
config.lodQualityLevel = try {
|
||||
val value = s.toString().toInt()
|
||||
if (value > 5) 5 else value
|
||||
}
|
||||
@ -191,44 +226,44 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
override fun onChangePresetQuality(level: Int) {
|
||||
when (level) {
|
||||
0 -> {
|
||||
binding.config!!.renderScale = 0.5f
|
||||
binding.config!!.qualitySettingsLevel = 1
|
||||
binding.config!!.volumeIndex = 0
|
||||
binding.config!!.maxBufferPixel = 1024
|
||||
binding.config!!.lodQualityLevel = 1
|
||||
binding.config!!.reflectionQualityLevel = 1
|
||||
config.renderScale = 0.5f
|
||||
config.qualitySettingsLevel = 1
|
||||
config.volumeIndex = 0
|
||||
config.maxBufferPixel = 1024
|
||||
config.lodQualityLevel = 1
|
||||
config.reflectionQualityLevel = 1
|
||||
}
|
||||
1 -> {
|
||||
binding.config!!.renderScale = 0.59f
|
||||
binding.config!!.qualitySettingsLevel = 1
|
||||
binding.config!!.volumeIndex = 1
|
||||
binding.config!!.maxBufferPixel = 1440
|
||||
binding.config!!.lodQualityLevel = 2
|
||||
binding.config!!.reflectionQualityLevel = 2
|
||||
config.renderScale = 0.59f
|
||||
config.qualitySettingsLevel = 1
|
||||
config.volumeIndex = 1
|
||||
config.maxBufferPixel = 1440
|
||||
config.lodQualityLevel = 2
|
||||
config.reflectionQualityLevel = 2
|
||||
}
|
||||
2 -> {
|
||||
binding.config!!.renderScale = 0.67f
|
||||
binding.config!!.qualitySettingsLevel = 2
|
||||
binding.config!!.volumeIndex = 2
|
||||
binding.config!!.maxBufferPixel = 2538
|
||||
binding.config!!.lodQualityLevel = 3
|
||||
binding.config!!.reflectionQualityLevel = 3
|
||||
config.renderScale = 0.67f
|
||||
config.qualitySettingsLevel = 2
|
||||
config.volumeIndex = 2
|
||||
config.maxBufferPixel = 2538
|
||||
config.lodQualityLevel = 3
|
||||
config.reflectionQualityLevel = 3
|
||||
}
|
||||
3 -> {
|
||||
binding.config!!.renderScale = 0.77f
|
||||
binding.config!!.qualitySettingsLevel = 3
|
||||
binding.config!!.volumeIndex = 3
|
||||
binding.config!!.maxBufferPixel = 3384
|
||||
binding.config!!.lodQualityLevel = 4
|
||||
binding.config!!.reflectionQualityLevel = 4
|
||||
config.renderScale = 0.77f
|
||||
config.qualitySettingsLevel = 3
|
||||
config.volumeIndex = 3
|
||||
config.maxBufferPixel = 3384
|
||||
config.lodQualityLevel = 4
|
||||
config.reflectionQualityLevel = 4
|
||||
}
|
||||
4 -> {
|
||||
binding.config!!.renderScale = 1.0f
|
||||
binding.config!!.qualitySettingsLevel = 5
|
||||
binding.config!!.volumeIndex = 4
|
||||
binding.config!!.maxBufferPixel = 8190
|
||||
binding.config!!.lodQualityLevel = 5
|
||||
binding.config!!.reflectionQualityLevel = 5
|
||||
config.renderScale = 1.0f
|
||||
config.qualitySettingsLevel = 5
|
||||
config.volumeIndex = 4
|
||||
config.maxBufferPixel = 8190
|
||||
config.lodQualityLevel = 5
|
||||
config.reflectionQualityLevel = 5
|
||||
}
|
||||
}
|
||||
checkConfigAndUpdateView()
|
||||
@ -236,33 +271,31 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onGameOrientationChanged(checkedId: Int) {
|
||||
when (checkedId) {
|
||||
R.id.radioButtonGameDefault -> binding.config!!.gameOrientation = 0
|
||||
R.id.radioButtonGamePortrait -> binding.config!!.gameOrientation = 1
|
||||
R.id.radioButtonGameLandscape -> binding.config!!.gameOrientation = 2
|
||||
if (checkedId in listOf(0, 1, 2)) {
|
||||
config.gameOrientation = checkedId
|
||||
}
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onEnableBreastParamChanged(value: Boolean) {
|
||||
binding.config!!.enableBreastParam = value
|
||||
config.enableBreastParam = value
|
||||
saveConfig()
|
||||
checkConfigAndUpdateView()
|
||||
}
|
||||
|
||||
override fun onBUseArmCorrectionChanged(value: Boolean) {
|
||||
binding.config!!.bUseArmCorrection = value
|
||||
config.bUseArmCorrection = value
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onBUseScaleChanged(value: Boolean) {
|
||||
binding.config!!.bUseScale = value
|
||||
config.bUseScale = value
|
||||
saveConfig()
|
||||
checkConfigAndUpdateView()
|
||||
}
|
||||
|
||||
override fun onBDampingChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.bDamping = try {
|
||||
config.bDamping = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -272,7 +305,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBStiffnessChanged(s: CharSequence, start: Int, before: Int, count: Int){
|
||||
binding.config!!.bStiffness = try {
|
||||
config.bStiffness = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -282,7 +315,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBSpringChanged(s: CharSequence, start: Int, before: Int, count: Int){
|
||||
binding.config!!.bSpring = try {
|
||||
config.bSpring = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -292,7 +325,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBPendulumChanged(s: CharSequence, start: Int, before: Int, count: Int){
|
||||
binding.config!!.bPendulum = try {
|
||||
config.bPendulum = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -302,7 +335,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBPendulumRangeChanged(s: CharSequence, start: Int, before: Int, count: Int){
|
||||
binding.config!!.bPendulumRange = try {
|
||||
config.bPendulumRange = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -312,7 +345,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBAverageChanged(s: CharSequence, start: Int, before: Int, count: Int){
|
||||
binding.config!!.bAverage = try {
|
||||
config.bAverage = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -322,7 +355,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBRootWeightChanged(s: CharSequence, start: Int, before: Int, count: Int){
|
||||
binding.config!!.bRootWeight = try {
|
||||
config.bRootWeight = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -332,13 +365,13 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBUseLimitChanged(value: Boolean){
|
||||
binding.config!!.bUseLimit = value
|
||||
config.bUseLimit = value
|
||||
saveConfig()
|
||||
checkConfigAndUpdateView()
|
||||
}
|
||||
|
||||
override fun onBLimitXxChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.bLimitXx = try {
|
||||
config.bLimitXx = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -348,7 +381,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBLimitXyChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.bLimitXy = try {
|
||||
config.bLimitXy = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -358,7 +391,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBLimitYxChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.bLimitYx = try {
|
||||
config.bLimitYx = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -368,7 +401,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBLimitYyChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.bLimitYy = try {
|
||||
config.bLimitYy = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -378,7 +411,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBLimitZxChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.bLimitZx = try {
|
||||
config.bLimitZx = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -388,7 +421,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
}
|
||||
|
||||
override fun onBLimitZyChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.bLimitZy = try {
|
||||
config.bLimitZy = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -399,7 +432,7 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
|
||||
|
||||
override fun onBScaleChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
binding.config!!.bScale = try {
|
||||
config.bScale = try {
|
||||
s.toString().toFloat()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
@ -429,30 +462,62 @@ interface ConfigUpdateListener: ConfigListener {
|
||||
1f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f)
|
||||
}
|
||||
|
||||
binding.config!!.bDamping = setData[0]
|
||||
binding.config!!.bStiffness = setData[1]
|
||||
binding.config!!.bSpring = setData[2]
|
||||
binding.config!!.bPendulum = setData[3]
|
||||
binding.config!!.bPendulumRange = setData[4]
|
||||
binding.config!!.bAverage = setData[5]
|
||||
binding.config!!.bRootWeight = setData[6]
|
||||
binding.config!!.bUseLimit = if (setData[7] == 0f) {
|
||||
config.bDamping = setData[0]
|
||||
config.bStiffness = setData[1]
|
||||
config.bSpring = setData[2]
|
||||
config.bPendulum = setData[3]
|
||||
config.bPendulumRange = setData[4]
|
||||
config.bAverage = setData[5]
|
||||
config.bRootWeight = setData[6]
|
||||
config.bUseLimit = if (setData[7] == 0f) {
|
||||
false
|
||||
}
|
||||
else {
|
||||
binding.config!!.bLimitXx = setData[8]
|
||||
binding.config!!.bLimitXy = setData[9]
|
||||
binding.config!!.bLimitYx = setData[10]
|
||||
binding.config!!.bLimitYy = setData[11]
|
||||
binding.config!!.bLimitZx = setData[12]
|
||||
binding.config!!.bLimitZy = setData[13]
|
||||
config.bLimitXx = setData[8]
|
||||
config.bLimitXy = setData[9]
|
||||
config.bLimitYx = setData[10]
|
||||
config.bLimitYy = setData[11]
|
||||
config.bLimitZx = setData[12]
|
||||
config.bLimitZy = setData[13]
|
||||
true
|
||||
}
|
||||
|
||||
binding.config!!.bUseArmCorrection = true
|
||||
config.bUseArmCorrection = true
|
||||
|
||||
checkConfigAndUpdateView()
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onPCheckBuiltInAssetsChanged(value: Boolean) {
|
||||
programConfig.checkBuiltInAssets = value
|
||||
saveProgramConfig()
|
||||
}
|
||||
|
||||
override fun onPUseRemoteAssetsChanged(value: Boolean) {
|
||||
programConfig.useRemoteAssets = value
|
||||
saveProgramConfig()
|
||||
}
|
||||
|
||||
override fun onPCleanLocalAssetsChanged(value: Boolean) {
|
||||
programConfig.cleanLocalAssets = value
|
||||
saveProgramConfig()
|
||||
}
|
||||
|
||||
override fun onPDelRemoteAfterUpdateChanged(value: Boolean) {
|
||||
programConfig.delRemoteAfterUpdate = value
|
||||
saveProgramConfig()
|
||||
}
|
||||
|
||||
override fun onPTransRemoteZipUrlChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
programConfig.transRemoteZipUrl = s.toString()
|
||||
saveProgramConfig()
|
||||
}
|
||||
|
||||
override fun mainPageAssetsViewDataUpdate(downloadAbleState: Boolean?, downloadProgressState: Float?,
|
||||
localResourceVersionState: String?, errorString: String?) {
|
||||
downloadAbleState?.let { programConfigViewModel.downloadAbleState.value = downloadAbleState }
|
||||
downloadProgressState?.let{ programConfigViewModel.downloadProgressState.value = downloadProgressState }
|
||||
localResourceVersionState?.let{ programConfigViewModel.localResourceVersionState.value = localResourceVersionState }
|
||||
errorString?.let{ programConfigViewModel.errorStringState.value = errorString }
|
||||
}
|
||||
}
|
@ -11,21 +11,30 @@ import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.widget.Toast
|
||||
import com.bytedance.shadowhook.ShadowHook
|
||||
import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
|
||||
import de.robv.android.xposed.IXposedHookLoadPackage
|
||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||
import de.robv.android.xposed.XC_MethodHook
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
|
||||
import android.view.KeyEvent
|
||||
import android.widget.Toast
|
||||
import com.google.gson.Gson
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import kotlin.system.measureTimeMillis
|
||||
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.json
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfig
|
||||
|
||||
val TAG = "GakumasLocalify"
|
||||
|
||||
@ -39,8 +48,23 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
private var gkmsDataInited = false
|
||||
|
||||
private var getConfigError: Exception? = null
|
||||
private var externalFilesChecked: Boolean = false
|
||||
|
||||
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
|
||||
// if (lpparam.packageName == "io.github.chinosk.gakumas.localify") {
|
||||
// XposedHelpers.findAndHookMethod(
|
||||
// "io.github.chinosk.gakumas.localify.MainActivity",
|
||||
// lpparam.classLoader,
|
||||
// "showToast",
|
||||
// String::class.java,
|
||||
// object : XC_MethodHook() {
|
||||
// override fun beforeHookedMethod(param: MethodHookParam) {
|
||||
// Log.d(TAG, "beforeHookedMethod hooked: ${param.args}")
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
|
||||
if (lpparam.packageName != targetPackageName) {
|
||||
return
|
||||
}
|
||||
@ -61,6 +85,50 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
}
|
||||
)
|
||||
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.app.Activity",
|
||||
lpparam.classLoader,
|
||||
"dispatchGenericMotionEvent",
|
||||
MotionEvent::class.java,
|
||||
object : XC_MethodHook() {
|
||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||
val motionEvent = param.args[0] as MotionEvent
|
||||
val action = motionEvent.action
|
||||
|
||||
// 左摇杆的X和Y轴
|
||||
val leftStickX = motionEvent.getAxisValue(MotionEvent.AXIS_X)
|
||||
val leftStickY = motionEvent.getAxisValue(MotionEvent.AXIS_Y)
|
||||
|
||||
// 右摇杆的X和Y轴
|
||||
val rightStickX = motionEvent.getAxisValue(MotionEvent.AXIS_Z)
|
||||
val rightStickY = motionEvent.getAxisValue(MotionEvent.AXIS_RZ)
|
||||
|
||||
// 左扳机
|
||||
val leftTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER)
|
||||
|
||||
// 右扳机
|
||||
val rightTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER)
|
||||
|
||||
// 十字键
|
||||
val hatX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X)
|
||||
val hatY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y)
|
||||
|
||||
// 处理摇杆和扳机事件
|
||||
joystickEvent(
|
||||
action,
|
||||
leftStickX,
|
||||
leftStickY,
|
||||
rightStickX,
|
||||
rightStickY,
|
||||
leftTrigger,
|
||||
rightTrigger,
|
||||
hatX,
|
||||
hatY
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val appActivityClass = XposedHelpers.findClass("android.app.Activity", lpparam.classLoader)
|
||||
XposedBridge.hookAllMethods(appActivityClass, "onStart", object : XC_MethodHook() {
|
||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||
@ -118,7 +186,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
requestConfig(app.applicationContext)
|
||||
}
|
||||
|
||||
FilesChecker.initAndCheck(app.filesDir, modulePath)
|
||||
FilesChecker.initDir(app.filesDir, modulePath)
|
||||
initHook(
|
||||
"${app.applicationInfo.nativeLibraryDir}/libil2cpp.so",
|
||||
File(
|
||||
@ -130,23 +198,74 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
alreadyInitialized = true
|
||||
}
|
||||
})
|
||||
|
||||
startLoop()
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun startLoop() {
|
||||
GlobalScope.launch {
|
||||
val interval = 1000L / 30
|
||||
while (isActive) {
|
||||
val timeTaken = measureTimeMillis {
|
||||
pluginCallbackLooper()
|
||||
}
|
||||
delay(interval - timeTaken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initGkmsConfig(activity: Activity) {
|
||||
val intent = activity.intent
|
||||
val gkmsData = intent.getStringExtra("gkmsData")
|
||||
val programData = intent.getStringExtra("localData")
|
||||
if (gkmsData != null) {
|
||||
gkmsDataInited = true
|
||||
val initConfig = try {
|
||||
Gson().fromJson(gkmsData, GakumasConfig::class.java)
|
||||
json.decodeFromString<GakumasConfig>(gkmsData)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val programConfig = try {
|
||||
if (programData == null) {
|
||||
ProgramConfig()
|
||||
} else {
|
||||
json.decodeFromString<ProgramConfig>(programData)
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
// 清理本地文件
|
||||
if (programConfig?.cleanLocalAssets == true) {
|
||||
FilesChecker.cleanAssets()
|
||||
}
|
||||
|
||||
// 检查 files 版本和 assets 版本并更新
|
||||
if (programConfig?.checkBuiltInAssets == true) {
|
||||
FilesChecker.initAndCheck(activity.filesDir, modulePath)
|
||||
}
|
||||
|
||||
// 强制导出 assets 文件
|
||||
if (initConfig?.forceExportResource == true) {
|
||||
FilesChecker.updateFiles()
|
||||
}
|
||||
|
||||
// 使用热更新文件
|
||||
if (programConfig?.useRemoteAssets == true) {
|
||||
val dataUri = intent.data
|
||||
if (dataUri != null) {
|
||||
if (!externalFilesChecked) {
|
||||
externalFilesChecked = true
|
||||
// Log.d(TAG, "dataUri: $dataUri")
|
||||
FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir,
|
||||
programConfig.delRemoteAfterUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadConfig(gkmsData)
|
||||
Log.d(TAG, "gkmsData: $gkmsData")
|
||||
}
|
||||
@ -228,7 +347,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
fun requestConfig(activity: Context) {
|
||||
try {
|
||||
val intent = Intent().apply {
|
||||
setClassName("io.github.chinosk.gakumas.localify", "io.github.chinosk.gakumas.localify.MainActivity")
|
||||
setClassName("io.github.chinosk.gakumas.localify", "io.github.chinosk.gakumas.localify.TranslucentActivity")
|
||||
putExtra("gkmsData", "requestConfig")
|
||||
flags = FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
@ -256,8 +375,23 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
@JvmStatic
|
||||
external fun keyboardEvent(keyCode: Int, action: Int)
|
||||
@JvmStatic
|
||||
external fun joystickEvent(
|
||||
action: Int,
|
||||
leftStickX: Float,
|
||||
leftStickY: Float,
|
||||
rightStickX: Float,
|
||||
rightStickY: Float,
|
||||
leftTrigger: Float,
|
||||
rightTrigger: Float,
|
||||
hatX: Float,
|
||||
hatY: Float
|
||||
)
|
||||
@JvmStatic
|
||||
external fun loadConfig(configJsonStr: String)
|
||||
|
||||
// Toast快速切换内容
|
||||
private var toast: Toast? = null
|
||||
|
||||
@JvmStatic
|
||||
fun showToast(message: String) {
|
||||
val app = AndroidAppHelper.currentApplication()
|
||||
@ -265,13 +399,21 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
if (context != null) {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
handler.post {
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
// 取消之前的 Toast
|
||||
toast?.cancel()
|
||||
// 创建新的 Toast
|
||||
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
|
||||
// 展示新的 Toast
|
||||
toast?.show()
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.e(TAG, "showToast: $message failed: applicationContext is null")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
external fun pluginCallbackLooper()
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -1,165 +1,198 @@
|
||||
package io.github.chinosk.gakumas.localify
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
|
||||
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
|
||||
import io.github.chinosk.gakumas.localify.hookUtils.MainKeyEventDispatcher
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.json
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfig
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModel
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModelFactory
|
||||
import io.github.chinosk.gakumas.localify.ui.pages.MainUI
|
||||
import io.github.chinosk.gakumas.localify.ui.theme.GakumasLocalifyTheme
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.File
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity(), ConfigUpdateListener {
|
||||
override lateinit var binding: ActivityMainBinding
|
||||
private val TAG = "GakumasLocalify"
|
||||
class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableActivity<MainActivity> {
|
||||
override lateinit var config: GakumasConfig
|
||||
override lateinit var programConfig: ProgramConfig
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
override lateinit var factory: UserConfigViewModelFactory
|
||||
override lateinit var viewModel: UserConfigViewModel
|
||||
|
||||
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
|
||||
loadConfig()
|
||||
binding.listener = this
|
||||
override lateinit var programConfigFactory: ProgramConfigViewModelFactory
|
||||
override lateinit var programConfigViewModel: ProgramConfigViewModel
|
||||
|
||||
val requestData = intent.getStringExtra("gkmsData")
|
||||
if (requestData != null) {
|
||||
if (requestData == "requestConfig") {
|
||||
onClickStartGame()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
showVersion()
|
||||
|
||||
val scrollView: ScrollView = findViewById(R.id.scrollView)
|
||||
scrollView.viewTreeObserver.addOnScrollChangedListener { onScrollChanged() }
|
||||
onScrollChanged()
|
||||
|
||||
val coordinatorLayout = findViewById<View>(R.id.coordinatorLayout)
|
||||
coordinatorLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
onScrollChanged()
|
||||
coordinatorLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onClickStartGame() {
|
||||
val intent = Intent().apply {
|
||||
setClassName("com.bandainamcoent.idolmaster_gakuen", "com.google.firebase.MessagingUnityPlayerActivity")
|
||||
putExtra("gkmsData", getConfigContent())
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun onScrollChanged() {
|
||||
val fab: FloatingActionButton = findViewById(R.id.fabStartGame)
|
||||
val startGameButton: MaterialButton = findViewById(R.id.StartGameButton)
|
||||
val scrollView: ScrollView = findViewById(R.id.scrollView)
|
||||
|
||||
val location = IntArray(2)
|
||||
startGameButton.getLocationOnScreen(location)
|
||||
val buttonTop = location[1]
|
||||
val buttonBottom = buttonTop + startGameButton.height
|
||||
|
||||
val scrollViewLocation = IntArray(2)
|
||||
scrollView.getLocationOnScreen(scrollViewLocation)
|
||||
val scrollViewTop = scrollViewLocation[1]
|
||||
val scrollViewBottom = scrollViewTop + scrollView.height
|
||||
|
||||
val isButtonVisible = buttonTop >= scrollViewTop && buttonBottom <= scrollViewBottom
|
||||
|
||||
if (isButtonVisible) {
|
||||
fab.hide()
|
||||
} else {
|
||||
fab.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showToast(message: String) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun getConfigContent(): String {
|
||||
val configFile = File(filesDir, "gkms-config.json")
|
||||
return if (configFile.exists()) {
|
||||
configFile.readText()
|
||||
}
|
||||
else {
|
||||
showToast("Initializing configuration file...")
|
||||
"{}"
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveConfig() {
|
||||
try {
|
||||
config.pf = false
|
||||
viewModel.configState.value = config.copy( pf = true ) // 更新 UI
|
||||
}
|
||||
catch (e: RuntimeException) {
|
||||
Log.d(TAG, e.toString())
|
||||
}
|
||||
val configFile = File(filesDir, "gkms-config.json")
|
||||
configFile.writeText(Gson().toJson(binding.config!!))
|
||||
configFile.writeText(json.encodeToString(config))
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun showVersion() {
|
||||
val titleLabel = findViewById<TextView>(R.id.textViewTitle)
|
||||
val versionLabel = findViewById<TextView>(R.id.textViewResVersion)
|
||||
var versionText = "unknown"
|
||||
override fun saveProgramConfig() {
|
||||
try {
|
||||
programConfig.p = false
|
||||
programConfigViewModel.configState.value = programConfig.copy( p = true ) // 更新 UI
|
||||
}
|
||||
catch (e: RuntimeException) {
|
||||
Log.d(TAG, e.toString())
|
||||
}
|
||||
val configFile = File(filesDir, "localify-config.json")
|
||||
configFile.writeText(json.encodeToString(programConfig))
|
||||
}
|
||||
|
||||
fun getVersion(): List<String> {
|
||||
var versionText = ""
|
||||
var resVersionText = "unknown"
|
||||
|
||||
try {
|
||||
val stream = assets.open("${FilesChecker.localizationFilesDir}/version.txt")
|
||||
versionText = FilesChecker.convertToString(stream)
|
||||
resVersionText = FilesChecker.convertToString(stream)
|
||||
|
||||
val packInfo = packageManager.getPackageInfo(packageName, 0)
|
||||
val version = packInfo.versionName
|
||||
titleLabel.text = "${titleLabel.text} $version"
|
||||
val versionCode = packInfo.longVersionCode
|
||||
versionText = "$version ($versionCode)"
|
||||
}
|
||||
catch (_: Exception) {}
|
||||
versionLabel.text = "Assets Version: $versionText"
|
||||
|
||||
return listOf(versionText, resVersionText)
|
||||
}
|
||||
|
||||
private fun loadConfig() {
|
||||
val configStr = getConfigContent()
|
||||
binding.config = try {
|
||||
Gson().fromJson(configStr, GakumasConfig::class.java)
|
||||
}
|
||||
catch (e: JsonSyntaxException) {
|
||||
showToast("Configuration file has been reset: $e")
|
||||
Gson().fromJson("{}", GakumasConfig::class.java)
|
||||
}
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun checkConfigAndUpdateView() {
|
||||
binding.config = binding.config
|
||||
binding.notifyChange()
|
||||
fun openUrl(url: String) {
|
||||
val webpage = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun pushKeyEvent(event: KeyEvent): Boolean {
|
||||
return dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
// Log.d(TAG, "${event.keyCode}, ${event.action}")
|
||||
if (MainKeyEventDispatcher.checkDbgKey(event.keyCode, event.action)) {
|
||||
val origDbg = binding.config?.dbgMode
|
||||
if (origDbg != null) {
|
||||
binding.config!!.dbgMode = !origDbg
|
||||
val origDbg = config.dbgMode
|
||||
config.dbgMode = !origDbg
|
||||
checkConfigAndUpdateView()
|
||||
saveConfig()
|
||||
showToast("Test Mode: ${!origDbg}")
|
||||
}
|
||||
}
|
||||
return if (event.action == 1145) true else super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
loadConfig()
|
||||
|
||||
factory = UserConfigViewModelFactory(config)
|
||||
viewModel = ViewModelProvider(this, factory)[UserConfigViewModel::class.java]
|
||||
|
||||
programConfigFactory = ProgramConfigViewModelFactory(programConfig,
|
||||
FileHotUpdater.getZipResourceVersion(File(filesDir, "update_trans.zip").absolutePath).toString()
|
||||
)
|
||||
programConfigViewModel = ViewModelProvider(this, programConfigFactory)[ProgramConfigViewModel::class.java]
|
||||
|
||||
setContent {
|
||||
GakumasLocalifyTheme(dynamicColor = false, darkTheme = false) {
|
||||
MainUI(context = this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun getConfigState(context: MainActivity?, previewData: GakumasConfig?): State<GakumasConfig> {
|
||||
return if (context != null) {
|
||||
context.viewModel.config.collectAsState()
|
||||
}
|
||||
else {
|
||||
val configMSF = MutableStateFlow(previewData!!)
|
||||
configMSF.asStateFlow().collectAsState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getProgramConfigState(context: MainActivity?, previewData: ProgramConfig? = null): State<ProgramConfig> {
|
||||
return if (context != null) {
|
||||
context.programConfigViewModel.config.collectAsState()
|
||||
}
|
||||
else {
|
||||
val configMSF = MutableStateFlow(previewData ?: ProgramConfig())
|
||||
configMSF.asStateFlow().collectAsState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getProgramDownloadState(context: MainActivity?): State<Float> {
|
||||
return if (context != null) {
|
||||
context.programConfigViewModel.downloadProgress.collectAsState()
|
||||
}
|
||||
else {
|
||||
val configMSF = MutableStateFlow(0f)
|
||||
configMSF.asStateFlow().collectAsState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getProgramDownloadAbleState(context: MainActivity?): State<Boolean> {
|
||||
return if (context != null) {
|
||||
context.programConfigViewModel.downloadAble.collectAsState()
|
||||
}
|
||||
else {
|
||||
val configMSF = MutableStateFlow(true)
|
||||
configMSF.asStateFlow().collectAsState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getProgramLocalResourceVersionState(context: MainActivity?): State<String> {
|
||||
return if (context != null) {
|
||||
context.programConfigViewModel.localResourceVersion.collectAsState()
|
||||
}
|
||||
else {
|
||||
val configMSF = MutableStateFlow("null")
|
||||
configMSF.asStateFlow().collectAsState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getProgramDownloadErrorStringState(context: MainActivity?): State<String> {
|
||||
return if (context != null) {
|
||||
context.programConfigViewModel.errorString.collectAsState()
|
||||
}
|
||||
else {
|
||||
val configMSF = MutableStateFlow("")
|
||||
configMSF.asStateFlow().collectAsState()
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package io.github.chinosk.gakumas.localify
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfig
|
||||
|
||||
|
||||
class TranslucentActivity : ComponentActivity(), IConfigurableActivity<TranslucentActivity> {
|
||||
override lateinit var config: GakumasConfig
|
||||
override lateinit var programConfig: ProgramConfig
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
loadConfig()
|
||||
val requestData = intent.getStringExtra("gkmsData")
|
||||
if (requestData != null) {
|
||||
if (requestData == "requestConfig") {
|
||||
onClickStartGame()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
package io.github.chinosk.gakumas.localify.hookUtils
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import io.github.chinosk.gakumas.localify.GakumasHookMain
|
||||
import io.github.chinosk.gakumas.localify.TAG
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
object FileHotUpdater {
|
||||
private fun unzip(zipFile: InputStream, destDir: String, matchNamePrefix: String = "",
|
||||
replaceMatchNamePrefix: String? = null) {
|
||||
val buffer = ByteArray(1024)
|
||||
try {
|
||||
val folder = File(destDir)
|
||||
if (!folder.exists()) {
|
||||
folder.mkdir()
|
||||
}
|
||||
|
||||
val zipIn = ZipInputStream(zipFile)
|
||||
|
||||
var entry = zipIn.nextEntry
|
||||
while (entry != null) {
|
||||
var writeEntryName = entry.name
|
||||
if (matchNamePrefix.isNotEmpty()) {
|
||||
if (!entry.name.startsWith(matchNamePrefix)) {
|
||||
zipIn.closeEntry()
|
||||
entry = zipIn.nextEntry
|
||||
continue
|
||||
}
|
||||
replaceMatchNamePrefix?.let {
|
||||
writeEntryName = replaceMatchNamePrefix + writeEntryName.substring(
|
||||
matchNamePrefix.length, writeEntryName.length
|
||||
)
|
||||
}
|
||||
}
|
||||
val filePath = destDir + File.separator + writeEntryName
|
||||
if (!entry.isDirectory) {
|
||||
extractFile(zipIn, filePath, buffer)
|
||||
} else {
|
||||
val dir = File(filePath)
|
||||
dir.mkdirs()
|
||||
}
|
||||
zipIn.closeEntry()
|
||||
entry = zipIn.nextEntry
|
||||
}
|
||||
zipIn.close()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "unzip error: $e")
|
||||
}
|
||||
}
|
||||
|
||||
private fun unzip(zipFile: String, destDir: String, matchNamePrefix: String = "") {
|
||||
return unzip(FileInputStream(zipFile), destDir, matchNamePrefix)
|
||||
}
|
||||
|
||||
private fun extractFile(zipIn: ZipInputStream, filePath: String, buffer: ByteArray) {
|
||||
val fout = FileOutputStream(filePath)
|
||||
var length: Int
|
||||
while (zipIn.read(buffer).also { length = it } > 0) {
|
||||
fout.write(buffer, 0, length)
|
||||
}
|
||||
fout.close()
|
||||
}
|
||||
|
||||
private fun getZipResourcePath(zipFile: InputStream): String? {
|
||||
try {
|
||||
val zipIn = ZipInputStream(zipFile)
|
||||
|
||||
var entry = zipIn.nextEntry
|
||||
while (entry != null) {
|
||||
if (entry.isDirectory) {
|
||||
if (entry.name.endsWith("local-files/")) {
|
||||
zipIn.close()
|
||||
var retPath = File(entry.name, "..").canonicalPath
|
||||
if (retPath.startsWith("/")) retPath = retPath.substring(1)
|
||||
return retPath
|
||||
}
|
||||
}
|
||||
zipIn.closeEntry()
|
||||
entry = zipIn.nextEntry
|
||||
}
|
||||
zipIn.close()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
Log.e(TAG, "getZipResourcePath error: $e")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getZipResourceVersion(zipFile: InputStream, basePath: String): String? {
|
||||
try {
|
||||
val targetVersionFilePath = File(basePath, "version.txt").canonicalPath
|
||||
|
||||
val zipIn = ZipInputStream(zipFile)
|
||||
var entry = zipIn.nextEntry
|
||||
while (entry != null) {
|
||||
if (!entry.isDirectory) {
|
||||
if ("/${entry.name}" == targetVersionFilePath) {
|
||||
Log.d(TAG, "targetVersionFilePath: $targetVersionFilePath")
|
||||
val reader = BufferedReader(InputStreamReader(zipIn))
|
||||
val versionContent = reader.use { it.readText() }
|
||||
Log.d(TAG, "versionContent: $versionContent")
|
||||
zipIn.close()
|
||||
return versionContent
|
||||
}
|
||||
}
|
||||
zipIn.closeEntry()
|
||||
entry = zipIn.nextEntry
|
||||
}
|
||||
zipIn.close()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
Log.e(TAG, "getZipResourceVersion error: $e")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getZipResourceVersion(zipFile: String, basePath: String): String? {
|
||||
return getZipResourceVersion(FileInputStream(zipFile), basePath)
|
||||
}
|
||||
|
||||
fun getZipResourceVersion(zipFile: String): String? {
|
||||
return try {
|
||||
val basePath = getZipResourcePath(FileInputStream(zipFile))
|
||||
basePath?.let { getZipResourceVersion(zipFile, it) }
|
||||
}
|
||||
catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFilesFromZip(activity: Activity, zipFileUri: Uri, filesDir: File, deleteAfterUpdate: Boolean) {
|
||||
try {
|
||||
GakumasHookMain.showToast("Updating files from zip...")
|
||||
|
||||
var basePath: String?
|
||||
activity.contentResolver.openInputStream(zipFileUri).use {
|
||||
basePath = it?.let { getZipResourcePath(it) }
|
||||
if (basePath == null) {
|
||||
Log.e(TAG, "getZipResourcePath failed.")
|
||||
return@updateFilesFromZip
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
var resourceVersion: String?
|
||||
activity.contentResolver.openInputStream(zipFileUri).use {
|
||||
resourceVersion = it?.let { getZipResourceVersion(it, basePath!!) }
|
||||
Log.d(TAG, "resourceVersion: $resourceVersion ($basePath)")
|
||||
}*/
|
||||
|
||||
activity.contentResolver.openInputStream(zipFileUri).use {
|
||||
it?.let {
|
||||
unzip(it, File(filesDir, FilesChecker.localizationFilesDir).absolutePath,
|
||||
basePath!!, "../gakumas-local/")
|
||||
if (deleteAfterUpdate) {
|
||||
activity.contentResolver.delete(zipFileUri, null, null)
|
||||
}
|
||||
GakumasHookMain.showToast("Update success.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (e: java.io.FileNotFoundException) {
|
||||
Log.i(TAG, "updateFilesFromZip - file not found: $e")
|
||||
GakumasHookMain.showToast("Update file not found.")
|
||||
}
|
||||
catch (e: Exception) {
|
||||
Log.e(TAG, "updateFilesFromZip failed: $e")
|
||||
GakumasHookMain.showToast("Updating files failed: $e")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -16,12 +16,16 @@ object FilesChecker {
|
||||
var filesUpdated = false
|
||||
|
||||
fun initAndCheck(fileDir: File, modulePath: String) {
|
||||
this.filesDir = fileDir
|
||||
this.modulePath = modulePath
|
||||
initDir(fileDir, modulePath)
|
||||
|
||||
checkFiles()
|
||||
}
|
||||
|
||||
fun initDir(fileDir: File, modulePath: String) {
|
||||
this.filesDir = fileDir
|
||||
this.modulePath = modulePath
|
||||
}
|
||||
|
||||
fun checkFiles() {
|
||||
val installedVersion = getInstalledVersion()
|
||||
val pluginVersion = getPluginVersion()
|
||||
@ -118,4 +122,45 @@ object FilesChecker {
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
private fun deleteRecursively(file: File): Boolean {
|
||||
if (file.isDirectory) {
|
||||
val children = file.listFiles()
|
||||
if (children != null) {
|
||||
for (child in children) {
|
||||
val success = deleteRecursively(child)
|
||||
if (!success) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return file.delete()
|
||||
}
|
||||
|
||||
fun cleanAssets() {
|
||||
val pluginBasePath = File(filesDir, localizationFilesDir)
|
||||
val localFilesDir = File(pluginBasePath, "local-files")
|
||||
|
||||
val fontFile = File(localFilesDir, "gkamsZHFontMIX.otf")
|
||||
val resourceDir = File(localFilesDir, "resource")
|
||||
val genericTransDir = File(localFilesDir, "genericTrans")
|
||||
val genericTransFile = File(localFilesDir, "generic.json")
|
||||
val i18nFile = File(localFilesDir, "localization.json")
|
||||
|
||||
if (fontFile.exists()) {
|
||||
fontFile.delete()
|
||||
}
|
||||
if (deleteRecursively(resourceDir)) {
|
||||
resourceDir.mkdirs()
|
||||
}
|
||||
if (deleteRecursively(genericTransDir)) {
|
||||
genericTransDir.mkdirs()
|
||||
}
|
||||
if (genericTransFile.exists()) {
|
||||
genericTransFile.writeText("{}")
|
||||
}
|
||||
if (i18nFile.exists()) {
|
||||
i18nFile.writeText("{}")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
package io.github.chinosk.gakumas.localify.hookUtils
|
||||
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
|
||||
object MainKeyEventDispatcher {
|
||||
private val targetDbgKeyList: IntArray = intArrayOf(19, 19, 20, 20, 21, 22, 21, 22, 30, 29)
|
||||
private var currentIndex = 0;
|
||||
private var currentIndex = 0
|
||||
|
||||
fun checkDbgKey(code: Int, action: Int): Boolean {
|
||||
if (action == KeyEvent.ACTION_UP) return false
|
||||
|
@ -0,0 +1,137 @@
|
||||
package io.github.chinosk.gakumas.localify.mainUtils
|
||||
|
||||
import okhttp3.*
|
||||
import java.io.IOException
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object FileDownloader {
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(0, TimeUnit.SECONDS)
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private var call: Call? = null
|
||||
|
||||
fun downloadFile(
|
||||
url: String,
|
||||
onDownload: (Float, downloaded: Long, size: Long) -> Unit,
|
||||
onSuccess: (ByteArray) -> Unit,
|
||||
onFailed: (Int, String) -> Unit,
|
||||
checkContentTypes: List<String>? = null
|
||||
) {
|
||||
try {
|
||||
if (call != null) {
|
||||
onFailed(-1, "Another file is downloading.")
|
||||
return
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
call = client.newCall(request)
|
||||
call?.enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
this@FileDownloader.call = null
|
||||
if (call.isCanceled()) {
|
||||
onFailed(-1, "Download canceled")
|
||||
} else {
|
||||
onFailed(-1, e.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (!response.isSuccessful) {
|
||||
this@FileDownloader.call = null
|
||||
onFailed(response.code, response.message)
|
||||
return
|
||||
}
|
||||
|
||||
if (checkContentTypes != null) {
|
||||
val contentType = response.header("Content-Type")
|
||||
if (!checkContentTypes.contains(contentType)) {
|
||||
onFailed(-1, "Unexpected content type: $contentType")
|
||||
this@FileDownloader.call = null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.body?.let { responseBody ->
|
||||
val contentLength = responseBody.contentLength()
|
||||
val inputStream = responseBody.byteStream()
|
||||
val buffer = ByteArray(8 * 1024)
|
||||
var downloadedBytes = 0L
|
||||
var read: Int
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
try {
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
downloadedBytes += read
|
||||
val progress = if (contentLength < 0) {
|
||||
0f
|
||||
}
|
||||
else {
|
||||
downloadedBytes.toFloat() / contentLength
|
||||
}
|
||||
onDownload(progress, downloadedBytes, contentLength)
|
||||
}
|
||||
onSuccess(outputStream.toByteArray())
|
||||
} catch (e: IOException) {
|
||||
if (call.isCanceled()) {
|
||||
onFailed(-1, "Download canceled")
|
||||
} else {
|
||||
onFailed(-1, e.message ?: "Error reading stream")
|
||||
}
|
||||
} finally {
|
||||
this@FileDownloader.call = null
|
||||
inputStream.close()
|
||||
outputStream.close()
|
||||
}
|
||||
} ?: run {
|
||||
this@FileDownloader.call = null
|
||||
onFailed(-1, "Response body is null")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (e: Exception) {
|
||||
onFailed(-1, e.toString())
|
||||
call = null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
call?.cancel()
|
||||
this@FileDownloader.call = null
|
||||
}
|
||||
|
||||
/**
|
||||
* return: Status, newString
|
||||
* Status: 0 - not change, 1 - need check, 2 - modified, 3 - checked
|
||||
**/
|
||||
fun checkAndChangeDownloadURL(url: String, forceEdit: Boolean = false): Pair<Int, String> {
|
||||
|
||||
if (!url.startsWith("https://github.com/")) { // check github only
|
||||
return Pair(0, url)
|
||||
}
|
||||
if (url.endsWith(".zip")) {
|
||||
return Pair(0, url)
|
||||
}
|
||||
|
||||
// https://github.com/chinosk6/GakumasTranslationData
|
||||
// https://github.com/chinosk6/GakumasTranslationData.git
|
||||
// https://github.com/chinosk6/GakumasTranslationData/archive/refs/heads/main.zip
|
||||
if (url.endsWith(".git")) {
|
||||
return Pair(2, "${url.substring(0, url.length - 4)}/archive/refs/heads/main.zip")
|
||||
}
|
||||
|
||||
if (forceEdit) {
|
||||
return Pair(3, "$url/archive/refs/heads/main.zip")
|
||||
}
|
||||
|
||||
return Pair(1, url)
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package io.github.chinosk.gakumas.localify.mainUtils
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
val json = Json {
|
||||
encodeDefaults = true
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package io.github.chinosk.gakumas.localify.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AboutPageConfig(
|
||||
val plugin_repo: String = "https://github.com/chinosk6/gakuen-imas-localify",
|
||||
val main_contributors: List<MainContributors> = listOf(),
|
||||
val contrib_img: ContribImg = ContribImg(
|
||||
"https://contrib.rocks/image?repo=chinosk6/gakuen-imas-localify",
|
||||
"https://contrib.rocks/image?repo=chinosk6/GakumasTranslationData"
|
||||
)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MainContributors(
|
||||
val name: String,
|
||||
val links: List<Links>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContribImg(
|
||||
val plugin: String,
|
||||
val translation: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Links(
|
||||
val name: String,
|
||||
val link: String
|
||||
)
|
@ -1,6 +1,8 @@
|
||||
package io.github.chinosk.gakumas.localify.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GakumasConfig (
|
||||
var dbgMode: Boolean = false,
|
||||
var enabled: Boolean = true,
|
||||
@ -41,4 +43,6 @@ data class GakumasConfig (
|
||||
var bLimitYy: Float = 1.0f,
|
||||
var bLimitZx: Float = 1.0f,
|
||||
var bLimitZy: Float = 1.0f,
|
||||
|
||||
var pf: Boolean = false
|
||||
)
|
||||
|
@ -0,0 +1,41 @@
|
||||
package io.github.chinosk.gakumas.localify.models
|
||||
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.json
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.encoding.encodeStructure
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
|
||||
@Serializable
|
||||
data class ProgramConfig(
|
||||
var checkBuiltInAssets: Boolean = true,
|
||||
var transRemoteZipUrl: String = "",
|
||||
var useRemoteAssets: Boolean = false,
|
||||
var delRemoteAfterUpdate: Boolean = true,
|
||||
var cleanLocalAssets: Boolean = false,
|
||||
var p: Boolean = false
|
||||
)
|
||||
|
||||
class ProgramConfigSerializer(
|
||||
private val excludes: List<String> = emptyList(),
|
||||
) : KSerializer<ProgramConfig> {
|
||||
override val descriptor: SerialDescriptor = ProgramConfig.serializer().descriptor
|
||||
override fun serialize(encoder: Encoder, value: ProgramConfig) {
|
||||
val jsonObject = json.encodeToJsonElement(value).jsonObject
|
||||
encoder.encodeStructure(descriptor) {
|
||||
jsonObject.keys.forEachIndexed { index, k ->
|
||||
if (k in excludes) return@forEachIndexed
|
||||
encodeSerializableElement(descriptor, index, JsonElement.serializer(), jsonObject[k]!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): ProgramConfig {
|
||||
return ProgramConfig.serializer().deserialize(decoder)
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package io.github.chinosk.gakumas.localify.models
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
open class CollapsibleBoxViewModel(initiallyBreastExpanded: Boolean = false) : ViewModel() {
|
||||
open var expanded by mutableStateOf(initiallyBreastExpanded)
|
||||
}
|
||||
|
||||
class BreastCollapsibleBoxViewModel(initiallyBreastExpanded: Boolean = false) : CollapsibleBoxViewModel(initiallyBreastExpanded) {
|
||||
override var expanded by mutableStateOf(initiallyBreastExpanded)
|
||||
}
|
||||
|
||||
class ResourceCollapsibleBoxViewModel(initiallyBreastExpanded: Boolean = false) : CollapsibleBoxViewModel(initiallyBreastExpanded) {
|
||||
override var expanded by mutableStateOf(initiallyBreastExpanded)
|
||||
}
|
||||
|
||||
class BreastCollapsibleBoxViewModelFactory(private val initiallyExpanded: Boolean) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(BreastCollapsibleBoxViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return BreastCollapsibleBoxViewModel(initiallyExpanded) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
|
||||
class ResourceCollapsibleBoxViewModelFactory(private val initiallyExpanded: Boolean) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(ResourceCollapsibleBoxViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return ResourceCollapsibleBoxViewModel(initiallyExpanded) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProgramConfigViewModelFactory(private val initialValue: ProgramConfig,
|
||||
private val localResourceVersion: String) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(ProgramConfigViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return ProgramConfigViewModel(initialValue, localResourceVersion) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
|
||||
class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion: String) : ViewModel() {
|
||||
val configState = MutableStateFlow(initValue)
|
||||
val config: StateFlow<ProgramConfig> = configState.asStateFlow()
|
||||
|
||||
val downloadProgressState = MutableStateFlow(-1f)
|
||||
val downloadProgress: StateFlow<Float> = downloadProgressState.asStateFlow()
|
||||
|
||||
val downloadAbleState = MutableStateFlow(true)
|
||||
val downloadAble: StateFlow<Boolean> = downloadAbleState.asStateFlow()
|
||||
|
||||
val localResourceVersionState = MutableStateFlow(initLocalResourceVersion)
|
||||
val localResourceVersion: StateFlow<String> = localResourceVersionState.asStateFlow()
|
||||
|
||||
val errorStringState = MutableStateFlow("")
|
||||
val errorString: StateFlow<String> = errorStringState.asStateFlow()
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.components
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@Composable
|
||||
fun GakuButton(
|
||||
onClick: () -> Unit,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = RoundedCornerShape(50.dp), // 用于实现左右两边的半圆角
|
||||
shadowElevation: Dp = 8.dp, // 阴影的高度
|
||||
borderWidth: Dp = 1.dp, // 描边的宽度
|
||||
borderColor: Color = Color.Transparent, // 描边的颜色
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
var buttonSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
|
||||
val gradient = remember(buttonSize) {
|
||||
Brush.linearGradient(
|
||||
colors = if (enabled) listOf(Color(0xFFFF5F19), Color(0xFFFFA028)) else
|
||||
listOf(Color(0xFFF9F9F9), Color(0xFFF0F0F0)),
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(buttonSize.width.toFloat(), buttonSize.height.toFloat()) // 动态终点
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = modifier
|
||||
.onGloballyPositioned { layoutCoordinates ->
|
||||
buttonSize = layoutCoordinates.size
|
||||
}
|
||||
.shadow(elevation = shadowElevation, shape = shape)
|
||||
.clip(shape)
|
||||
.background(gradient)
|
||||
.border(borderWidth, borderColor, shape),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Text(text = text, color = if (enabled) Color.White else Color(0xFF111111))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
fun GakuButtonPreview() {
|
||||
GakuButton(modifier = Modifier.width(80.dp).height(40.dp), text = "Button", onClick = {},
|
||||
enabled = true)
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.components
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.chinosk.gakumas.localify.R
|
||||
|
||||
@Composable
|
||||
fun GakuGroupBox(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = "Title",
|
||||
maxWidth: Dp = 500.dp,
|
||||
contentPadding: Dp = 8.dp,
|
||||
rightHead: @Composable (() -> Unit)? = null,
|
||||
onHeadClick: () -> Unit = {},
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.shadow(4.dp, RoundedCornerShape(
|
||||
bottomStart = 16.dp,
|
||||
bottomEnd = 8.dp,
|
||||
topEnd = 16.dp,
|
||||
topStart = 0.dp
|
||||
))
|
||||
// .background(Color.White, RoundedCornerShape(8.dp))
|
||||
) {
|
||||
Column(modifier = modifier.widthIn(max = maxWidth)) {
|
||||
// Header
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.Transparent)
|
||||
.height(23.dp)
|
||||
.clickable {
|
||||
onHeadClick()
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.bg_h1),
|
||||
contentDescription = null,
|
||||
// modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.FillBounds
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = Color.White,
|
||||
modifier = modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.padding(start = (maxWidth.value * 0.043f).dp)
|
||||
)
|
||||
if (rightHead != null) {
|
||||
Box(modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = (maxWidth.value * 0.1f).dp)) {
|
||||
rightHead()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = 16.dp,
|
||||
bottomEnd = 8.dp
|
||||
)
|
||||
)
|
||||
.padding(contentPadding)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
fun PreviewGakuGroupBox() {
|
||||
GakuGroupBox(
|
||||
title = "GroupBox Title"
|
||||
) {
|
||||
Column {
|
||||
Text("This is the content of the GroupBox.")
|
||||
Text("This is the content of the GroupBox.")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.components
|
||||
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@Composable
|
||||
fun GakuProgressBar(modifier: Modifier = Modifier, progress: Float, isError: Boolean = false) {
|
||||
val animatedProgress by animateFloatAsState(targetValue = progress, label = "progressAnime")
|
||||
|
||||
Row(
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
) {
|
||||
if (progress <= 0f) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.height(8.dp),
|
||||
color = if (isError) Color(0xFFE2041B) else Color(0xFFF9C114),
|
||||
)
|
||||
}
|
||||
else {
|
||||
LinearProgressIndicator(
|
||||
progress = { animatedProgress },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.height(8.dp),
|
||||
color = if (isError) Color(0xFFE2041B) else Color(0xFFF9C114),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(if (progress > 0f) "${(progress * 100).toInt()}%" else if (isError) "Failed" else "Downloading")
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
fun GakuProgressBarPreview() {
|
||||
GakuProgressBar(progress = 0.25f)
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.components
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.RadioButtonDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.github.chinosk.gakumas.localify.ui.components.base.AutoSizeText
|
||||
|
||||
@Composable
|
||||
fun GakuRadio(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
selected: Boolean,
|
||||
fontSize: TextUnit = 14.sp,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val backgroundColor = if (selected) Color(0xFFFFEEC3) else Color(0xFFF8F7F5)
|
||||
val radioButtonColor = if (selected) Color(0xFFFF7601) else MaterialTheme.colorScheme.onSurface
|
||||
|
||||
Surface(
|
||||
shape = RoundedCornerShape(
|
||||
topStart = 4.dp,
|
||||
topEnd = 16.dp,
|
||||
bottomEnd = 4.dp,
|
||||
bottomStart = 16.dp
|
||||
),
|
||||
color = backgroundColor,
|
||||
modifier = modifier
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(onTap = {
|
||||
onClick()
|
||||
})
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier.padding(start = 0.dp, end = 4.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
modifier = Modifier.padding(start = 0.dp),
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = radioButtonColor,
|
||||
unselectedColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
// Spacer(modifier = modifier.width(16.dp))
|
||||
AutoSizeText(text = text,
|
||||
textStyle = TextStyle(color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = fontSize))
|
||||
// Text(text = text, color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO, widthDp = 100, heightDp = 40)
|
||||
@Composable
|
||||
fun GakuRadioPreview() {
|
||||
GakuRadio(text = "GakuRadioooo", selected = true, onClick = {})
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.components
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.github.chinosk.gakumas.localify.ui.components.base.AutoSizeText
|
||||
|
||||
|
||||
@Composable
|
||||
fun GakuSwitch(modifier: Modifier = Modifier,
|
||||
text: String = "",
|
||||
checked: Boolean = false,
|
||||
leftPart: @Composable (() -> Unit)? = null,
|
||||
onCheckedChange: (Boolean) -> Unit = {}) {
|
||||
Row(modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
if (text.isNotEmpty()) {
|
||||
AutoSizeText(text = text, fontSize = 16.sp)
|
||||
}
|
||||
leftPart?.invoke()
|
||||
Switch(checked = checked,
|
||||
onCheckedChange = { value -> onCheckedChange(value) },
|
||||
modifier = Modifier,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color(0xFFFFFFFF),
|
||||
checkedTrackColor = Color(0xFFF9C114),
|
||||
|
||||
uncheckedThumbColor = Color(0xFFFFFFFF),
|
||||
uncheckedTrackColor = Color(0xFFCFD8DC),
|
||||
uncheckedBorderColor = Color(0xFFCFD8DC),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
fun GakuSwitchPreview() {
|
||||
GakuSwitch(text = "Switch", checked = true)
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.components
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun GakuTabRow(
|
||||
modifier: Modifier = Modifier,
|
||||
pagerState: PagerState,
|
||||
tabs: List<String>,
|
||||
onTabSelected: (index: Int) -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
onTabSelected(pagerState.currentPage)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.shadow(4.dp, RoundedCornerShape(16.dp))
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.shadow(4.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column {
|
||||
TabRow(
|
||||
modifier = modifier.background(Color.Transparent),
|
||||
containerColor = Color.Transparent,
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
indicator = @Composable { tabPositions ->
|
||||
Box(
|
||||
Modifier
|
||||
.tabIndicatorOffset(tabPositions[pagerState.currentPage])
|
||||
.height(4.dp)
|
||||
.background(Color(0xFFFFA500))
|
||||
.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.scrollToPage(index)
|
||||
// pagerState.animateScrollToPage(
|
||||
// page = index,
|
||||
// animationSpec = tween(durationMillis = 250)
|
||||
// )
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = title,
|
||||
color = if (pagerState.currentPage == index) Color(0xFFFFA500) else Color.Black
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
fun GakuTabRowPreview(modifier: Modifier = Modifier) {
|
||||
val pagerState = rememberPagerState(initialPage = 1, pageCount = { 3 })
|
||||
GakuTabRow(modifier, pagerState, listOf("TAB 1", "TAB 2", "TAB 3")) { _ -> }
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.components
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun GakuTextInput(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
fontSize: Float = 16f,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default
|
||||
) {
|
||||
val shape: Shape = remember {
|
||||
RoundedCornerShape(
|
||||
topStart = 4.dp,
|
||||
topEnd = 16.dp,
|
||||
bottomEnd = 4.dp,
|
||||
bottomStart = 16.dp
|
||||
)
|
||||
}
|
||||
|
||||
var localValue by remember { mutableStateOf(value) }
|
||||
var isUserInput by remember { mutableStateOf(false) }
|
||||
val textStyle = remember {
|
||||
TextStyle(fontSize = fontSize.sp)
|
||||
}
|
||||
|
||||
LaunchedEffect(value) {
|
||||
if (!isUserInput) {
|
||||
localValue = value
|
||||
}
|
||||
isUserInput = false
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
) {
|
||||
OutlinedTextFieldNoPadding(
|
||||
singleLine = true,
|
||||
value = localValue,
|
||||
onValueChange = { newValue ->
|
||||
isUserInput = true
|
||||
localValue = newValue
|
||||
onValueChange(newValue)
|
||||
},
|
||||
label = label,
|
||||
modifier = modifier,
|
||||
textStyle = textStyle,
|
||||
shape = shape,
|
||||
keyboardOptions = keyboardOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OutlinedTextFieldNoPadding(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
prefix: @Composable (() -> Unit)? = null,
|
||||
suffix: @Composable (() -> Unit)? = null,
|
||||
supportingText: @Composable (() -> Unit)? = null,
|
||||
isError: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = OutlinedTextFieldDefaults.shape,
|
||||
colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
|
||||
) {
|
||||
// If color is not provided via the text style, use content color as a default
|
||||
val textColor = textStyle.color
|
||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||
|
||||
CompositionLocalProvider {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
modifier = if (label != null) {
|
||||
modifier
|
||||
// Merge semantics at the beginning of the modifier chain to ensure padding is
|
||||
// considered part of the text field.
|
||||
.semantics(mergeDescendants = true) {}
|
||||
.padding(top = 8.dp)
|
||||
} else {
|
||||
modifier
|
||||
}
|
||||
.defaultMinSize(
|
||||
minWidth = OutlinedTextFieldDefaults.MinWidth,
|
||||
minHeight = OutlinedTextFieldDefaults.MinHeight
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = mergedTextStyle,
|
||||
cursorBrush = SolidColor(if (!isError) MaterialTheme.colorScheme.primary else Color.Red),
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
interactionSource = interactionSource,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
OutlinedTextFieldDefaults.DecorationBox(
|
||||
contentPadding = PaddingValues.Absolute(left = 16.dp, right = 16.dp),
|
||||
value = value,
|
||||
visualTransformation = visualTransformation,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = placeholder,
|
||||
label = label,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
prefix = prefix,
|
||||
suffix = suffix,
|
||||
supportingText = supportingText,
|
||||
singleLine = singleLine,
|
||||
enabled = enabled,
|
||||
isError = isError,
|
||||
interactionSource = interactionSource,
|
||||
colors = colors,
|
||||
container = {
|
||||
OutlinedTextFieldDefaults.ContainerBox(
|
||||
enabled,
|
||||
isError,
|
||||
interactionSource,
|
||||
colors,
|
||||
shape
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
fun GakuTextInputPreview() {
|
||||
GakuTextInput(modifier = Modifier.height(50.dp),
|
||||
fontSize = 16f,
|
||||
value = "123456", onValueChange = { }, label = { Text("Label") })
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.components.base
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
|
||||
@Composable
|
||||
fun AutoSizeText(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
color: Color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
textStyle: TextStyle? = null,
|
||||
minSize: TextUnit = 8.sp
|
||||
) {
|
||||
var scaledTextStyle by remember { mutableStateOf(textStyle ?: TextStyle(color = color, fontSize = fontSize)) }
|
||||
var readyToDraw by remember { mutableStateOf(false) }
|
||||
|
||||
if (LocalInspectionMode.current) {
|
||||
Text(
|
||||
text,
|
||||
modifier,
|
||||
style = scaledTextStyle
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
Text(
|
||||
text,
|
||||
modifier.drawWithContent {
|
||||
if (readyToDraw) {
|
||||
drawContent()
|
||||
}
|
||||
},
|
||||
style = scaledTextStyle,
|
||||
softWrap = false,
|
||||
onTextLayout = { textLayoutResult ->
|
||||
if (textLayoutResult.didOverflowWidth) {
|
||||
val newSize = (scaledTextStyle.fontSize.value - 1.sp.value).sp
|
||||
if (minSize <= newSize) {
|
||||
scaledTextStyle = scaledTextStyle.copy(fontSize = newSize)
|
||||
}
|
||||
else {
|
||||
readyToDraw = true
|
||||
}
|
||||
} else {
|
||||
readyToDraw = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.components.base
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import io.github.chinosk.gakumas.localify.models.CollapsibleBoxViewModel
|
||||
|
||||
@Composable
|
||||
fun CollapsibleBox(
|
||||
modifier: Modifier = Modifier,
|
||||
collapsedHeight: Dp = 28.dp,
|
||||
viewModel: CollapsibleBoxViewModel = viewModel(),
|
||||
showExpand: Boolean = true,
|
||||
expandState: Boolean? = null,
|
||||
innerPaddingTopBottom: Dp = 0.dp,
|
||||
innerPaddingLeftRight: Dp = 0.dp,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val expanded by viewModel::expanded
|
||||
|
||||
// var offsetY by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
val animatedHeight by animateDpAsState(
|
||||
targetValue = if (expandState ?: expanded) Dp.Unspecified else collapsedHeight,
|
||||
label = "CollapsibleBox$collapsedHeight"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.animateContentSize()/*
|
||||
.pointerInput(Unit) {
|
||||
detectVerticalDragGestures(
|
||||
onVerticalDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
offsetY += dragAmount
|
||||
if (expanded && offsetY > 0) {
|
||||
viewModel.expanded = false
|
||||
} else if (!expanded && offsetY < 0) {
|
||||
viewModel.expanded = true
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
offsetY = 0f
|
||||
}
|
||||
)
|
||||
}*/
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.height(animatedHeight)
|
||||
.fillMaxWidth()
|
||||
.padding(start = innerPaddingLeftRight, end = innerPaddingLeftRight,
|
||||
top = innerPaddingTopBottom, bottom = innerPaddingTopBottom)
|
||||
// .fillMaxSize()
|
||||
.clickable {
|
||||
if (!expanded && showExpand) {
|
||||
viewModel.expanded = expandState ?: true
|
||||
}
|
||||
},
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
//item {
|
||||
if (expandState ?: expanded) {
|
||||
content()
|
||||
}
|
||||
else if (showExpand) {
|
||||
Text(text = "Details ↓", color = Color.Gray)
|
||||
}
|
||||
//}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
fun CollapsibleBoxPreview() {
|
||||
CollapsibleBox(showExpand = true) {}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.pages
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.github.chinosk.gakumas.localify.MainActivity
|
||||
import io.github.chinosk.gakumas.localify.R
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.ui.theme.GakumasLocalifyTheme
|
||||
|
||||
|
||||
@Composable
|
||||
fun MainUI(modifier: Modifier = Modifier, context: MainActivity? = null,
|
||||
previewData: GakumasConfig? = null) {
|
||||
val imagePainter = painterResource(R.drawable.bg_pattern)
|
||||
val versionInfo = remember {
|
||||
context?.getVersion() ?: listOf("", "Unknown")
|
||||
}
|
||||
// val config = getConfigState(context, previewData)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFFDFDFD))
|
||||
) {
|
||||
val screenH = imageRepeater(
|
||||
painter = imagePainter,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.TopCenter)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(10.dp, 10.dp, 10.dp, 0.dp),
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
Text(text = "Gakumas Localify ${versionInfo[0]}", fontSize = 18.sp)
|
||||
Text(text = "Assets version: ${versionInfo[1]}", fontSize = 13.sp)
|
||||
|
||||
SettingsTabs(modifier, listOf(stringResource(R.string.about), stringResource(R.string.home),
|
||||
stringResource(R.string.advanced_settings)),
|
||||
context = context, previewData = previewData, screenH = screenH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun imageRepeater(
|
||||
painter: Painter,
|
||||
modifier: Modifier = Modifier
|
||||
): Dp {
|
||||
val density = LocalDensity.current
|
||||
val imageHeightPx = painter.intrinsicSize.height
|
||||
val imageHeightDp = with(density) { imageHeightPx.toDp() }
|
||||
var retMaxH = 1080.dp
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
retMaxH = maxHeight
|
||||
val screenHeight = maxHeight
|
||||
val repeatCount = (screenHeight / imageHeightDp).toInt() + 1
|
||||
|
||||
Column {
|
||||
repeat(repeatCount) {
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(imageHeightDp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return retMaxH
|
||||
}
|
||||
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO, widthDp = 380)
|
||||
@Composable
|
||||
fun MainUIPreview(modifier: Modifier = Modifier) {
|
||||
val previewConfig = GakumasConfig()
|
||||
previewConfig.enabled = true
|
||||
|
||||
GakumasLocalifyTheme {
|
||||
MainUI(previewData = previewConfig)
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.pages
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.chinosk.gakumas.localify.MainActivity
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.onClickStartGame
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuTabRow
|
||||
import io.github.chinosk.gakumas.localify.ui.pages.subPages.AboutPage
|
||||
import io.github.chinosk.gakumas.localify.ui.pages.subPages.AdvanceSettingsPage
|
||||
import io.github.chinosk.gakumas.localify.ui.pages.subPages.HomePage
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SettingsTabs(modifier: Modifier = Modifier,
|
||||
titles: List<String>,
|
||||
context: MainActivity? = null,
|
||||
previewData: GakumasConfig? = null,
|
||||
screenH: Dp = 1080.dp
|
||||
) {
|
||||
|
||||
val pagerState = rememberPagerState(initialPage = 1, pageCount = { titles.size })
|
||||
|
||||
Box {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
pageSpacing = 10.dp
|
||||
) { page ->
|
||||
Column(modifier = modifier
|
||||
.padding(5.dp)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
when (page) {
|
||||
0 -> AboutPage(modifier, context = context, previewData = previewData, screenH = screenH)
|
||||
1 -> HomePage(modifier, context = context, previewData = previewData, screenH = screenH)
|
||||
2 -> AdvanceSettingsPage(modifier, context = context, previewData = previewData, screenH = screenH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 6.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
FloatingActionButton(
|
||||
onClick = { context?.onClickStartGame() },
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
shape = CircleShape
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = "StartGame")
|
||||
}
|
||||
|
||||
GakuTabRow(modifier, pagerState, titles) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO, heightDp = 760)
|
||||
@Composable
|
||||
fun SettingTabsPreview(modifier: Modifier = Modifier) {
|
||||
SettingsTabs(titles = listOf("TAB 1", "TAB 2", "TAB 3"), previewData = GakumasConfig())
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.navigation.NavController
|
||||
import io.github.chinosk.gakumas.localify.R
|
||||
|
||||
@Composable
|
||||
fun SplashScreen(navController: NavController) {
|
||||
/*Image(
|
||||
painter = painterResource(id = R.drawable.splash_image),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.FillHeight
|
||||
)*/
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(100)
|
||||
|
||||
navController.navigate("main") {
|
||||
popUpTo("splash") { inclusive = true }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.pages.subPages
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.ImageLoader
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import io.github.chinosk.gakumas.localify.MainActivity
|
||||
import io.github.chinosk.gakumas.localify.R
|
||||
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.convertToString
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.json
|
||||
import io.github.chinosk.gakumas.localify.models.AboutPageConfig
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuButton
|
||||
|
||||
|
||||
@Composable
|
||||
fun AboutPage(modifier: Modifier = Modifier,
|
||||
context: MainActivity? = null,
|
||||
previewData: GakumasConfig? = null,
|
||||
bottomSpacerHeight: Dp = 120.dp,
|
||||
screenH: Dp = 1080.dp) {
|
||||
// val config = getConfigState(context, previewData)
|
||||
val contributorInfo = remember {
|
||||
val dataJsonString = context?.getString(R.string.about_contributors_asset_file)?.let {
|
||||
convertToString(context.assets?.open(it))
|
||||
}
|
||||
dataJsonString?.let { json.decodeFromString<AboutPageConfig>(it) }
|
||||
?: AboutPageConfig()
|
||||
}
|
||||
|
||||
LazyColumn(modifier = modifier
|
||||
.sizeIn(maxHeight = screenH)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(stringResource(R.string.about_warn_title), fontSize = 24.sp, color = MaterialTheme.colorScheme.error)
|
||||
Text(stringResource(R.string.about_warn_p1))
|
||||
Text(stringResource(R.string.about_warn_p2))
|
||||
}
|
||||
|
||||
item {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(stringResource(R.string.about_about_title), fontSize = 24.sp, color = MaterialTheme.colorScheme.onPrimaryContainer)
|
||||
Text(stringResource(R.string.about_about_p1))
|
||||
Text(stringResource(R.string.about_about_p2))
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 8.dp, end = 8.dp, top = 8.dp, bottom = 0.dp
|
||||
)) {
|
||||
GakuButton(text = "Github", modifier = modifier
|
||||
.weight(1f)
|
||||
.sizeIn(maxWidth = 600.dp), onClick = {
|
||||
context?.openUrl(contributorInfo.plugin_repo)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
LazyColumn(modifier = modifier
|
||||
.sizeIn(maxHeight = screenH)
|
||||
.fillMaxWidth()) {
|
||||
item {
|
||||
Text(stringResource(R.string.project_contribution), fontSize = 24.sp, color = MaterialTheme.colorScheme.onPrimaryContainer)
|
||||
}
|
||||
for (contributor in contributorInfo.main_contributors) {
|
||||
item {
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(0.dp, 8.dp, 8.dp, 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(contributor.name, fontSize = 16.sp)
|
||||
for (link in contributor.links) {
|
||||
GakuButton(text = link.name, modifier = modifier.height(40.dp),
|
||||
onClick = {
|
||||
context?.openUrl(link.link)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Text(stringResource(R.string.contributors), fontSize = 24.sp, color = MaterialTheme.colorScheme.onPrimaryContainer)
|
||||
|
||||
Text(stringResource(R.string.plugin_code), fontSize = 16.sp)
|
||||
NetworkSvgImage(
|
||||
url = contributorInfo.contrib_img.plugin,
|
||||
contentDescription = "plugin-contrib"
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(stringResource(R.string.translation_repository), fontSize = 16.sp)
|
||||
NetworkSvgImage(
|
||||
url = contributorInfo.contrib_img.translation,
|
||||
contentDescription = "translation-contrib"
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = modifier.height(bottomSpacerHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun NetworkImage(
|
||||
url: String,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val painter = rememberAsyncImagePainter(model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(url)
|
||||
.crossfade(true)
|
||||
.size(Size.ORIGINAL)
|
||||
.build())
|
||||
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NetworkSvgImage(
|
||||
url: String,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val imageLoader = ImageLoader.Builder(LocalContext.current)
|
||||
.components {
|
||||
add(SvgDecoder.Factory())
|
||||
}
|
||||
.build()
|
||||
|
||||
val painter = rememberAsyncImagePainter(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(url)
|
||||
.size(Size.ORIGINAL)
|
||||
.build(),
|
||||
imageLoader = imageLoader
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
fun AboutPagePreview(modifier: Modifier = Modifier, data: GakumasConfig = GakumasConfig()) {
|
||||
AboutPage(modifier, previewData = data)
|
||||
}
|
@ -0,0 +1,397 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.pages.subPages
|
||||
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuGroupBox
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import io.github.chinosk.gakumas.localify.MainActivity
|
||||
import io.github.chinosk.gakumas.localify.R
|
||||
import io.github.chinosk.gakumas.localify.getConfigState
|
||||
import io.github.chinosk.gakumas.localify.models.BreastCollapsibleBoxViewModel
|
||||
import io.github.chinosk.gakumas.localify.models.BreastCollapsibleBoxViewModelFactory
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.ui.components.base.CollapsibleBox
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuButton
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuSwitch
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuTextInput
|
||||
|
||||
|
||||
@Composable
|
||||
fun AdvanceSettingsPage(modifier: Modifier = Modifier,
|
||||
context: MainActivity? = null,
|
||||
previewData: GakumasConfig? = null,
|
||||
bottomSpacerHeight: Dp = 120.dp,
|
||||
screenH: Dp = 1080.dp) {
|
||||
val config = getConfigState(context, previewData)
|
||||
// val scrollState = rememberScrollState()
|
||||
|
||||
val breastParamViewModel: BreastCollapsibleBoxViewModel =
|
||||
viewModel(factory = BreastCollapsibleBoxViewModelFactory(initiallyExpanded = false))
|
||||
val keyBoardOptionsDecimal = remember {
|
||||
KeyboardOptions(keyboardType = KeyboardType.Decimal)
|
||||
}
|
||||
|
||||
LazyColumn(modifier = modifier
|
||||
.sizeIn(maxHeight = screenH)
|
||||
// .fillMaxHeight()
|
||||
// .verticalScroll(scrollState)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
GakuGroupBox(modifier, stringResource(R.string.camera_settings)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
GakuSwitch(modifier, stringResource(R.string.enable_free_camera), checked = config.value.enableFreeCamera) {
|
||||
v -> context?.onEnableFreeCameraChanged(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(6.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
GakuGroupBox(modifier, stringResource(R.string.debug_settings)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
GakuSwitch(modifier, stringResource(R.string.text_hook_test_mode), checked = config.value.textTest) {
|
||||
v -> context?.onTextTestChanged(v)
|
||||
}
|
||||
|
||||
GakuSwitch(modifier, stringResource(R.string.export_text), checked = config.value.dumpText) {
|
||||
v -> context?.onDumpTextChanged(v)
|
||||
}
|
||||
|
||||
GakuSwitch(modifier, stringResource(R.string.force_export_resource), checked = config.value.forceExportResource) {
|
||||
v -> context?.onForceExportResourceChanged(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(6.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
GakuGroupBox(modifier, stringResource(R.string.breast_param),
|
||||
contentPadding = 0.dp,
|
||||
onHeadClick = {
|
||||
breastParamViewModel.expanded = !breastParamViewModel.expanded
|
||||
}) {
|
||||
CollapsibleBox(modifier = modifier,
|
||||
viewModel = breastParamViewModel
|
||||
) {
|
||||
LazyColumn(modifier = modifier
|
||||
.padding(8.dp)
|
||||
.sizeIn(maxHeight = screenH),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
GakuSwitch(modifier = modifier,
|
||||
checked = config.value.enableBreastParam,
|
||||
text = stringResource(R.string.enable_breast_param)
|
||||
) { v -> context?.onEnableBreastParamChanged(v) }
|
||||
}
|
||||
item {
|
||||
Row(modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
val buttonModifier = remember {
|
||||
modifier
|
||||
.height(40.dp)
|
||||
.weight(1f)
|
||||
}
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = "??", onClick = { context?.onBClickPresetChanged(5) })
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = "+5", onClick = { context?.onBClickPresetChanged(4) })
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = "+4", onClick = { context?.onBClickPresetChanged(3) })
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = "+3", onClick = { context?.onBClickPresetChanged(2) })
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = "+2", onClick = { context?.onBClickPresetChanged(1) })
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = "+1", onClick = { context?.onBClickPresetChanged(0) })
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp)
|
||||
.weight(1f),
|
||||
fontSize = 14f,
|
||||
value = config.value.bDamping.toString(),
|
||||
onValueChange = { c -> context?.onBDampingChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.damping)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp)
|
||||
.weight(1f),
|
||||
fontSize = 14f,
|
||||
value = config.value.bStiffness.toString(),
|
||||
onValueChange = { c -> context?.onBStiffnessChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.stiffness)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp)
|
||||
.weight(1f),
|
||||
fontSize = 14f,
|
||||
value = config.value.bSpring.toString(),
|
||||
onValueChange = { c -> context?.onBSpringChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.spring)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp)
|
||||
.weight(1f),
|
||||
fontSize = 14f,
|
||||
value = config.value.bPendulum.toString(),
|
||||
onValueChange = { c -> context?.onBPendulumChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.pendulum)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp)
|
||||
.weight(1f),
|
||||
fontSize = 14f,
|
||||
value = config.value.bPendulumRange.toString(),
|
||||
onValueChange = { c -> context?.onBPendulumRangeChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.pendulumrange)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp)
|
||||
.weight(1f),
|
||||
fontSize = 14f,
|
||||
value = config.value.bAverage.toString(),
|
||||
onValueChange = { c -> context?.onBAverageChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.average)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp)
|
||||
.fillMaxWidth(),
|
||||
fontSize = 14f,
|
||||
value = config.value.bRootWeight.toString(),
|
||||
onValueChange = { c -> context?.onBRootWeightChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.rootweight)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
GakuSwitch(modifier = modifier,
|
||||
checked = config.value.bUseScale,
|
||||
leftPart = {
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp),
|
||||
fontSize = 14f,
|
||||
value = config.value.bScale.toString(),
|
||||
onValueChange = { c -> context?.onBScaleChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.breast_scale)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
}
|
||||
) { v -> context?.onBUseScaleChanged(v) }
|
||||
}
|
||||
|
||||
item {
|
||||
GakuSwitch(modifier = modifier,
|
||||
checked = config.value.bUseArmCorrection,
|
||||
text = stringResource(R.string.usearmcorrection)
|
||||
) { v -> context?.onBUseArmCorrectionChanged(v) }
|
||||
}
|
||||
|
||||
item {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
GakuSwitch(modifier = modifier,
|
||||
checked = config.value.bUseLimit,
|
||||
text = stringResource(R.string.uselimit_0_1)
|
||||
) { v ->
|
||||
context?.onBUseLimitChanged(v)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
CollapsibleBox(modifier = modifier,
|
||||
expandState = config.value.bUseLimit,
|
||||
collapsedHeight = 0.dp,
|
||||
showExpand = false
|
||||
){
|
||||
Row(modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
val textInputModifier = remember {
|
||||
modifier
|
||||
.height(45.dp)
|
||||
.weight(1f)
|
||||
}
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.bLimitXx.toString(),
|
||||
onValueChange = { c -> context?.onBLimitXxChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.axisx_x)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.bLimitYx.toString(),
|
||||
onValueChange = { c -> context?.onBLimitYxChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.axisy_x)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.bLimitZx.toString(),
|
||||
onValueChange = { c -> context?.onBLimitZxChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.axisz_x)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
}
|
||||
|
||||
Row(modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
val textInputModifier = remember {
|
||||
modifier
|
||||
.height(45.dp)
|
||||
.weight(1f)
|
||||
}
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.bLimitXy.toString(),
|
||||
onValueChange = { c -> context?.onBLimitXyChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.axisx_y)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.bLimitYy.toString(),
|
||||
onValueChange = { c -> context?.onBLimitYyChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.axisy_y)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.bLimitZy.toString(),
|
||||
onValueChange = { c -> context?.onBLimitZyChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.axisz_y)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (config.value.dbgMode) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
|
||||
GakuGroupBox(modifier, stringResource(R.string.test_mode_live)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
GakuSwitch(modifier, stringResource(R.string.unlockAllLive),
|
||||
checked = config.value.unlockAllLive) {
|
||||
v -> context?.onUnlockAllLiveChanged(v)
|
||||
}
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
|
||||
)
|
||||
GakuSwitch(modifier, stringResource(R.string.liveUseCustomeDress),
|
||||
checked = config.value.enableLiveCustomeDress) {
|
||||
v -> context?.onLiveCustomeDressChanged(v)
|
||||
}
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp)
|
||||
.fillMaxWidth(),
|
||||
fontSize = 14f,
|
||||
value = config.value.liveCustomeHeadId,
|
||||
onValueChange = { c -> context?.onLiveCustomeHeadIdChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.live_costume_head_id),
|
||||
fontSize = 12.sp) }
|
||||
)
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp)
|
||||
.fillMaxWidth(),
|
||||
fontSize = 14f,
|
||||
value = config.value.liveCustomeCostumeId,
|
||||
onValueChange = { c -> context?.onLiveCustomeCostumeIdChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.live_custome_dress_id)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = modifier.height(bottomSpacerHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
fun AdvanceSettingsPagePreview(modifier: Modifier = Modifier, data: GakumasConfig = GakumasConfig()) {
|
||||
AdvanceSettingsPage(modifier, previewData = data)
|
||||
}
|
@ -0,0 +1,476 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.pages.subPages
|
||||
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuGroupBox
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import io.github.chinosk.gakumas.localify.MainActivity
|
||||
import io.github.chinosk.gakumas.localify.R
|
||||
import io.github.chinosk.gakumas.localify.getConfigState
|
||||
import io.github.chinosk.gakumas.localify.getProgramConfigState
|
||||
import io.github.chinosk.gakumas.localify.getProgramDownloadAbleState
|
||||
import io.github.chinosk.gakumas.localify.getProgramDownloadErrorStringState
|
||||
import io.github.chinosk.gakumas.localify.getProgramDownloadState
|
||||
import io.github.chinosk.gakumas.localify.getProgramLocalResourceVersionState
|
||||
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.FileDownloader
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.models.ResourceCollapsibleBoxViewModel
|
||||
import io.github.chinosk.gakumas.localify.models.ResourceCollapsibleBoxViewModelFactory
|
||||
import io.github.chinosk.gakumas.localify.ui.components.base.CollapsibleBox
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuButton
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuProgressBar
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuRadio
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuSwitch
|
||||
import io.github.chinosk.gakumas.localify.ui.components.GakuTextInput
|
||||
import java.io.File
|
||||
|
||||
|
||||
@Composable
|
||||
fun HomePage(modifier: Modifier = Modifier,
|
||||
context: MainActivity? = null,
|
||||
previewData: GakumasConfig? = null,
|
||||
bottomSpacerHeight: Dp = 120.dp,
|
||||
screenH: Dp = 1080.dp) {
|
||||
val config = getConfigState(context, previewData)
|
||||
val programConfig = getProgramConfigState(context)
|
||||
|
||||
val downloadProgress by getProgramDownloadState(context)
|
||||
val downloadAble by getProgramDownloadAbleState(context)
|
||||
val localResourceVersion by getProgramLocalResourceVersionState(context)
|
||||
val downloadErrorString by getProgramDownloadErrorStringState(context)
|
||||
|
||||
// val scrollState = rememberScrollState()
|
||||
val keyboardOptionsNumber = remember {
|
||||
KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
}
|
||||
val keyBoardOptionsDecimal = remember {
|
||||
KeyboardOptions(keyboardType = KeyboardType.Decimal)
|
||||
}
|
||||
|
||||
val resourceSettingsViewModel: ResourceCollapsibleBoxViewModel =
|
||||
viewModel(factory = ResourceCollapsibleBoxViewModelFactory(initiallyExpanded = false))
|
||||
|
||||
fun onClickDownload() {
|
||||
context?.mainPageAssetsViewDataUpdate(
|
||||
downloadAbleState = false,
|
||||
errorString = "",
|
||||
downloadProgressState = -1f
|
||||
)
|
||||
val (_, newUrl) = FileDownloader.checkAndChangeDownloadURL(programConfig.value.transRemoteZipUrl)
|
||||
context?.onPTransRemoteZipUrlChanged(newUrl, 0, 0, 0)
|
||||
FileDownloader.downloadFile(
|
||||
newUrl,
|
||||
checkContentTypes = listOf("application/zip", "application/octet-stream"),
|
||||
onDownload = { progress, _, _ ->
|
||||
context?.mainPageAssetsViewDataUpdate(downloadProgressState = progress)
|
||||
},
|
||||
|
||||
onSuccess = { byteArray ->
|
||||
context?.mainPageAssetsViewDataUpdate(
|
||||
downloadAbleState = true,
|
||||
errorString = "",
|
||||
downloadProgressState = -1f
|
||||
)
|
||||
val file = File(context?.filesDir, "update_trans.zip")
|
||||
file.writeBytes(byteArray)
|
||||
val newFileVersion = FileHotUpdater.getZipResourceVersion(file.absolutePath)
|
||||
if (newFileVersion != null) {
|
||||
context?.mainPageAssetsViewDataUpdate(
|
||||
localResourceVersionState = newFileVersion
|
||||
)
|
||||
}
|
||||
else {
|
||||
context?.mainPageAssetsViewDataUpdate(
|
||||
localResourceVersionState = context.getString(
|
||||
R.string.invalid_zip_file
|
||||
),
|
||||
errorString = context.getString(R.string.invalid_zip_file_warn)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
onFailed = { code, reason ->
|
||||
context?.mainPageAssetsViewDataUpdate(
|
||||
downloadAbleState = true,
|
||||
errorString = reason,
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
LazyColumn(modifier = modifier
|
||||
.sizeIn(maxHeight = screenH)
|
||||
// .fillMaxHeight()
|
||||
// .verticalScroll(scrollState)
|
||||
// .width(IntrinsicSize.Max)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
GakuGroupBox(modifier = modifier, stringResource(R.string.basic_settings)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
GakuSwitch(modifier, stringResource(R.string.enable_plugin), checked = config.value.enabled) {
|
||||
v -> context?.onEnabledChanged(v)
|
||||
}
|
||||
|
||||
GakuSwitch(modifier, stringResource(R.string.replace_font), checked = config.value.replaceFont) {
|
||||
v -> context?.onReplaceFontChanged(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
GakuGroupBox(modifier, stringResource(R.string.resource_settings),
|
||||
contentPadding = 0.dp,
|
||||
onHeadClick = {
|
||||
resourceSettingsViewModel.expanded = !resourceSettingsViewModel.expanded
|
||||
}) {
|
||||
CollapsibleBox(modifier = modifier,
|
||||
viewModel = resourceSettingsViewModel
|
||||
) {
|
||||
LazyColumn(modifier = modifier
|
||||
// .padding(8.dp)
|
||||
.sizeIn(maxHeight = screenH),
|
||||
// verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
GakuSwitch(modifier = modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp),
|
||||
checked = programConfig.value.checkBuiltInAssets,
|
||||
text = stringResource(id = R.string.check_built_in_resource)
|
||||
) { v -> context?.onPCheckBuiltInAssetsChanged(v) }
|
||||
}
|
||||
item {
|
||||
GakuSwitch(modifier = modifier.padding(start = 8.dp, end = 8.dp),
|
||||
checked = programConfig.value.cleanLocalAssets,
|
||||
text = stringResource(id = R.string.delete_plugin_resource)
|
||||
) { v -> context?.onPCleanLocalAssetsChanged(v) }
|
||||
}
|
||||
|
||||
item {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
GakuSwitch(modifier = modifier.padding(start = 8.dp, end = 8.dp),
|
||||
checked = programConfig.value.useRemoteAssets,
|
||||
text = stringResource(id = R.string.use_remote_zip_resource)
|
||||
) { v -> context?.onPUseRemoteAssetsChanged(v) }
|
||||
|
||||
CollapsibleBox(modifier = modifier.graphicsLayer(clip = false),
|
||||
expandState = programConfig.value.useRemoteAssets,
|
||||
collapsedHeight = 0.dp,
|
||||
innerPaddingLeftRight = 8.dp,
|
||||
showExpand = false
|
||||
) {
|
||||
GakuSwitch(modifier = modifier,
|
||||
checked = programConfig.value.delRemoteAfterUpdate,
|
||||
text = stringResource(id = R.string.del_remote_after_update)
|
||||
) { v -> context?.onPDelRemoteAfterUpdateChanged(v) }
|
||||
|
||||
LazyColumn(modifier = modifier
|
||||
// .padding(8.dp)
|
||||
.sizeIn(maxHeight = screenH),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
Row(modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
GakuTextInput(modifier = modifier
|
||||
.height(45.dp)
|
||||
.padding(end = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
fontSize = 14f,
|
||||
value = programConfig.value.transRemoteZipUrl,
|
||||
onValueChange = { c -> context?.onPTransRemoteZipUrlChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(id = R.string.resource_url)) },
|
||||
keyboardOptions = keyboardOptionsNumber)
|
||||
|
||||
if (downloadAble) {
|
||||
GakuButton(modifier = modifier
|
||||
.height(40.dp)
|
||||
.sizeIn(minWidth = 80.dp),
|
||||
text = stringResource(id = R.string.download),
|
||||
onClick = { onClickDownload() })
|
||||
}
|
||||
else {
|
||||
GakuButton(modifier = modifier
|
||||
.height(40.dp)
|
||||
.sizeIn(minWidth = 80.dp),
|
||||
text = stringResource(id = R.string.cancel), onClick = {
|
||||
FileDownloader.cancel()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadProgress >= 0) {
|
||||
item {
|
||||
GakuProgressBar(progress = downloadProgress, isError = downloadErrorString.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadErrorString.isNotEmpty()) {
|
||||
item {
|
||||
Text(text = downloadErrorString, color = Color(0xFFE2041B))
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Text(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val file =
|
||||
File(context?.filesDir, "update_trans.zip")
|
||||
context?.mainPageAssetsViewDataUpdate(
|
||||
localResourceVersionState = FileHotUpdater
|
||||
.getZipResourceVersion(file.absolutePath)
|
||||
.toString()
|
||||
)
|
||||
}, text = "${stringResource(R.string.downloaded_resource_version)}: $localResourceVersion")
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(Modifier.height(0.dp))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(6.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
GakuGroupBox(modifier = modifier, contentPadding = 0.dp, title = stringResource(R.string.graphic_settings)) {
|
||||
LazyColumn(modifier = Modifier
|
||||
.sizeIn(maxHeight = screenH),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
GakuTextInput(modifier = modifier
|
||||
.padding(start = 4.dp, end = 4.dp)
|
||||
.height(45.dp)
|
||||
.fillMaxWidth(),
|
||||
fontSize = 14f,
|
||||
value = config.value.targetFrameRate.toString(),
|
||||
onValueChange = { c -> context?.onTargetFpsChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.setFpsTitle)) },
|
||||
keyboardOptions = keyboardOptionsNumber)
|
||||
}
|
||||
|
||||
item {
|
||||
Column(modifier = Modifier.padding(start = 8.dp, end = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(stringResource(R.string.orientation_lock))
|
||||
Row(modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
val radioModifier = remember {
|
||||
modifier
|
||||
.height(40.dp)
|
||||
.weight(1f)
|
||||
}
|
||||
|
||||
GakuRadio(modifier = radioModifier,
|
||||
text = stringResource(R.string.orientation_orig), selected = config.value.gameOrientation == 0,
|
||||
onClick = { context?.onGameOrientationChanged(0) })
|
||||
|
||||
GakuRadio(modifier = radioModifier,
|
||||
text = stringResource(R.string.orientation_portrait), selected = config.value.gameOrientation == 1,
|
||||
onClick = { context?.onGameOrientationChanged(1) })
|
||||
|
||||
GakuRadio(modifier = radioModifier,
|
||||
text = stringResource(R.string.orientation_landscape), selected = config.value.gameOrientation == 2,
|
||||
onClick = { context?.onGameOrientationChanged(2) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
GakuSwitch(modifier.padding(start = 8.dp, end = 8.dp),
|
||||
stringResource(R.string.useCustomeGraphicSettings),
|
||||
checked = config.value.useCustomeGraphicSettings) {
|
||||
v -> context?.onUseCustomeGraphicSettingsChanged(v)
|
||||
}
|
||||
|
||||
CollapsibleBox(modifier = modifier,
|
||||
expandState = config.value.useCustomeGraphicSettings,
|
||||
collapsedHeight = 0.dp,
|
||||
showExpand = false
|
||||
) {
|
||||
LazyColumn(modifier = modifier
|
||||
.padding(8.dp)
|
||||
.sizeIn(maxHeight = screenH)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
Row(modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
val buttonModifier = remember {
|
||||
modifier
|
||||
.height(40.dp)
|
||||
.weight(1f)
|
||||
}
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = stringResource(R.string.max_high), onClick = { context?.onChangePresetQuality(4) })
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = stringResource(R.string.very_high), onClick = { context?.onChangePresetQuality(3) })
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = stringResource(R.string.hign), onClick = { context?.onChangePresetQuality(2) })
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = stringResource(R.string.middle), onClick = { context?.onChangePresetQuality(1) })
|
||||
|
||||
GakuButton(modifier = buttonModifier,
|
||||
text = stringResource(R.string.low), onClick = { context?.onChangePresetQuality(0) })
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
val textInputModifier = remember {
|
||||
modifier
|
||||
.height(45.dp)
|
||||
.weight(1f)
|
||||
}
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.renderScale.toString(),
|
||||
onValueChange = { c -> context?.onRenderScaleChanged(c, 0, 0, 0)},
|
||||
label = { Text(stringResource(R.string.renderscale)) },
|
||||
keyboardOptions = keyBoardOptionsDecimal)
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.qualitySettingsLevel.toString(),
|
||||
onValueChange = { c -> context?.onQualitySettingsLevelChanged(c, 0, 0, 0)},
|
||||
label = { Text("QualityLevel (1/1/2/3/5)") },
|
||||
keyboardOptions = keyboardOptionsNumber)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
val textInputModifier = remember {
|
||||
modifier
|
||||
.height(45.dp)
|
||||
.weight(1f)
|
||||
}
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.volumeIndex.toString(),
|
||||
onValueChange = { c -> context?.onVolumeIndexChanged(c, 0, 0, 0)},
|
||||
label = { Text("VolumeIndex (0/1/2/3/4)") },
|
||||
keyboardOptions = keyboardOptionsNumber)
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.maxBufferPixel.toString(),
|
||||
onValueChange = { c -> context?.onMaxBufferPixelChanged(c, 0, 0, 0)},
|
||||
label = { Text("MaxBufferPixel (1024/1440/2538/3384/8190)", fontSize = 10.sp) },
|
||||
keyboardOptions = keyboardOptionsNumber)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
val textInputModifier = remember {
|
||||
modifier
|
||||
.height(45.dp)
|
||||
.weight(1f)
|
||||
}
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.reflectionQualityLevel.toString(),
|
||||
onValueChange = { c -> context?.onReflectionQualityLevelChanged(c, 0, 0, 0)},
|
||||
label = { Text( text = "ReflectionLevel (0~5)") },
|
||||
keyboardOptions = keyboardOptionsNumber)
|
||||
|
||||
GakuTextInput(modifier = textInputModifier,
|
||||
fontSize = 14f,
|
||||
value = config.value.lodQualityLevel.toString(),
|
||||
onValueChange = { c -> context?.onLodQualityLevelChanged(c, 0, 0, 0)},
|
||||
label = { Text("LOD Level (0~5)") },
|
||||
keyboardOptions = keyboardOptionsNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = modifier.height(bottomSpacerHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
fun HomePagePreview(modifier: Modifier = Modifier, data: GakumasConfig = GakumasConfig()) {
|
||||
HomePage(modifier, previewData = data)
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package io.github.chinosk.gakumas.localify.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Color(0xFFF89400),
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GakumasLocalifyTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = colorScheme.primary.toArgb()
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
BIN
app/src/main/res/drawable-hdpi/splash.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
BIN
app/src/main/res/drawable-mdpi/splash.png
Normal file
After Width: | Height: | Size: 650 KiB |
BIN
app/src/main/res/drawable-xhdpi/splash.png
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
app/src/main/res/drawable-xxhdpi/splash.png
Normal file
After Width: | Height: | Size: 5.1 MiB |
BIN
app/src/main/res/drawable/bg_h1.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
app/src/main/res/drawable/bg_pattern.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
app/src/main/res/drawable/splash.png
Normal file
After Width: | Height: | Size: 2.6 MiB |
13
app/src/main/res/drawable/splash_style.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/white" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:src="@drawable/splash"
|
||||
android:gravity="center"
|
||||
android:tileMode="disabled"/>
|
||||
</item>
|
||||
</layer-list>
|
@ -43,5 +43,36 @@
|
||||
<string name="axisx_y">axisX.y</string>
|
||||
<string name="axisy_y">axisY.y</string>
|
||||
<string name="axisz_y">axisZ.y</string>
|
||||
<string name="basic_settings">Basic Ssettings</string>
|
||||
<string name="graphic_settings">Graphic Settings</string>
|
||||
<string name="camera_settings">Camera Settings</string>
|
||||
<string name="test_mode_live">Test Mode - LIVE</string>
|
||||
<string name="debug_settings">Debug Settings</string>
|
||||
<string name="breast_param">Breast Parameters</string>
|
||||
<string name="about">About</string>
|
||||
<string name="home">Home</string>
|
||||
<string name="advanced_settings">Advanced</string>
|
||||
<string name="about_warn_title">WARNING</string>
|
||||
<string name="about_warn_p1">This plugin is for learning and communication only.</string>
|
||||
<string name="about_warn_p2">Using external plugin against the relevant TOS so proceed at your own risk.</string>
|
||||
<string name="about_about_title">About This Plugin</string>
|
||||
<string name="about_about_p1">This plugin is completely free. If you paid for this plugin, please report the seller.</string>
|
||||
<string name="about_about_p2">Plugin QQ group: 975854705</string>
|
||||
<string name="project_contribution">Project Contribution</string>
|
||||
<string name="plugin_code">Plugin Code</string>
|
||||
<string name="contributors">Contributors</string>
|
||||
<string name="translation_repository">Translation Repository</string>
|
||||
<string name="resource_settings">Resource Settings</string>
|
||||
<string name="check_built_in_resource">Check Built-in Assets Update</string>
|
||||
<string name="delete_plugin_resource">Delete Plugin Resource</string>
|
||||
<string name="use_remote_zip_resource">Use Remote ZIP Resource</string>
|
||||
<string name="resource_url">Resource URL</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="invalid_zip_file">Invalid file</string>
|
||||
<string name="invalid_zip_file_warn">This file is not a valid ZIP translation resource pack.</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="downloaded_resource_version">Downloaded Version</string>
|
||||
<string name="del_remote_after_update">Delete Cache File After Update</string>
|
||||
|
||||
<string name="about_contributors_asset_file">about_contributors_en.json</string>
|
||||
</resources>
|
@ -1,5 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.GakumasLocalify" parent="@style/Theme.Material3.DynamicColors.DayNight.NoActionBar" />
|
||||
<style name="Theme.GakumasLocalify" parent="@style/Theme.MaterialComponents.Light.NoActionBar" >
|
||||
<item name="android:windowBackground">@drawable/splash_style</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.GakumasLocalify.NoDisplay" parent="@style/Theme.MaterialComponents.Light.NoActionBar" >
|
||||
<item name="android:windowFrame">@null</item>
|
||||
<!-- 设置是否可滑动 -->
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<!-- 设置是否透明 -->
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<!-- 无标题 -->
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<!-- 背景 -->
|
||||
<item name="android:background">@null</item>
|
||||
<!-- 窗口背景 -->
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<!-- 是否变暗 -->
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
<!-- 点击空白部分activity不消失 -->
|
||||
<item name="android:windowCloseOnTouchOutside">true</item>
|
||||
<!-- 无标题 有的手机设置这行代码-->
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowAnimationStyle">@null</item>
|
||||
<item name="android:windowDisablePreview">true</item>
|
||||
<item name="android:windowNoDisplay">true</item>
|
||||
</style>
|
||||
</resources>
|
4
app/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<files-path name="files" path="." />
|
||||
</paths>
|
@ -1,5 +1,4 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.4.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||
alias(libs.plugins.androidApplication) apply false
|
||||
alias(libs.plugins.kotlinAndroid) apply false
|
||||
}
|
54
gradle/libs.versions.toml
Normal file
@ -0,0 +1,54 @@
|
||||
[versions]
|
||||
accompanistPager = "0.30.0"
|
||||
activityCompose = "1.9.0"
|
||||
okhttpBom = "4.12.0"
|
||||
xposedApi = "82"
|
||||
appcompat = "1.7.0"
|
||||
coil = "2.6.0"
|
||||
composeBom = "2024.06.00"
|
||||
agp = "8.5.0"
|
||||
coreKtx = "1.13.1"
|
||||
kotlin = "2.0.0"
|
||||
lifecycle = "2.8.2"
|
||||
material = "1.12.0"
|
||||
navigationCompose = "2.7.7"
|
||||
xdl = "2.1.1"
|
||||
shadowhook = "1.0.9"
|
||||
serialization="1.7.1"
|
||||
|
||||
[libraries]
|
||||
accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanistPager" }
|
||||
accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanistPager" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
||||
androidx-animation = { module = "androidx.compose.animation:animation" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
|
||||
androidx-foundation = { module = "androidx.compose.foundation:foundation" }
|
||||
androidx-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
|
||||
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
androidx-material = { module = "androidx.compose.material:material" }
|
||||
androidx-material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-runtime = { module = "androidx.compose.runtime:runtime" }
|
||||
androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
|
||||
androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||
androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
||||
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" }
|
||||
xposed-api = { module = "de.robv.android.xposed:api", version.ref = "xposedApi" }
|
||||
coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" }
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||
shadowhook = { module = "com.bytedance.android:shadowhook", version.ref = "shadowhook" }
|
||||
xdl = { module = "io.github.hexhacking:xdl", version.ref = "xdl" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,7 @@
|
||||
#Fri May 17 13:19:52 CST 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
294
gradlew
vendored
Normal file → Executable file
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -17,67 +17,99 @@
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@ -98,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
31
gradlew.bat
vendored
@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
@ -1,10 +1,17 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
|