From d36666f4369a7793072c945811d952cc5362a454 Mon Sep 17 00:00:00 2001 From: chinosk <74499927+chinosk6@users.noreply.github.com> Date: Mon, 24 Jun 2024 03:20:00 -0500 Subject: [PATCH] UI rewrite (#27) * rewrite UI * update submodule and ci * update submodule * AboutPage - use config --- .github/workflows/build.yml | 2 + app/build.gradle | 29 +- app/src/main/AndroidManifest.xml | 2 + .../main/assets/about_contributors_en.json | 29 ++ .../main/assets/about_contributors_zh_cn.json | 29 ++ app/src/main/assets/gakumas-local | 2 +- app/src/main/cpp/GakumasLocalify/Hook.cpp | 191 ++++----- .../gakumas/localify/ConfigUpdateListener.kt | 31 ++ .../chinosk/gakumas/localify/MainActivity.kt | 184 ++++++++- .../localify/models/AboutPageConfig.kt | 24 ++ .../gakumas/localify/models/GakumasConfig.kt | 2 + .../gakumas/localify/models/ViewModels.kt | 22 + .../localify/ui/components/GakuButton.kt | 73 ++++ .../localify/ui/components/GakuGroupBox.kt | 108 +++++ .../localify/ui/components/GakuRadio.kt | 77 ++++ .../localify/ui/components/GakuSwitch.kt | 51 +++ .../localify/ui/components/GakuTableRow.kt | 103 +++++ .../localify/ui/components/GakuTextInput.kt | 188 +++++++++ .../ui/components/base/AutoSizeText.kt | 63 +++ .../ui/components/base/CollapsibleBox.kt | 94 +++++ .../gakumas/localify/ui/pages/MainPage.kt | 113 +++++ .../gakumas/localify/ui/pages/SettingsTab.kt | 89 ++++ .../gakumas/localify/ui/pages/SplashScreen.kt | 27 ++ .../localify/ui/pages/subPages/AboutPage.kt | 214 ++++++++++ .../ui/pages/subPages/AdvancedSettingsPage.kt | 391 ++++++++++++++++++ .../localify/ui/pages/subPages/HomePage.kt | 270 ++++++++++++ .../gakumas/localify/ui/theme/Theme.kt | 4 +- app/src/main/res/drawable/bg_h1.png | Bin 0 -> 18611 bytes app/src/main/res/drawable/bg_pattern.png | Bin 0 -> 29351 bytes app/src/main/res/values-zh-rCN/strings.xml | 23 +- app/src/main/res/values/strings.xml | 20 + 31 files changed, 2343 insertions(+), 112 deletions(-) create mode 100644 app/src/main/assets/about_contributors_en.json create mode 100644 app/src/main/assets/about_contributors_zh_cn.json create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/models/AboutPageConfig.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuButton.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuGroupBox.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuRadio.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuSwitch.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuTableRow.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuTextInput.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/AutoSizeText.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/CollapsibleBox.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/MainPage.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/SettingsTab.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/SplashScreen.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AboutPage.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt create mode 100644 app/src/main/res/drawable/bg_h1.png create mode 100644 app/src/main/res/drawable/bg_pattern.png diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 849f32e..dad21a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,11 +37,13 @@ jobs: git clone https://${{ secrets.ACCESS_TOKEN_GITHUB }}@github.com/imas-tools/gakumas-raw-txts.git app/src/main/assets/gakumas-local/gakumas-raw-txts mv app/src/main/assets/gakumas-local/gakumas-raw-txts/Resource app/src/main/assets/gakumas-local/raw rm -rf app/src/main/assets/gakumas-local/gakumas-raw-txts + continue-on-error: true - name: Build Assets run: | mv app/src/main/assets/gakumas-local/GakumasPreTranslation/.env.sample app/src/main/assets/gakumas-local/GakumasPreTranslation/.env cd app/src/main/assets/gakumas-local && make build-resource + continue-on-error: true - name: Write branch and commit info run: | diff --git a/app/build.gradle b/app/build.gradle index 97ffc5e..0838e55 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -84,16 +84,37 @@ android { dependencies { - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' - implementation platform('androidx.compose:compose-bom:2023.08.00') + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.2' implementation 'androidx.compose.material3:material3' implementation 'com.google.android.material:material:1.12.0' + implementation "androidx.activity:activity-compose:1.9.0" + implementation "androidx.appcompat:appcompat:1.7.0" + implementation 'androidx.navigation:navigation-compose:2.7.7' + + def composeBom = platform('androidx.compose:compose-bom:2024.06.00') + implementation(composeBom) + androidTestImplementation(composeBom) + implementation "androidx.compose.runtime:runtime" + implementation "androidx.compose.material:material" + implementation "androidx.compose.foundation:foundation" + implementation "androidx.compose.foundation:foundation-layout" + implementation "androidx.compose.animation:animation" + implementation "androidx.compose.ui:ui-tooling-preview" + androidTestImplementation "androidx.compose.ui:ui-test-junit4" + debugImplementation "androidx.compose.ui:ui-tooling" + debugImplementation "androidx.compose.ui:ui-test-manifest" + implementation "com.google.accompanist:accompanist-pager:0.30.0" + implementation "com.google.accompanist:accompanist-pager-indicators:0.30.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2" + + implementation "io.coil-kt:coil-compose:2.6.0" + implementation "io.coil-kt:coil-svg:2.6.0" 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.0" + implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.22" implementation 'com.google.code.gson:gson:2.11.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2f6ff8..4481f0b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + 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); } } @@ -322,7 +322,7 @@ namespace GakumasLocal::HookMain { } std::unordered_set updatedFontPtrs{}; - void UpdateFont(void* TMP_Text_this) { + void UpdateFont(void* TMP_Textself) { if (!Config::replaceFont) return; static auto get_font = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro", "TMP_Text", "get_font"); @@ -334,7 +334,7 @@ namespace GakumasLocal::HookMain { static auto UpdateFontAssetData = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "UpdateFontAssetData"); - auto fontAsset = get_font->Invoke(TMP_Text_this); + auto fontAsset = get_font->Invoke(TMP_Textself); auto newFont = GetReplaceFont(); if (fontAsset && newFont) { set_sourceFontFile->Invoke(fontAsset, newFont); @@ -344,12 +344,12 @@ namespace GakumasLocal::HookMain { } if (updatedFontPtrs.size() > 200) updatedFontPtrs.clear(); } - 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", @@ -359,48 +359,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)) { @@ -410,7 +410,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()); @@ -423,24 +423,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()); @@ -448,16 +448,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,", @@ -468,18 +468,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); @@ -501,7 +501,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) { @@ -510,23 +510,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", @@ -545,8 +545,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; @@ -557,7 +557,7 @@ namespace GakumasLocal::HookMain { qualitySettingsLevel, maxBufferPixel, renderScale, volumeIndex, Config::lodQualityLevel, Config::reflectionQualityLevel); } - 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)) { @@ -566,10 +566,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, @@ -584,8 +584,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); } @@ -607,6 +607,7 @@ namespace GakumasLocal::HookMain { } std::vector namesVec{}; + namesVec.reserve(names.size()); for (auto i :names) { namesVec.push_back(i->ToString()); } @@ -645,7 +646,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"); @@ -654,10 +655,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); @@ -668,13 +669,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) { @@ -684,7 +685,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++) { @@ -706,12 +707,12 @@ namespace GakumasLocal::HookMain { } } else { - cacheTrans = NULL; + cacheTrans = nullptr; } } - CampusActorController_LateUpdate_Orig(_this, mtd); + CampusActorController_LateUpdate_Orig(self, mtd); } void UpdateSwingBreastBonesData(void* initializeData) { @@ -814,9 +815,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() { @@ -862,14 +863,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/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt index 457fdf4..9606238 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,7 +1,16 @@ package io.github.chinosk.gakumas.localify import android.view.KeyEvent +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding +import io.github.chinosk.gakumas.localify.models.GakumasConfig +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow interface ConfigListener { @@ -48,9 +57,26 @@ interface ConfigListener { fun onBClickPresetChanged(index: Int) } +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 + var factory: UserConfigViewModelFactory + var viewModel: UserConfigViewModel fun pushKeyEvent(event: KeyEvent): Boolean fun getConfigContent(): String @@ -247,6 +273,11 @@ interface ConfigUpdateListener: ConfigListener { R.id.radioButtonGameDefault -> binding.config!!.gameOrientation = 0 R.id.radioButtonGamePortrait -> binding.config!!.gameOrientation = 1 R.id.radioButtonGameLandscape -> binding.config!!.gameOrientation = 2 + else -> { + if (listOf(0, 1, 2).contains(checkedId)) { + binding.config!!.gameOrientation = checkedId + } + } } saveConfig() } 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 094d222..cedd43a 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,32 +1,195 @@ package io.github.chinosk.gakumas.localify - +import SplashScreen 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.compose.runtime.Composable 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 io.github.chinosk.gakumas.localify.hookUtils.FilesChecker import io.github.chinosk.gakumas.localify.hookUtils.MainKeyEventDispatcher import io.github.chinosk.gakumas.localify.models.GakumasConfig +import io.github.chinosk.gakumas.localify.ui.theme.GakumasLocalifyTheme import java.io.File +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import kotlinx.coroutines.flow.MutableStateFlow +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import io.github.chinosk.gakumas.localify.ui.pages.MainUI +import kotlinx.coroutines.flow.asStateFlow -class MainActivity : AppCompatActivity(), ConfigUpdateListener { +class MainActivity : ComponentActivity(), ConfigUpdateListener { + override lateinit var binding: ActivityMainBinding + + override lateinit var factory: UserConfigViewModelFactory + override lateinit var viewModel: UserConfigViewModel + + 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 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("检测到第一次启动,初始化配置文件...") + "{}" + } + } + + override fun saveConfig() { + try { + binding.config!!.pf = false + viewModel.configState.value = binding.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!!)) + } + + fun getVersion(): List { + var versionText = "" + var resVersionText = "unknown" + + try { + val stream = assets.open("${FilesChecker.localizationFilesDir}/version.txt") + resVersionText = FilesChecker.convertToString(stream) + + val packInfo = packageManager.getPackageInfo(packageName, 0) + val version = packInfo.versionName + val versionCode = packInfo.longVersionCode + versionText = "$version ($versionCode)" + } + catch (_: Exception) {} + + return listOf(versionText, resVersionText) + } + + fun openUrl(url: String) { + val webpage = Uri.parse(url) + val intent = Intent(Intent.ACTION_VIEW, webpage) + startActivity(intent) + } + + private fun loadConfig() { + val configStr = getConfigContent() + binding.config = try { + Gson().fromJson(configStr, GakumasConfig::class.java) + } + catch (e: JsonSyntaxException) { + showToast("配置文件异常,已重置: $e") + Gson().fromJson("{}", GakumasConfig::class.java) + } + saveConfig() + } + + override fun checkConfigAndUpdateView() { + binding.config = binding.config + binding.notifyChange() + } + + 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("TestMode: ${!origDbg}") + } + } + return if (event.action == 1145) true else super.dispatchKeyEvent(event) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + loadConfig() + binding.listener = this + + val requestData = intent.getStringExtra("gkmsData") + if (requestData != null) { + if (requestData == "requestConfig") { + onClickStartGame() + finish() + } + } + + factory = UserConfigViewModelFactory(binding.config!!) + viewModel = ViewModelProvider(this, factory)[UserConfigViewModel::class.java] + + setContent { + GakumasLocalifyTheme(dynamicColor = false) { + MainUI(context = this) + /* + val navController = rememberNavController() + NavHost(navController, startDestination = "splash") { + composable("splash") { + SplashScreen(navController) + } + composable("main") { + MainUI(context = this@MainActivity) + } + }*/ + } + + } + } +} + + +@Composable +fun getConfigState(context: MainActivity?, previewData: GakumasConfig?): State { + return if (context != null) { + context.viewModel.config.collectAsState() + } + else { + val configMSF = MutableStateFlow(previewData!!) + configMSF.asStateFlow().collectAsState() + } +} + +/* +class OldActivity : AppCompatActivity(), ConfigUpdateListener { override lateinit var binding: ActivityMainBinding private val TAG = "GakumasLocalify" + override lateinit var factory: UserConfigViewModelFactory // No usage + override lateinit var viewModel: UserConfigViewModel // No usage + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -163,4 +326,5 @@ class MainActivity : AppCompatActivity(), ConfigUpdateListener { } return if (event.action == 1145) true else super.dispatchKeyEvent(event) } -} \ No newline at end of file +} + */ \ 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..b26af40 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/AboutPageConfig.kt @@ -0,0 +1,24 @@ +package io.github.chinosk.gakumas.localify.models + +data class AboutPageConfig ( + var plugin_repo: String = "https://github.com/chinosk6/gakuen-imas-localify", + var main_contributors: List = listOf(), + var contrib_img: ContribImg = ContribImg( + "https://contrib.rocks/image?repo=chinosk6/gakuen-imas-localify", + "https://contrib.rocks/image?repo=chinosk6/GakumasTranslationData") +) + +data class MainContributors ( + var name: String, + var links: List +) + +data class ContribImg ( + var plugin: String, + var translation: String +) + +data class Links ( + var name: String, + var link: String +) \ No newline at end of file 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 af45b52..1a72e7c 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 @@ -42,4 +42,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/ViewModels.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt new file mode 100644 index 0000000..a6c190f --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt @@ -0,0 +1,22 @@ +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 + + +class CollapsibleBoxViewModel(initiallyExpanded: Boolean = false) : ViewModel() { + var expanded by mutableStateOf(initiallyExpanded) +} + +class CollapsibleBoxViewModelFactory(private val initiallyExpanded: Boolean) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(CollapsibleBoxViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return CollapsibleBoxViewModel(initiallyExpanded) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file 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..5f9b446 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuButton.kt @@ -0,0 +1,73 @@ +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 // 描边的颜色 +) { + var buttonSize by remember { mutableStateOf(IntSize.Zero) } + + val gradient = remember(buttonSize) { + Brush.linearGradient( + colors = listOf(Color(0xFFFF5F19), Color(0xFFFFA028)), + start = Offset(0f, 0f), + end = Offset(buttonSize.width.toFloat(), buttonSize.height.toFloat()) // 动态终点 + ) + } + + Button( + onClick = onClick, + 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) + } +} + + +@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO) +@Composable +fun GakuButtonPreview() { + GakuButton(modifier = Modifier.width(80.dp).height(40.dp), text = "Button", onClick = {}) +} 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..a0726d1 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuGroupBox.kt @@ -0,0 +1,108 @@ +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/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..aa1b0ef --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuSwitch.kt @@ -0,0 +1,51 @@ +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.Text +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(0xFFF89400), + + 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..d263bf1 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuTableRow.kt @@ -0,0 +1,103 @@ +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.Image +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..ffcd4a7 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/CollapsibleBox.kt @@ -0,0 +1,94 @@ +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.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, + 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() + // .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..b6b8f80 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/SettingsTab.kt @@ -0,0 +1,89 @@ +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.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..7c0d346 --- /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 com.google.gson.Gson +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.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)) + } + Gson().fromJson(dataJsonString, AboutPageConfig::class.java) + ?: 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..1037ddd --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt @@ -0,0 +1,391 @@ +package io.github.chinosk.gakumas.localify.ui.pages.subPages + +import 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.CollapsibleBoxViewModel +import io.github.chinosk.gakumas.localify.models.CollapsibleBoxViewModelFactory +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: CollapsibleBoxViewModel = + viewModel(factory = CollapsibleBoxViewModelFactory(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 { + 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..f818ea6 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt @@ -0,0 +1,270 @@ +package io.github.chinosk.gakumas.localify.ui.pages.subPages + +import 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 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.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.GakuRadio +import io.github.chinosk.gakumas.localify.ui.components.GakuSwitch +import io.github.chinosk.gakumas.localify.ui.components.GakuTextInput + + +@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 scrollState = rememberScrollState() + val keyboardOptionsNumber = remember { + KeyboardOptions(keyboardType = KeyboardType.Number) + } + val keyBoardOptionsDecimal = remember { + KeyboardOptions(keyboardType = KeyboardType.Decimal) + } + + + 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 = 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, widthDp = 880) +@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 index afe5886..ce8435b 100644 --- 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 @@ -10,6 +10,7 @@ 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 @@ -22,7 +23,7 @@ private val DarkColorScheme = darkColorScheme( ) private val LightColorScheme = lightColorScheme( - primary = Purple40, + primary = Color(0xFFF89400), secondary = PurpleGrey40, tertiary = Pink40 @@ -53,6 +54,7 @@ fun GakumasLocalifyTheme( darkTheme -> DarkColorScheme else -> LightColorScheme } + val view = LocalView.current if (!view.isInEditMode) { SideEffect { 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 0000000000000000000000000000000000000000..7f14d2bb0fe7b49f97ec2b890e2f4cbdde787cc5 GIT binary patch literal 18611 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5fcc3338632209d7d427ed2ec09ca704e4fb3e70 GIT binary patch literal 29351 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 76a9cd1..2b4dfdf 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -20,7 +20,7 @@ - 游戏原版 + 原版 竖屏 横屏 方向锁定 @@ -44,4 +44,25 @@ axisX.y axisY.y axisZ.y + 基础设置 + 画面设置 + 摄像机设置 + 测试模式 - LIVE + 调试设置 + 胸部参数 + 关于 + 主页 + 高级设置 + 使用前警告 + 本插件仅供学习和交流使用。 + 使用外部插件属于违反游戏条款的行为。若使用插件后账号被封禁,造成的后果由用户自行承担。 + 关于本插件 + 本插件完全免费。若您付费购买了本插件,请举报店家。 + 插件交流群: 975854705 + 项目贡献 + 插件本体 + 贡献者列表 + 译文仓库 + + about_contributors_zh_cn.json \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0de9161..be1df2f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,5 +44,25 @@ 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 + about_contributors_en.json \ No newline at end of file