diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e929c1..f778c86 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: | diff --git a/.gitignore b/.gitignore index c705b33..5dfcbbd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ /app/debug /app/release /local.properties +/.vs +/.kotlin +/app/debug +/app/release diff --git a/app/build.gradle b/app/build.gradle index f6fe7e8..169bec2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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) } \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb43..774e59c 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,9 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +-keep class io.github.chinosk.gakumas.localify.GakumasHookMain { + (); + native ; +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a110088..2335f9b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + @@ -40,6 +42,23 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/about_contributors_en.json b/app/src/main/assets/about_contributors_en.json new file mode 100644 index 0000000..1e4b825 --- /dev/null +++ b/app/src/main/assets/about_contributors_en.json @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/assets/about_contributors_zh_cn.json b/app/src/main/assets/about_contributors_zh_cn.json new file mode 100644 index 0000000..d825d11 --- /dev/null +++ b/app/src/main/assets/about_contributors_zh_cn.json @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/cpp/GakumasLocalify/BaseDefine.h b/app/src/main/cpp/GakumasLocalify/BaseDefine.h index 5b5cc2d..18a5153 100644 --- a/app/src/main/cpp/GakumasLocalify/BaseDefine.h +++ b/app/src/main/cpp/GakumasLocalify/BaseDefine.h @@ -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 \ No newline at end of file diff --git a/app/src/main/cpp/GakumasLocalify/Hook.cpp b/app/src/main/cpp/GakumasLocalify/Hook.cpp index 01b4fa9..cc29b4f 100644 --- a/app/src/main/cpp/GakumasLocalify/Hook.cpp +++ b/app/src/main/cpp/GakumasLocalify/Hook.cpp @@ -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(obj); @@ -109,18 +109,18 @@ namespace GakumasLocal::HookMain { return GetResolution->Invoke(); } - 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(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( 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 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 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(fontAsset); } - set_font->Invoke(TMP_Text_this, fontAsset); + set_font->Invoke(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("get_text"); const auto set_Text_method = TMP_Text_klass->Get("set_text"); - const auto currText = get_Text_method->Invoke(_this); + const auto currText = get_Text_method->Invoke(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(_this, UnityResolve::UnityType::String::New("[TA]" + transText)); + set_Text_method->Invoke(self, UnityResolve::UnityType::String::New("[TA]" + transText)); } else { - set_Text_method->Invoke(_this, UnityResolve::UnityType::String::New(transText)); + set_Text_method->Invoke(self, UnityResolve::UnityType::String::New(transText)); } } } - // set_font->Invoke(_this, font); - UpdateFont(_this); - TextMeshProUGUI_Awake_Orig(_this, method); + // set_font->Invoke(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( 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(); PictureBookLiveSelectMusicListItemModel_ctor->Invoke(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(_this, values[Config::lodQualityLevel]); - SetReflectionQuality->Invoke(_this, values[Config::reflectionQualityLevel]); + SetLODQuality->Invoke(self, values[Config::lodQualityLevel]); + SetReflectionQuality->Invoke(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 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("_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( 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(_this, rootBody_field); + auto rootBody = Il2cppUtils::ClassGetFieldValue(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", diff --git a/app/src/main/cpp/GakumasLocalify/Log.cpp b/app/src/main/cpp/GakumasLocalify/Log.cpp index 529e962..c857f36 100644 --- a/app/src/main/cpp/GakumasLocalify/Log.cpp +++ b/app/src/main/cpp/GakumasLocalify/Log.cpp @@ -4,6 +4,7 @@ #include #include #include +#include extern JavaVM* g_javaVM; extern jclass g_gakumasHookMainClass; @@ -24,9 +25,13 @@ extern jmethodID showToastMethodId; namespace GakumasLocal::Log { + namespace { + std::queue 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"); + } + } } diff --git a/app/src/main/cpp/GakumasLocalify/Log.h b/app/src/main/cpp/GakumasLocalify/Log.h index 429f886..d01c2eb 100644 --- a/app/src/main/cpp/GakumasLocalify/Log.h +++ b/app/src/main/cpp/GakumasLocalify/Log.h @@ -2,6 +2,7 @@ #define GAKUMAS_LOCALIFY_LOG_H #include +#include 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 diff --git a/app/src/main/cpp/GakumasLocalify/camera/baseCamera.cpp b/app/src/main/cpp/GakumasLocalify/camera/baseCamera.cpp index beb4a8e..8733231 100644 --- a/app/src/main/cpp/GakumasLocalify/camera/baseCamera.cpp +++ b/app/src/main/cpp/GakumasLocalify/camera/baseCamera.cpp @@ -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) { diff --git a/app/src/main/cpp/GakumasLocalify/camera/baseCamera.hpp b/app/src/main/cpp/GakumasLocalify/camera/baseCamera.hpp index 20051b2..2cfdebb 100644 --- a/app/src/main/cpp/GakumasLocalify/camera/baseCamera.hpp +++ b/app/src/main/cpp/GakumasLocalify/camera/baseCamera.hpp @@ -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); diff --git a/app/src/main/cpp/GakumasLocalify/camera/camera.cpp b/app/src/main/cpp/GakumasLocalify/camera/camera.cpp index 879afe9..5ac799e 100644 --- a/app/src/main/cpp/GakumasLocalify/camera/camera.cpp +++ b/app/src/main/cpp/GakumasLocalify/camera/camera.cpp @@ -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,14 +489,50 @@ 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(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(this); + + // 遍历对象的每个字节 + for (size_t offset = 0; offset < sizeof(*this); ) { + if (offset + sizeof(bool) <= sizeof(*this) && reinterpret_cast(p + offset) == reinterpret_cast(this) + offset / sizeof(bool)) { + // 如果当前偏移量适用于 bool 类型,则将其设置为 false + *reinterpret_cast(p + offset) = false; + offset += sizeof(bool); + } else if (offset + sizeof(float) <= sizeof(*this) && reinterpret_cast(p + offset) == reinterpret_cast(this) + offset / sizeof(float)) { + // 如果当前偏移量适用于 float 类型,则将其设置为 0.0 + *reinterpret_cast(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(); diff --git a/app/src/main/cpp/GakumasLocalify/camera/camera.hpp b/app/src/main/cpp/GakumasLocalify/camera/camera.hpp index 0b410e8..d4b035e 100644 --- a/app/src/main/cpp/GakumasLocalify/camera/camera.hpp +++ b/app/src/main/cpp/GakumasLocalify/camera/camera.hpp @@ -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(); } diff --git a/app/src/main/cpp/deps/Joystick/JoystickEvent.h b/app/src/main/cpp/deps/Joystick/JoystickEvent.h new file mode 100644 index 0000000..422af8d --- /dev/null +++ b/app/src/main/cpp/deps/Joystick/JoystickEvent.h @@ -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 \ No newline at end of file diff --git a/app/src/main/cpp/libMarryKotone.cpp b/app/src/main/cpp/libMarryKotone.cpp index 6ce2560..d63b360 100644 --- a/app/src/main/cpp/libMarryKotone.cpp +++ b/app/src/main/cpp/libMarryKotone.cpp @@ -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, @@ -94,4 +111,11 @@ Java_io_github_chinosk_gakumas_localify_GakumasHookMain_loadConfig(JNIEnv *env, const auto configJsonStrChars = env->GetStringUTFChars(config_json_str, nullptr); 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); } \ No newline at end of file diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ActivityExtends.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ActivityExtends.kt new file mode 100644 index 0000000..55c5c37 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ActivityExtends.kt @@ -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 : IHasConfigItems + + +fun 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.getProgramConfigContent( + excludes: List = 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(configFile.readText()) + json.encodeToString(ProgramConfigSerializer(excludes), parsedConfig) + } else { + "{}" + } + } else { + json.encodeToString(ProgramConfigSerializer(excludes), origProgramConfig) + } + } +} + +fun T.loadConfig() where T : Activity, T : IHasConfigItems { + val configStr = getConfigContent() + config = try { + json.decodeFromString(configStr) + } catch (e: SerializationException) { + Toast.makeText(this, "配置文件异常: $e", Toast.LENGTH_SHORT).show() + GakumasConfig() + } + saveConfig() + + val programConfigStr = getProgramConfigContent() + programConfig = try { + json.decodeFromString(programConfigStr) + } catch (e: SerializationException) { + ProgramConfig() + } +} + +fun 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) +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt index 38560e5..1b962fe 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt @@ -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 create(modelClass: Class): 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 = 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() } -} \ No newline at end of file + 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 } + } +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt index 56a37c3..e4b7e00 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt @@ -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(gkmsData) } catch (e: Exception) { null } + val programConfig = try { + if (programData == null) { + ProgramConfig() + } else { + json.decodeFromString(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 { diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt index 8ca7bcb..fc363c2 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt @@ -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 { + 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(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(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(R.id.textViewTitle) - val versionLabel = findViewById(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 { + 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 - checkConfigAndUpdateView() - saveConfig() - showToast("Test Mode: ${!origDbg}") - } + val origDbg = config.dbgMode + config.dbgMode = !origDbg + checkConfigAndUpdateView() + saveConfig() + showToast("Test Mode: ${!origDbg}") } return if (event.action == 1145) true else super.dispatchKeyEvent(event) } -} \ No newline at end of file + + 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 { + 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 { + return if (context != null) { + context.programConfigViewModel.config.collectAsState() + } + else { + val configMSF = MutableStateFlow(previewData ?: ProgramConfig()) + configMSF.asStateFlow().collectAsState() + } +} + +@Composable +fun getProgramDownloadState(context: MainActivity?): State { + return if (context != null) { + context.programConfigViewModel.downloadProgress.collectAsState() + } + else { + val configMSF = MutableStateFlow(0f) + configMSF.asStateFlow().collectAsState() + } +} + +@Composable +fun getProgramDownloadAbleState(context: MainActivity?): State { + return if (context != null) { + context.programConfigViewModel.downloadAble.collectAsState() + } + else { + val configMSF = MutableStateFlow(true) + configMSF.asStateFlow().collectAsState() + } +} + +@Composable +fun getProgramLocalResourceVersionState(context: MainActivity?): State { + return if (context != null) { + context.programConfigViewModel.localResourceVersion.collectAsState() + } + else { + val configMSF = MutableStateFlow("null") + configMSF.asStateFlow().collectAsState() + } +} + +@Composable +fun getProgramDownloadErrorStringState(context: MainActivity?): State { + return if (context != null) { + context.programConfigViewModel.errorString.collectAsState() + } + else { + val configMSF = MutableStateFlow("") + configMSF.asStateFlow().collectAsState() + } +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/TranslucentActivity.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/TranslucentActivity.kt new file mode 100644 index 0000000..43213af --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/TranslucentActivity.kt @@ -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 { + 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() + } + } + } +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FileHotUpdater.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FileHotUpdater.kt new file mode 100644 index 0000000..b96dc99 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FileHotUpdater.kt @@ -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") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt index b62e567..61d72e7 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt @@ -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("{}") + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/MainKeyEventDispatcher.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/MainKeyEventDispatcher.kt index 4cc24fb..26b0182 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/MainKeyEventDispatcher.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/MainKeyEventDispatcher.kt @@ -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 diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt new file mode 100644 index 0000000..a67105a --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt @@ -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? = 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 { + + 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) + } +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/JsonHelper.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/JsonHelper.kt new file mode 100644 index 0000000..b8b6f10 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/JsonHelper.kt @@ -0,0 +1,7 @@ +package io.github.chinosk.gakumas.localify.mainUtils + +import kotlinx.serialization.json.Json + +val json = Json { + encodeDefaults = true +} \ No newline at end of file diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/models/AboutPageConfig.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/AboutPageConfig.kt new file mode 100644 index 0000000..e39f6e0 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/AboutPageConfig.kt @@ -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 = 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 +) + +@Serializable +data class ContribImg( + val plugin: String, + val translation: String +) + +@Serializable +data class Links( + val name: String, + val link: String +) diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt index f78a7e9..ee35c7e 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt @@ -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 ) diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt new file mode 100644 index 0000000..ea68ab1 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt @@ -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 = emptyList(), +) : KSerializer { + 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) + } +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt new file mode 100644 index 0000000..4a66d9a --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt @@ -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 create(modelClass: Class): 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 create(modelClass: Class): 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 create(modelClass: Class): 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 = configState.asStateFlow() + + val downloadProgressState = MutableStateFlow(-1f) + val downloadProgress: StateFlow = downloadProgressState.asStateFlow() + + val downloadAbleState = MutableStateFlow(true) + val downloadAble: StateFlow = downloadAbleState.asStateFlow() + + val localResourceVersionState = MutableStateFlow(initLocalResourceVersion) + val localResourceVersion: StateFlow = localResourceVersionState.asStateFlow() + + val errorStringState = MutableStateFlow("") + val errorString: StateFlow = errorStringState.asStateFlow() +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuButton.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuButton.kt new file mode 100644 index 0000000..8dc5279 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuButton.kt @@ -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) +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuGroupBox.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuGroupBox.kt new file mode 100644 index 0000000..98c2395 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuGroupBox.kt @@ -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.") + } + } +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuProgressBar.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuProgressBar.kt new file mode 100644 index 0000000..3d6ce9b --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuProgressBar.kt @@ -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) +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuRadio.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuRadio.kt new file mode 100644 index 0000000..2f430e1 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuRadio.kt @@ -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 = {}) +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuSwitch.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuSwitch.kt new file mode 100644 index 0000000..3852de9 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuSwitch.kt @@ -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) +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuTableRow.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuTableRow.kt new file mode 100644 index 0000000..9e0fe94 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuTableRow.kt @@ -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, + 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")) { _ -> } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuTextInput.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuTextInput.kt new file mode 100644 index 0000000..8a8aa33 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuTextInput.kt @@ -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") }) +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/AutoSizeText.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/AutoSizeText.kt new file mode 100644 index 0000000..2b58b82 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/AutoSizeText.kt @@ -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 + } + } + ) +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/CollapsibleBox.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/CollapsibleBox.kt new file mode 100644 index 0000000..bbc45aa --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/CollapsibleBox.kt @@ -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) {} +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/MainPage.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/MainPage.kt new file mode 100644 index 0000000..d667ca5 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/MainPage.kt @@ -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) + } +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/SettingsTab.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/SettingsTab.kt new file mode 100644 index 0000000..7570648 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/SettingsTab.kt @@ -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, + 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()) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/SplashScreen.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/SplashScreen.kt new file mode 100644 index 0000000..c67b52c --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/SplashScreen.kt @@ -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 } + } + } +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AboutPage.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AboutPage.kt new file mode 100644 index 0000000..c4a35d5 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AboutPage.kt @@ -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(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) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt new file mode 100644 index 0000000..c920b92 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt @@ -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) +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt new file mode 100644 index 0000000..8abde41 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt @@ -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) +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/theme/Theme.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/theme/Theme.kt new file mode 100644 index 0000000..ce8435b --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/theme/Theme.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/splash.png b/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000..169884e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/app/src/main/res/drawable-mdpi/splash.png b/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 0000000..503701b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/app/src/main/res/drawable-xhdpi/splash.png b/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000..b5090f8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/app/src/main/res/drawable-xxhdpi/splash.png b/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000..e1abdf3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/app/src/main/res/drawable/bg_h1.png b/app/src/main/res/drawable/bg_h1.png new file mode 100644 index 0000000..7f14d2b Binary files /dev/null and b/app/src/main/res/drawable/bg_h1.png differ diff --git a/app/src/main/res/drawable/bg_pattern.png b/app/src/main/res/drawable/bg_pattern.png new file mode 100644 index 0000000..5fcc333 Binary files /dev/null and b/app/src/main/res/drawable/bg_pattern.png differ diff --git a/app/src/main/res/drawable/splash.png b/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000..4cb5d6a Binary files /dev/null and b/app/src/main/res/drawable/splash.png differ diff --git a/app/src/main/res/drawable/splash_style.xml b/app/src/main/res/drawable/splash_style.xml new file mode 100644 index 0000000..ccd389f --- /dev/null +++ b/app/src/main/res/drawable/splash_style.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index c94de9c..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,1178 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cba08c2..12cd362 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,5 +43,36 @@ axisX.y axisY.y axisZ.y + Basic Ssettings + Graphic Settings + Camera Settings + Test Mode - LIVE + Debug Settings + Breast Parameters + About + Home + Advanced + WARNING + This plugin is for learning and communication only. + Using external plugin against the relevant TOS so proceed at your own risk. + About This Plugin + This plugin is completely free. If you paid for this plugin, please report the seller. + Plugin QQ group: 975854705 + Project Contribution + Plugin Code + Contributors + Translation Repository + Resource Settings + Check Built-in Assets Update + Delete Plugin Resource + Use Remote ZIP Resource + Resource URL + Download + Invalid file + This file is not a valid ZIP translation resource pack. + Cancel + Downloaded Version + Delete Cache File After Update + about_contributors_en.json \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index d756493..3d3f98f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,30 @@ - - \ No newline at end of file + + diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..4f7310f --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + diff --git a/build.gradle b/build.gradle index 3e14fb1..6e929d1 100644 --- a/build.gradle +++ b/build.gradle @@ -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 -} \ No newline at end of file + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.kotlinAndroid) apply false +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..10d5072 --- /dev/null +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c..e644113 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 245dac2..a441313 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index 4f906e0..b740cf1 --- a/gradlew +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd3..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +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 diff --git a/settings.gradle b/settings.gradle index 8e623ef..4e3c53a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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 {