1
0

Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Natsu 2024-07-01 11:42:49 +07:00
commit e663f5ca80
Signed by: NatsumeLS
GPG Key ID: 6DB67FB460CF46C6
66 changed files with 4349 additions and 1692 deletions

View File

@ -60,6 +60,7 @@ jobs:
keyStorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} keyStorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
buildToolsVersion: 33.0.0 buildToolsVersion: 33.0.0
continue-on-error: true
- name: Rename Signed APK - name: Rename Signed APK
run: | run: |

4
.gitignore vendored
View File

@ -6,3 +6,7 @@
/app/debug /app/debug
/app/release /app/release
/local.properties /local.properties
/.vs
/.kotlin
/app/debug
/app/release

View File

@ -1,8 +1,9 @@
plugins { plugins {
id 'com.android.application' alias(libs.plugins.androidApplication)
id 'org.jetbrains.kotlin.android' alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.serialization)
} }
android.buildFeatures.buildConfig true
android { android {
namespace 'io.github.chinosk.gakumas.localify' namespace 'io.github.chinosk.gakumas.localify'
@ -14,7 +15,7 @@ android {
minSdk 29 minSdk 29
targetSdk 34 targetSdk 34
versionCode 2 versionCode 2
versionName "Dev" versionName "v1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -35,38 +36,38 @@ android {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
buildConfigField "boolean", "ENABLE_LOG", "true" buildConfigField "boolean", "ENABLE_LOG", "true"
signingConfig signingConfigs.debug
} }
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '11'
} }
buildFeatures { buildFeatures {
buildConfig true
compose true compose true
prefab true prefab true
} }
composeOptions {
kotlinCompilerExtensionVersion '1.5.1'
}
externalNativeBuild { externalNativeBuild {
cmake { cmake {
path file('src/main/cpp/CMakeLists.txt') path file('src/main/cpp/CMakeLists.txt')
version '3.22.1' version '3.22.1'
} }
} }
packagingOptions {
resources { packaging {
excludes.add('/META-INF/{AL2.0,LGPL2.1}')
}
jniLibs { jniLibs {
pickFirsts += ['**/libxdl.so', '**/libshadowhook.so'] pickFirsts += "**/libxdl.so"
pickFirsts += "**/libshadowhook.so"
}
resources {
excludes += "**/META-INF/{AL2.0,LGPL2.1}"
excludes += "kotlin/**"
excludes += "**.bin"
} }
}
dataBinding {
enable true
} }
applicationVariants.configureEach { variant -> applicationVariants.configureEach { variant ->
@ -80,17 +81,40 @@ android {
} }
dependencies { dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.material3)
implementation(libs.material)
implementation 'androidx.core:core-ktx:1.13.1' implementation(libs.androidx.activity.compose)
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.2' implementation(libs.androidx.appcompat)
implementation platform('androidx.compose:compose-bom:2024.06.00') implementation(libs.androidx.navigation.compose)
implementation 'androidx.compose.material3:material3'
implementation 'com.google.android.material:material:1.12.0'
def composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
implementation(libs.androidx.runtime)
implementation(libs.androidx.material)
implementation(libs.androidx.foundation)
implementation(libs.androidx.foundation.layout)
implementation(libs.androidx.animation)
implementation(libs.androidx.ui.tooling.preview)
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.accompanist.pager)
implementation(libs.accompanist.pager.indicators)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation 'io.github.hexhacking:xdl:2.1.1' implementation(libs.coil.compose)
implementation 'com.bytedance.android:shadowhook:1.0.9' implementation(libs.coil.svg)
compileOnly 'de.robv.android.xposed:api:82'
implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.20" implementation(platform(libs.okhttp.bom))
implementation 'com.google.code.gson:gson:2.11.0' implementation(libs.okhttp)
implementation(libs.logging.interceptor)
implementation(libs.xdl)
implementation(libs.shadowhook)
compileOnly(libs.xposed.api)
implementation(libs.kotlinx.serialization.json)
} }

View File

@ -18,4 +18,9 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-keep class io.github.chinosk.gakumas.localify.GakumasHookMain {
<init>();
native <methods>;
}

View File

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@ -10,6 +12,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.GakumasLocalify" android:theme="@style/Theme.GakumasLocalify"
android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<meta-data <meta-data
@ -32,7 +35,6 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GakumasLocalify"> android:theme="@style/Theme.GakumasLocalify">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -40,6 +42,23 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".TranslucentActivity"
android:exported="true"
android:theme="@style/Theme.GakumasLocalify.NoDisplay">
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,29 @@
{
"plugin_repo": "https://github.com/chinosk6/gakuen-imas-localify",
"main_contributors": [
{
"name": "chinosk (Plugin code)",
"links": [
{"name": "Github", "link": "https://github.com/chinosk6"},
{"name": "Bilibili", "link": "https://space.bilibili.com/287061163"}
]
},
{
"name": "DarwinTree (Translation Workflow)",
"links": [
{"name": "Github", "link": "https://github.com/darwintree"},
{"name": "Bilibili", "link": "https://space.bilibili.com/6069705"}
]
},
{
"name": "And all other translators",
"links": [
{"name": "Github", "link": "https://github.com/chinosk6/GakumasTranslationData/graphs/contributors"}
]
}
],
"contrib_img": {
"plugin": "https://contrib.rocks/image?repo=chinosk6/gakuen-imas-localify",
"translation": "https://contrib.rocks/image?repo=chinosk6/GakumasTranslationData"
}
}

View File

@ -0,0 +1,29 @@
{
"plugin_repo": "https://github.com/chinosk6/gakuen-imas-localify",
"main_contributors": [
{
"name": "chinosk插件本体",
"links": [
{"name": "Github", "link": "https://github.com/chinosk6"},
{"name": "Bilibili", "link": "https://space.bilibili.com/287061163"}
]
},
{
"name": "DarwinTree译文工作流",
"links": [
{"name": "Github", "link": "https://github.com/darwintree"},
{"name": "Bilibili", "link": "https://space.bilibili.com/6069705"}
]
},
{
"name": "以及其他所有翻译贡献者",
"links": [
{"name": "Github", "link": "https://github.com/chinosk6/GakumasTranslationData/graphs/contributors"}
]
}
],
"contrib_img": {
"plugin": "https://contrib.rocks/image?repo=chinosk6/gakuen-imas-localify",
"translation": "https://contrib.rocks/image?repo=chinosk6/GakumasTranslationData"
}
}

View File

@ -24,3 +24,16 @@
#define WM_KEYDOWN 0 #define WM_KEYDOWN 0
#define WM_KEYUP 1 #define WM_KEYUP 1
#define BTN_A 96
#define BTN_B 97
#define BTN_X 99
#define BTN_Y 100
#define BTN_LB 102
#define BTN_RB 103
#define BTN_THUMBL 106
#define BTN_THUMBR 107
#define BTN_SELECT 109
#define BTN_START 108
#define BTN_SHARE 130
#define BTN_XBOX 110

View File

@ -87,7 +87,7 @@ namespace GakumasLocal::HookMain {
} }
bool IsNativeObjectAlive(void* obj) { bool IsNativeObjectAlive(void* obj) {
static UnityResolve::Method* IsNativeObjectAliveMtd = NULL; static UnityResolve::Method* IsNativeObjectAliveMtd = nullptr;
if (!IsNativeObjectAliveMtd) IsNativeObjectAliveMtd = Il2cppUtils::GetMethod("UnityEngine.CoreModule.dll", "UnityEngine", if (!IsNativeObjectAliveMtd) IsNativeObjectAliveMtd = Il2cppUtils::GetMethod("UnityEngine.CoreModule.dll", "UnityEngine",
"Object", "IsNativeObjectAlive"); "Object", "IsNativeObjectAlive");
return IsNativeObjectAliveMtd->Invoke<bool>(obj); return IsNativeObjectAliveMtd->Invoke<bool>(obj);
@ -109,18 +109,18 @@ namespace GakumasLocal::HookMain {
return GetResolution->Invoke<Il2cppUtils::Resolution_t>(); return GetResolution->Invoke<Il2cppUtils::Resolution_t>();
} }
DEFINE_HOOK(void, Unity_set_fieldOfView, (UnityResolve::UnityType::Camera* _this, float value)) { DEFINE_HOOK(void, Unity_set_fieldOfView, (UnityResolve::UnityType::Camera* self, float value)) {
if (Config::enableFreeCamera) { if (Config::enableFreeCamera) {
if (_this == mainCameraCache) { if (self == mainCameraCache) {
value = GKCamera::baseCamera.fov; 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 (Config::enableFreeCamera) {
if (_this == mainCameraCache) { if (self == mainCameraCache) {
static auto get_orthographic = reinterpret_cast<bool (*)(void*)>(Il2cppUtils::il2cpp_resolve_icall( static auto get_orthographic = reinterpret_cast<bool (*)(void*)>(Il2cppUtils::il2cpp_resolve_icall(
"UnityEngine.Camera::get_orthographic()" "UnityEngine.Camera::get_orthographic()"
)); ));
@ -133,30 +133,30 @@ namespace GakumasLocal::HookMain {
// set_orthographic(i, false); // set_orthographic(i, false);
Unity_set_fieldOfView_Orig(i, GKCamera::baseCamera.fov); 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 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::Quaternion cacheRotation{};
UnityResolve::UnityType::Vector3 cachePosition{}; UnityResolve::UnityType::Vector3 cachePosition{};
UnityResolve::UnityType::Vector3 cacheForward{}; UnityResolve::UnityType::Vector3 cacheForward{};
UnityResolve::UnityType::Vector3 cacheLookAt{}; 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) { if (Config::enableFreeCamera) {
static auto lookat_injected = reinterpret_cast<void (*)(void*_this, static auto lookat_injected = reinterpret_cast<void (*)(void*self,
UnityResolve::UnityType::Vector3* worldPosition, UnityResolve::UnityType::Vector3* worldUp)>( UnityResolve::UnityType::Vector3* worldPosition, UnityResolve::UnityType::Vector3* worldUp)>(
Il2cppUtils::il2cpp_resolve_icall( Il2cppUtils::il2cpp_resolve_icall(
"UnityEngine.Transform::Internal_LookAt_Injected(UnityEngine.Vector3&,UnityEngine.Vector3&)")); "UnityEngine.Transform::Internal_LookAt_Injected(UnityEngine.Vector3&,UnityEngine.Vector3&)"));
static auto worldUp = UnityResolve::UnityType::Vector3(0, 1, 0); static auto worldUp = UnityResolve::UnityType::Vector3(0, 1, 0);
if (cameraTransformCache == _this) { if (cameraTransformCache == self) {
const auto cameraMode = GKCamera::GetCameraMode(); const auto cameraMode = GKCamera::GetCameraMode();
if (cameraMode == GKCamera::CameraMode::FIRST_PERSON) { if (cameraMode == GKCamera::CameraMode::FIRST_PERSON) {
if (cacheTrans && IsNativeObjectAlive(cacheTrans)) { if (cacheTrans && IsNativeObjectAlive(cacheTrans)) {
@ -167,7 +167,7 @@ namespace GakumasLocal::HookMain {
static GakumasLocal::Misc::FixedSizeQueue<float> recordsY(60); static GakumasLocal::Misc::FixedSizeQueue<float> recordsY(60);
const auto newY = GKCamera::CheckNewY(cacheLookAt, true, recordsY); const auto newY = GKCamera::CheckNewY(cacheLookAt, true, recordsY);
UnityResolve::UnityType::Vector3 newCacheLookAt{cacheLookAt.x, newY, cacheLookAt.z}; UnityResolve::UnityType::Vector3 newCacheLookAt{cacheLookAt.x, newY, cacheLookAt.z};
lookat_injected(_this, &newCacheLookAt, &worldUp); lookat_injected(self, &newCacheLookAt, &worldUp);
return; return;
} }
} }
@ -175,25 +175,25 @@ namespace GakumasLocal::HookMain {
else if (cameraMode == GKCamera::CameraMode::FOLLOW) { else if (cameraMode == GKCamera::CameraMode::FOLLOW) {
auto newLookAtPos = GKCamera::CalcFollowModeLookAt(cachePosition, auto newLookAtPos = GKCamera::CalcFollowModeLookAt(cachePosition,
GKCamera::followPosOffset, true); GKCamera::followPosOffset, true);
lookat_injected(_this, &newLookAtPos, &worldUp); lookat_injected(self, &newLookAtPos, &worldUp);
return; return;
} }
else { else {
auto& origCameraLookat = GKCamera::baseCamera.lookAt; 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); // Log::DebugFmt("fov: %f, target: %f", Unity_get_fieldOfView_Orig(mainCameraCache), GKCamera::baseCamera.fov);
return; 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) { if (Config::enableFreeCamera) {
CheckAndUpdateMainCamera(); CheckAndUpdateMainCamera();
if (cameraTransformCache == _this) { if (cameraTransformCache == self) {
const auto cameraMode = GKCamera::GetCameraMode(); const auto cameraMode = GKCamera::GetCameraMode();
if (cameraMode == GKCamera::CameraMode::FIRST_PERSON) { if (cameraMode == GKCamera::CameraMode::FIRST_PERSON) {
if (cacheTrans && IsNativeObjectAlive(cacheTrans)) { 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) { switch (Config::gameOrientation) {
case 1: type = 0x2; break; // FixedPortrait case 1: type = 0x2; break; // FixedPortrait
case 2: type = 0x3; break; // FixedLandscape case 2: type = 0x3; break; // FixedLandscape
default: break; 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)) { DEFINE_HOOK(void, EndCameraRendering, (void* ctx, void* camera, void* method)) {
@ -248,16 +248,16 @@ namespace GakumasLocal::HookMain {
std::unordered_map<void*, std::string> loadHistory{}; std::unordered_map<void*, std::string> loadHistory{};
DEFINE_HOOK(void*, AssetBundle_LoadAssetAsync, (void* _this, Il2cppString* name, void* type)) { DEFINE_HOOK(void*, AssetBundle_LoadAssetAsync, (void* self, Il2cppString* name, void* type)) {
// Log::InfoFmt("AssetBundle_LoadAssetAsync: %s, type: %s", name->ToString().c_str()); // 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()); loadHistory.emplace(ret, name->ToString());
return ret; return ret;
} }
DEFINE_HOOK(void*, AssetBundleRequest_GetResult, (void* _this)) { DEFINE_HOOK(void*, AssetBundleRequest_GetResult, (void* self)) {
auto result = AssetBundleRequest_GetResult_Orig(_this); auto result = AssetBundleRequest_GetResult_Orig(self);
if (const auto iter = loadHistory.find(_this); iter != loadHistory.end()) { if (const auto iter = loadHistory.find(self); iter != loadHistory.end()) {
const auto name = iter->second; const auto name = iter->second;
loadHistory.erase(iter); loadHistory.erase(iter);
@ -275,25 +275,25 @@ namespace GakumasLocal::HookMain {
return ret; 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()); // Log::InfoFmt("SetUpI18n lang: %s, key: %d text: %s", lang->ToString().c_str(), keyComparison, localizationText->ToString().c_str());
// TODO 此处为 dump 原文 csv // 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()); // Log::InfoFmt("I18nHelper_SetValue: %s - %s", key->ToString().c_str(), value->ToString().c_str());
std::string local; std::string local;
if (Local::GetI18n(key->ToString(), &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; return;
} }
Local::DumpI18nItem(key->ToString(), value->ToString()); Local::DumpI18nItem(key->ToString(), value->ToString());
if (Config::textTest) { if (Config::textTest) {
I18nHelper_SetValue_Orig(_this, key, Il2cppString::New("[I18]" + value->ToString())); I18nHelper_SetValue_Orig(self, key, Il2cppString::New("[I18]" + value->ToString()));
} }
else { else {
I18nHelper_SetValue_Orig(_this, key, value); I18nHelper_SetValue_Orig(self, key, value);
} }
} }
@ -315,12 +315,12 @@ namespace GakumasLocal::HookMain {
updatedFontPtrs.emplace(fontAsset); updatedFontPtrs.emplace(fontAsset);
UpdateFontAssetData->Invoke<void>(fontAsset); UpdateFontAssetData->Invoke<void>(fontAsset);
} }
set_font->Invoke<void>(TMP_Text_this, fontAsset); set_font->Invoke<void>(TMP_Textself, fontAsset);
} }
DEFINE_HOOK(void, TMP_Text_PopulateTextBackingArray, (void* _this, UnityResolve::UnityType::String* text, int start, int length)) { DEFINE_HOOK(void, TMP_Text_PopulateTextBackingArray, (void* self, UnityResolve::UnityType::String* text, int start, int length)) {
if (!text) { 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", static auto Substring = Il2cppUtils::GetMethod("mscorlib.dll", "System", "String", "Substring",
@ -330,48 +330,48 @@ namespace GakumasLocal::HookMain {
std::string transText; std::string transText;
if (Local::GetGenericText(origText, &transText)) { if (Local::GetGenericText(origText, &transText)) {
const auto newText = UnityResolve::UnityType::String::New(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) { 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 { 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)) { DEFINE_HOOK(void, TextMeshProUGUI_Awake, (void* self, void* method)) {
// Log::InfoFmt("TextMeshProUGUI_Awake at %p, _this at %p", TextMeshProUGUI_Awake_Orig, _this); // Log::InfoFmt("TextMeshProUGUI_Awake at %p, self at %p", TextMeshProUGUI_Awake_Orig, self);
const auto TMP_Text_klass = Il2cppUtils::GetClass("Unity.TextMeshPro.dll", const auto TMP_Text_klass = Il2cppUtils::GetClass("Unity.TextMeshPro.dll",
"TMPro", "TMP_Text"); "TMPro", "TMP_Text");
const auto get_Text_method = TMP_Text_klass->Get<UnityResolve::Method>("get_text"); const auto get_Text_method = TMP_Text_klass->Get<UnityResolve::Method>("get_text");
const auto set_Text_method = TMP_Text_klass->Get<UnityResolve::Method>("set_text"); const auto set_Text_method = TMP_Text_klass->Get<UnityResolve::Method>("set_text");
const auto currText = get_Text_method->Invoke<UnityResolve::UnityType::String*>(_this); const auto currText = get_Text_method->Invoke<UnityResolve::UnityType::String*>(self);
if (currText) { if (currText) {
//Log::InfoFmt("TextMeshProUGUI_Awake: %s", currText->ToString().c_str()); //Log::InfoFmt("TextMeshProUGUI_Awake: %s", currText->ToString().c_str());
std::string transText; std::string transText;
if (Local::GetGenericText(currText->ToString(), &transText)) { if (Local::GetGenericText(currText->ToString(), &transText)) {
if (Config::textTest) { if (Config::textTest) {
set_Text_method->Invoke<void>(_this, UnityResolve::UnityType::String::New("[TA]" + transText)); set_Text_method->Invoke<void>(self, UnityResolve::UnityType::String::New("[TA]" + transText));
} }
else { else {
set_Text_method->Invoke<void>(_this, UnityResolve::UnityType::String::New(transText)); set_Text_method->Invoke<void>(self, UnityResolve::UnityType::String::New(transText));
} }
} }
} }
// set_font->Invoke<void>(_this, font); // set_font->Invoke<void>(self, font);
UpdateFont(_this); UpdateFont(self);
TextMeshProUGUI_Awake_Orig(_this, method); TextMeshProUGUI_Awake_Orig(self, method);
} }
// TODO 文本未hook完整 // 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()); 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)) { DEFINE_HOOK(Il2cppString*, OctoCaching_GetResourceFileName, (void* data, void* method)) {
@ -381,7 +381,7 @@ namespace GakumasLocal::HookMain {
} }
DEFINE_HOOK(void, OctoResourceLoader_LoadFromCacheOrDownload, 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()); Log::DebugFmt("OctoResourceLoader_LoadFromCacheOrDownload: %s\n", resourceName->ToString().c_str());
@ -394,24 +394,24 @@ namespace GakumasLocal::HookMain {
const auto onComplete_invoke = reinterpret_cast<void (*)(void*, Il2cppString*, void*)>( const auto onComplete_invoke = reinterpret_cast<void (*)(void*, Il2cppString*, void*)>(
onComplete_invoke_mtd->methodPointer onComplete_invoke_mtd->methodPointer
); );
onComplete_invoke(onComplete, UnityResolve::UnityType::String::New(replaceStr), NULL); onComplete_invoke(onComplete, UnityResolve::UnityType::String::New(replaceStr), nullptr);
return; 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); 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 // 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)) { 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()); // Log::DebugFmt("UI_I18n_GetOrDefault: key: %s, default: %s, result: %s", key->ToString().c_str(), defaultKey->ToString().c_str(), ret->ToString().c_str());
@ -419,16 +419,16 @@ namespace GakumasLocal::HookMain {
// return UnityResolve::UnityType::String::New("[I18]" + ret->ToString()); // 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); // Log::DebugFmt("PictureBookLiveThumbnailView_SetData: isUnlocked: %d, isNew: %d", isUnlocked, isNew);
if (Config::unlockAllLive) { if (Config::dbgMode && Config::unlockAllLive) {
isUnlocked = true; isUnlocked = true;
} }
PictureBookLiveThumbnailView_SetData_Orig(_this, liveData, isUnlocked, isNew); PictureBookLiveThumbnailView_SetData_Orig(self, liveData, isUnlocked, isNew, ct, mtd);
} }
bool needRestoreHides = false; 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)) { Il2cppString* characterId, Il2cppString* costumeId, Il2cppString* costumeHeadId)) {
needRestoreHides = false; needRestoreHides = false;
Log::InfoFmt("MoveLiveScene: characterId: %s, costumeId: %s, costumeHeadId: %s,", Log::InfoFmt("MoveLiveScene: characterId: %s, costumeId: %s, costumeHeadId: %s,",
@ -439,18 +439,18 @@ namespace GakumasLocal::HookMain {
characterId: shro, costumeId: shro-cstm-0006, costumeHeadId: costume_head_shro-cstm-0006, characterId: shro, costumeId: shro-cstm-0006, costumeHeadId: costume_head_shro-cstm-0006,
*/ */
if (Config::enableLiveCustomeDress) { if (Config::dbgMode && Config::enableLiveCustomeDress) {
// 修改 LiveFixedData_GetCharacter 可以更改 Loading 角色和演唱者名字,而不变更实际登台人 // 修改 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::liveCustomeCostumeId.empty() ? costumeId : Il2cppString::New(Config::liveCustomeCostumeId),
Config::liveCustomeHeadId.empty() ? costumeHeadId : Il2cppString::New(Config::liveCustomeHeadId)); 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; // 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 /* // 修改角色后Live 结束返回时, itemModel 为 null
Log::DebugFmt("OnSelectMusic itemModel at %p", itemModel); Log::DebugFmt("OnSelectMusic itemModel at %p", itemModel);
@ -472,7 +472,7 @@ namespace GakumasLocal::HookMain {
auto newItemModel = PictureBookLiveSelectMusicListItemModel_klass->New<void*>(); auto newItemModel = PictureBookLiveSelectMusicListItemModel_klass->New<void*>();
PictureBookLiveSelectMusicListItemModel_ctor->Invoke<void>(newItemModel, music, false); PictureBookLiveSelectMusicListItemModel_ctor->Invoke<void>(newItemModel, music, false);
return PictureBookLiveSelectScreenPresenter_OnSelectMusic_Orig(_this, newItemModel, isFirst, mtd); return PictureBookLiveSelectScreenPresenter_OnSelectMusic_Orig(self, newItemModel, isFirst, mtd);
} }
if (itemModel) { if (itemModel) {
@ -481,23 +481,23 @@ namespace GakumasLocal::HookMain {
lastMusicId = musicId->ToString(); lastMusicId = musicId->ToString();
}*/ }*/
if (!itemModel) return; 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; 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); // Log::InfoFmt("CampusQualityManager_set_TargetFrameRate: %f", value);
const auto configFps = Config::targetFrameRate; 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) { if (Config::targetFrameRate != 0) {
CampusQualityManager_set_TargetFrameRate_Orig(_this, Config::targetFrameRate); CampusQualityManager_set_TargetFrameRate_Orig(self, Config::targetFrameRate);
} }
if (Config::useCustomeGraphicSettings) { if (Config::useCustomeGraphicSettings) {
static auto SetReflectionQuality = Il2cppUtils::GetMethod("campus-submodule.Runtime.dll", "Campus.Common", static auto SetReflectionQuality = Il2cppUtils::GetMethod("campus-submodule.Runtime.dll", "Campus.Common",
@ -516,8 +516,8 @@ namespace GakumasLocal::HookMain {
if (Config::lodQualityLevel >= values.size()) Config::lodQualityLevel = values.size() - 1; if (Config::lodQualityLevel >= values.size()) Config::lodQualityLevel = values.size() - 1;
if (Config::reflectionQualityLevel >= values.size()) Config::reflectionQualityLevel = values.size() - 1; if (Config::reflectionQualityLevel >= values.size()) Config::reflectionQualityLevel = values.size() - 1;
SetLODQuality->Invoke<void>(_this, values[Config::lodQualityLevel]); SetLODQuality->Invoke<void>(self, values[Config::lodQualityLevel]);
SetReflectionQuality->Invoke<void>(_this, values[Config::reflectionQualityLevel]); SetReflectionQuality->Invoke<void>(self, values[Config::reflectionQualityLevel]);
qualitySettingsLevel = Config::qualitySettingsLevel; qualitySettingsLevel = Config::qualitySettingsLevel;
maxBufferPixel = Config::maxBufferPixel; maxBufferPixel = Config::maxBufferPixel;
@ -528,7 +528,7 @@ namespace GakumasLocal::HookMain {
qualitySettingsLevel , renderScale); qualitySettingsLevel , renderScale);
} }
CampusQualityManager_ApplySetting_Orig(_this, qualitySettingsLevel, maxBufferPixel, renderScale, volumeIndex); CampusQualityManager_ApplySetting_Orig(self, qualitySettingsLevel, maxBufferPixel, renderScale, volumeIndex);
} }
DEFINE_HOOK(void, UIManager_UpdateRenderTarget, (UnityResolve::UnityType::Vector2 ratio, void* mtd)) { DEFINE_HOOK(void, UIManager_UpdateRenderTarget, (UnityResolve::UnityType::Vector2 ratio, void* mtd)) {
@ -537,10 +537,10 @@ namespace GakumasLocal::HookMain {
return UIManager_UpdateRenderTarget_Orig(ratio, mtd); 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(); // const auto resolution = GetResolution();
// Log::DebugFmt("VLSRPCameraController_UpdateRenderTarget: %d, %d", width, height); // 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, DEFINE_HOOK(void*, VLUtility_GetLimitedResolution, (int32_t screenWidth, int32_t screenHeight,
@ -555,8 +555,8 @@ namespace GakumasLocal::HookMain {
} }
DEFINE_HOOK(void, CampusActorModelParts_OnRegisterBone, (void* _this, Il2cppString** name, UnityResolve::UnityType::Transform* bone)) { DEFINE_HOOK(void, CampusActorModelParts_OnRegisterBone, (void* self, Il2cppString** name, UnityResolve::UnityType::Transform* bone)) {
CampusActorModelParts_OnRegisterBone_Orig(_this, name, bone); CampusActorModelParts_OnRegisterBone_Orig(self, name, bone);
// Log::DebugFmt("CampusActorModelParts_OnRegisterBone: %s, %p", (*name)->ToString().c_str(), bone); // Log::DebugFmt("CampusActorModelParts_OnRegisterBone: %s, %p", (*name)->ToString().c_str(), bone);
} }
@ -578,6 +578,7 @@ namespace GakumasLocal::HookMain {
} }
std::vector<std::string> namesVec{}; std::vector<std::string> namesVec{};
namesVec.reserve(names.size());
for (auto i :names) { for (auto i :names) {
namesVec.push_back(i->ToString()); namesVec.push_back(i->ToString());
} }
@ -616,7 +617,7 @@ namespace GakumasLocal::HookMain {
} }
} }
DEFINE_HOOK(void, CampusActorController_LateUpdate, (void* _this, void* mtd)) { DEFINE_HOOK(void, CampusActorController_LateUpdate, (void* self, void* mtd)) {
static auto CampusActorController_klass = Il2cppUtils::GetClass("campus-submodule.Runtime.dll", static auto CampusActorController_klass = Il2cppUtils::GetClass("campus-submodule.Runtime.dll",
"Campus.Common", "CampusActorController"); "Campus.Common", "CampusActorController");
static auto rootBody_field = CampusActorController_klass->Get<UnityResolve::Field>("_rootBody"); static auto rootBody_field = CampusActorController_klass->Get<UnityResolve::Field>("_rootBody");
@ -625,10 +626,10 @@ namespace GakumasLocal::HookMain {
if (!Config::enableFreeCamera || (GKCamera::GetCameraMode() == GKCamera::CameraMode::FREE)) { if (!Config::enableFreeCamera || (GKCamera::GetCameraMode() == GKCamera::CameraMode::FREE)) {
if (needRestoreHides) { if (needRestoreHides) {
needRestoreHides = false; needRestoreHides = false;
HideHead(NULL, false); HideHead(nullptr, false);
HideHead(NULL, true); 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); static auto GetHumanBodyBoneTransform_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(parentKlass, "GetHumanBodyBoneTransform", 1);
@ -639,13 +640,13 @@ namespace GakumasLocal::HookMain {
static auto get_Index = get_index_mtd ? reinterpret_cast<int (*)(void*)>( static auto get_Index = get_index_mtd ? reinterpret_cast<int (*)(void*)>(
get_index_mtd->methodPointer) : [](void*){return 0;}; get_index_mtd->methodPointer) : [](void*){return 0;};
const auto currIndex = get_Index(_this); const auto currIndex = get_Index(self);
if (currIndex == GKCamera::followCharaIndex) { if (currIndex == GKCamera::followCharaIndex) {
static auto initPartsSuccess = InitBodyParts(); static auto initPartsSuccess = InitBodyParts();
static auto headBodyId = initPartsSuccess ? GKCamera::bodyPartsEnum.GetValueByName("Head") : 0xA; static auto headBodyId = initPartsSuccess ? GKCamera::bodyPartsEnum.GetValueByName("Head") : 0xA;
const auto isFirstPerson = GKCamera::GetCameraMode() == GKCamera::CameraMode::FIRST_PERSON; const auto isFirstPerson = GKCamera::GetCameraMode() == GKCamera::CameraMode::FIRST_PERSON;
auto targetTrans = GetHumanBodyBoneTransform(_this, auto targetTrans = GetHumanBodyBoneTransform(self,
isFirstPerson ? headBodyId : GKCamera::bodyPartsEnum.GetCurrent().second); isFirstPerson ? headBodyId : GKCamera::bodyPartsEnum.GetCurrent().second);
if (targetTrans) { if (targetTrans) {
@ -655,7 +656,7 @@ namespace GakumasLocal::HookMain {
cacheForward = cacheTrans->GetForward(); cacheForward = cacheTrans->GetForward();
cacheLookAt = cacheTrans->GetPosition() + cacheTrans->GetForward() * 3; cacheLookAt = cacheTrans->GetPosition() + cacheTrans->GetForward() * 3;
auto rootBody = Il2cppUtils::ClassGetFieldValue<UnityResolve::UnityType::Transform*>(_this, rootBody_field); auto rootBody = Il2cppUtils::ClassGetFieldValue<UnityResolve::UnityType::Transform*>(self, rootBody_field);
auto rootModel = rootBody->GetParent(); auto rootModel = rootBody->GetParent();
auto rootModelChildCount = rootModel->GetChildCount(); auto rootModelChildCount = rootModel->GetChildCount();
for (int i = 0; i < rootModelChildCount; i++) { for (int i = 0; i < rootModelChildCount; i++) {
@ -677,12 +678,12 @@ namespace GakumasLocal::HookMain {
} }
} }
else { else {
cacheTrans = NULL; cacheTrans = nullptr;
} }
} }
CampusActorController_LateUpdate_Orig(_this, mtd); CampusActorController_LateUpdate_Orig(self, mtd);
} }
void UpdateSwingBreastBonesData(void* initializeData) { void UpdateSwingBreastBonesData(void* initializeData) {
@ -785,9 +786,9 @@ namespace GakumasLocal::HookMain {
// Log::DebugFmt("\n"); // 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); UpdateSwingBreastBonesData(initializeData);
return CampusActorAnimation_Setup_Orig(_this, rootTrans, initializeData); return CampusActorAnimation_Setup_Orig(self, rootTrans, initializeData);
} }
void StartInjectFunctions() { void StartInjectFunctions() {
@ -833,14 +834,14 @@ namespace GakumasLocal::HookMain {
ADD_HOOK(PictureBookLiveThumbnailView_SetData, ADD_HOOK(PictureBookLiveThumbnailView_SetData,
Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame.PictureBook", Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame.PictureBook",
"PictureBookLiveThumbnailView", "SetData")); "PictureBookLiveThumbnailView", "SetDataAsync", {"*", "*", "*", "*"}));
ADD_HOOK(PictureBookLiveSelectScreenPresenter_MoveLiveScene, ADD_HOOK(PictureBookLiveSelectScreenPresenter_MoveLiveScene,
Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame", Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame",
"PictureBookLiveSelectScreenPresenter", "MoveLiveScene")); "PictureBookLiveSelectScreenPresenter", "MoveLiveScene"));
ADD_HOOK(PictureBookLiveSelectScreenPresenter_OnSelectMusic, ADD_HOOK(PictureBookLiveSelectScreenPresenter_OnSelectMusic,
Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame", Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame",
"PictureBookLiveSelectScreenPresenter", "OnSelectMusic")); "PictureBookLiveSelectScreenPresenter", "OnSelectMusicAsync"));
ADD_HOOK(VLDOF_IsActive, ADD_HOOK(VLDOF_IsActive,
Il2cppUtils::GetMethodPointer("Unity.RenderPipelines.Universal.Runtime.dll", "VL.Rendering", Il2cppUtils::GetMethodPointer("Unity.RenderPipelines.Universal.Runtime.dll", "VL.Rendering",

View File

@ -4,6 +4,7 @@
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <thread> #include <thread>
#include <queue>
extern JavaVM* g_javaVM; extern JavaVM* g_javaVM;
extern jclass g_gakumasHookMainClass; extern jclass g_gakumasHookMainClass;
@ -24,9 +25,13 @@ extern jmethodID showToastMethodId;
namespace GakumasLocal::Log { namespace GakumasLocal::Log {
namespace {
std::queue<std::string> showingToasts{};
}
std::string StringFormat(const char* fmt, ...) { std::string StringFormat(const char* fmt, ...) {
GetParamStringResult(result); GetParamStringResult(result);
return result.c_str(); return result;
} }
void Log(int prio, const char* msg) { void Log(int prio, const char* msg) {
@ -70,8 +75,8 @@ namespace GakumasLocal::Log {
__android_log_write(prio, "GakumasLog", result.c_str()); __android_log_write(prio, "GakumasLog", result.c_str());
} }
void ShowToast(const std::string& text) { void ShowToastJNI(const char* text) {
DebugFmt("Toast: %s", text.c_str()); DebugFmt("Toast: %s", text);
std::thread([text](){ std::thread([text](){
auto env = Misc::GetJNIEnv(); auto env = Misc::GetJNIEnv();
@ -89,15 +94,50 @@ namespace GakumasLocal::Log {
g_javaVM->DetachCurrentThread(); g_javaVM->DetachCurrentThread();
return; return;
} }
jstring param = env->NewStringUTF(text.c_str()); jstring param = env->NewStringUTF(text);
env->CallStaticVoidMethod(kotlinClass, methodId, param); env->CallStaticVoidMethod(kotlinClass, methodId, param);
g_javaVM->DetachCurrentThread(); g_javaVM->DetachCurrentThread();
}).detach(); }).detach();
} }
void ShowToast(const std::string& text) {
showingToasts.push(text);
}
void ShowToast(const char* text) {
DebugFmt("Toast: %s", text);
return ShowToast(std::string(text));
}
void ShowToastFmt(const char* fmt, ...) { void ShowToastFmt(const char* fmt, ...) {
GetParamStringResult(result); GetParamStringResult(result);
ShowToast(result); ShowToast(result);
} }
std::string GetQueuedToast() {
if (showingToasts.empty()) {
return "";
}
const auto ret = showingToasts.front();
showingToasts.pop();
return ret;
}
void ToastLoop(JNIEnv *env, jclass clazz) {
const auto toastString = GetQueuedToast();
if (toastString.empty()) return;
static auto _showToastMethodId = env->GetStaticMethodID(clazz, "showToast", "(Ljava/lang/String;)V");
if (env && clazz && _showToastMethodId) {
jstring param = env->NewStringUTF(toastString.c_str());
env->CallStaticVoidMethod(clazz, _showToastMethodId, param);
env->DeleteLocalRef(param);
}
else {
_showToastMethodId = env->GetStaticMethodID(clazz, "showToast", "(Ljava/lang/String;)V");
}
}
} }

View File

@ -2,6 +2,7 @@
#define GAKUMAS_LOCALIFY_LOG_H #define GAKUMAS_LOCALIFY_LOG_H
#include <string> #include <string>
#include <jni.h>
namespace GakumasLocal::Log { namespace GakumasLocal::Log {
std::string StringFormat(const char* fmt, ...); std::string StringFormat(const char* fmt, ...);
@ -16,6 +17,8 @@ namespace GakumasLocal::Log {
void ShowToast(const char* text); void ShowToast(const char* text);
void ShowToastFmt(const char* fmt, ...); void ShowToastFmt(const char* fmt, ...);
void ToastLoop(JNIEnv *env, jclass clazz);
} }
#endif //GAKUMAS_LOCALIFY_LOG_H #endif //GAKUMAS_LOCALIFY_LOG_H

View File

@ -62,14 +62,14 @@ namespace BaseCamera {
return lookAt; return lookAt;
} }
void Camera::set_lon_move(float vertanglePlus, LonMoveHState moveState) { // 前后移动 void Camera::set_lon_move(float vertanglePlus, LonMoveHState moveState, float multiplier) { // 前后移动
auto radian = (verticalAngle + vertanglePlus) * M_PI / 180; auto radian = (verticalAngle + vertanglePlus) * M_PI / 180;
auto radianH = (double)horizontalAngle * M_PI / 180; auto radianH = (double)horizontalAngle * M_PI / 180;
auto f_step = cos(radian) * moveStep * cos(radianH) / smoothLevel; // ↑↓ auto f_step = cos(radian) * moveStep * cos(radianH) / smoothLevel * multiplier; // ↑↓
auto l_step = sin(radian) * moveStep * cos(radianH) / smoothLevel; // ←→ auto l_step = sin(radian) * moveStep * cos(radianH) / smoothLevel * multiplier; // ←→
// auto h_step = tan(radianH) * sqrt(pow(f_step, 2) + pow(l_step, 2)); // auto h_step = tan(radianH) * sqrt(pow(f_step, 2) + pow(l_step, 2));
auto h_step = sin(radianH) * moveStep / smoothLevel; auto h_step = sin(radianH) * moveStep / smoothLevel * multiplier;
switch (moveState) switch (moveState)
{ {

View File

@ -30,7 +30,7 @@ namespace BaseCamera {
void setPos(float x, float y, float z); void setPos(float x, float y, float z);
void setLookAt(float x, float y, float z); void setLookAt(float x, float y, float z);
void set_lon_move(float vertanglePlus, LonMoveHState moveState = LonMoveHState::LonMoveLeftAndRight); void set_lon_move(float vertanglePlus, LonMoveHState moveState = LonMoveHState::LonMoveLeftAndRight, float multiplier = 1.0f);
void updateVertLook(); void updateVertLook();
void setHoriLook(float vertangle); void setHoriLook(float vertangle);

View File

@ -16,6 +16,9 @@ namespace GKCamera {
UnityResolve::UnityType::Vector2 followLookAtOffset{0, 0}; UnityResolve::UnityType::Vector2 followLookAtOffset{0, 0};
float offsetMoveStep = 0.008; float offsetMoveStep = 0.008;
int followCharaIndex = 0; int followCharaIndex = 0;
float l_sensitivity = 0.5f;
float r_sensitivity = 0.5f;
bool showToast = true;
GakumasLocal::Misc::CSEnum bodyPartsEnum("Head", 0xa); GakumasLocal::Misc::CSEnum bodyPartsEnum("Head", 0xa);
// bool rMousePressFlg = false; // bool rMousePressFlg = false;
@ -59,16 +62,16 @@ namespace GKCamera {
} }
} }
void camera_back() { // 后退 void camera_back(float multiplier = 1.0f) { // 后退
switch (cameraMode) { switch (cameraMode) {
case CameraMode::FREE: { case CameraMode::FREE: {
baseCamera.set_lon_move(180, LonMoveHState::LonMoveBack); baseCamera.set_lon_move(180, LonMoveHState::LonMoveBack, multiplier);
} break; } break;
case CameraMode::FIRST_PERSON: { case CameraMode::FIRST_PERSON: {
firstPersonPosOffset.z -= offsetMoveStep; firstPersonPosOffset.z -= offsetMoveStep * multiplier;
} break; } break;
case CameraMode::FOLLOW: { case CameraMode::FOLLOW: {
followPosOffset.z += offsetMoveStep; followPosOffset.z += offsetMoveStep * multiplier;
} }
} }
} }
@ -86,24 +89,24 @@ namespace GKCamera {
} }
} }
void camera_right() { // 向右 void camera_right(float multiplier = 1.0f) { // 向右
switch (cameraMode) { switch (cameraMode) {
case CameraMode::FREE: { case CameraMode::FREE: {
baseCamera.set_lon_move(-90); baseCamera.set_lon_move(-90, LonMoveLeftAndRight, multiplier);
} break; } break;
case CameraMode::FOLLOW: { case CameraMode::FOLLOW: {
// followPosOffset.x -= 0.8; // followPosOffset.x -= 0.8;
followLookAtOffset.x -= offsetMoveStep; followLookAtOffset.x -= offsetMoveStep * multiplier;
} }
default: default:
break; break;
} }
} }
void camera_down() { // 向下 void camera_down(float multiplier = 1.0f) { // 向下
switch (cameraMode) { switch (cameraMode) {
case CameraMode::FREE: { case CameraMode::FREE: {
float preStep = BaseCamera::moveStep / BaseCamera::smoothLevel; float preStep = BaseCamera::moveStep / BaseCamera::smoothLevel * multiplier;
for (int i = 0; i < BaseCamera::smoothLevel; i++) { for (int i = 0; i < BaseCamera::smoothLevel; i++) {
baseCamera.pos.y -= preStep; baseCamera.pos.y -= preStep;
@ -112,19 +115,19 @@ namespace GKCamera {
} }
} break; } break;
case CameraMode::FIRST_PERSON: { case CameraMode::FIRST_PERSON: {
firstPersonPosOffset.y -= offsetMoveStep; firstPersonPosOffset.y -= offsetMoveStep * multiplier;
} break; } break;
case CameraMode::FOLLOW: { case CameraMode::FOLLOW: {
// followPosOffset.y -= offsetMoveStep; // followPosOffset.y -= offsetMoveStep;
followLookAtOffset.y -= offsetMoveStep; followLookAtOffset.y -= offsetMoveStep * multiplier;
} }
} }
} }
void camera_up() { // 向上 void camera_up(float multiplier = 1.0f) { // 向上
switch (cameraMode) { switch (cameraMode) {
case CameraMode::FREE: { case CameraMode::FREE: {
float preStep = BaseCamera::moveStep / BaseCamera::smoothLevel; float preStep = BaseCamera::moveStep / BaseCamera::smoothLevel * multiplier;
for (int i = 0; i < BaseCamera::smoothLevel; i++) { for (int i = 0; i < BaseCamera::smoothLevel; i++) {
baseCamera.pos.y += preStep; baseCamera.pos.y += preStep;
@ -133,11 +136,11 @@ namespace GKCamera {
} }
} break; } break;
case CameraMode::FIRST_PERSON: { case CameraMode::FIRST_PERSON: {
firstPersonPosOffset.y += offsetMoveStep; firstPersonPosOffset.y += offsetMoveStep * multiplier;
} break; } break;
case CameraMode::FOLLOW: { case CameraMode::FOLLOW: {
// followPosOffset.y += offsetMoveStep; // followPosOffset.y += offsetMoveStep;
followLookAtOffset.y += offsetMoveStep; followLookAtOffset.y += offsetMoveStep * multiplier;
} }
} }
} }
@ -249,6 +252,142 @@ namespace GKCamera {
} }
} }
void ShowToast(const char *text) {
if (showToast) {
GakumasLocal::Log::ShowToast(text);
}
}
void JLThumbRight(float value) {
camera_right(value * l_sensitivity * baseCamera.fov / 60);
}
void JLThumbDown(float value) {
camera_back(value * l_sensitivity * baseCamera.fov / 60);
}
void JRThumbRight(float value) {
cameraLookat_right(value * r_sensitivity * baseCamera.fov / 60);
ChangeLiveFollowCameraOffsetX(-1 * value * r_sensitivity * baseCamera.fov / 60);
}
void JRThumbDown(float value) {
cameraLookat_down(value * r_sensitivity * baseCamera.fov / 60);
ChangeLiveFollowCameraOffsetY(-0.1 * value * r_sensitivity * baseCamera.fov / 60);
}
void JDadUp(){
reset_camera();
ShowToast("Reset Camera");
}
void JDadDown(){
ShowToast("Notification off, click again to turn it on.");
showToast = !showToast;
}
void JDadLeft(){
l_sensitivity = 1.0f;
ShowToast("Reset Movement Sensitivity");
}
void JDadRight(){
r_sensitivity = 1.0f;
ShowToast("Reset Camera Sensitivity");
}
void JAKeyDown() {
if (cameraMode == CameraMode::FOLLOW) {
const auto currPart = bodyPartsEnum.Next();
if (showToast) {
GakumasLocal::Log::ShowToastFmt("Look at: %s (0x%x)", currPart.first.c_str(),
currPart.second);
}
} else {
r_sensitivity *= 0.8f;
}
}
void JBKeyDown() {
if (cameraMode == CameraMode::FOLLOW) {
const auto currPart = bodyPartsEnum.Last();
if (showToast) {
GakumasLocal::Log::ShowToastFmt("Look at: %s (0x%x)", currPart.first.c_str(),
currPart.second);
}
} else {
r_sensitivity *= 1.2f;
}
}
void JXKeyDown() {
if (cameraMode == CameraMode::FOLLOW) {
OnLeftDown();
if (showToast) {
GakumasLocal::Log::ShowToastFmt("Look at position: %d", followCharaIndex);
}
} else {
l_sensitivity *= 0.8f;
}
}
void JYKeyDown() {
if (cameraMode == CameraMode::FOLLOW) {
OnRightDown();
if (showToast) {
GakumasLocal::Log::ShowToastFmt("Look at position: %d", followCharaIndex);
}
} else {
l_sensitivity *= 1.2f;
}
}
void JSelectKeyDown() {
switch (cameraMode) {
case CameraMode::FREE: {
cameraMode = CameraMode::FOLLOW;
ShowToast("Follow Mode");
} break;
case CameraMode::FOLLOW: {
cameraMode = CameraMode::FIRST_PERSON;
ShowToast("First-person Mode");
} break;
case CameraMode::FIRST_PERSON: {
cameraMode = CameraMode::FREE;
ShowToast("Free Mode");
} break;
}
}
void JStartKeyDown() {
switch (cameraMode) {
case CameraMode::FIRST_PERSON: {
if (firstPersonRoll == FirstPersonRoll::ENABLE_ROLL) {
firstPersonRoll = FirstPersonRoll::DISABLE_ROLL;
ShowToast("Camera Horizontal Fixed");
}
else {
firstPersonRoll = FirstPersonRoll::ENABLE_ROLL;
ShowToast("Camera Horizontal Rollable");
}
} break;
case CameraMode::FOLLOW: {
if (followModeY == FollowModeY::APPLY_Y) {
followModeY = FollowModeY::SMOOTH_Y;
ShowToast("Smooth Lift");
}
else {
followModeY = FollowModeY::APPLY_Y;
ShowToast("Instant Lift");
}
} break;
default: break;
}
}
UnityResolve::UnityType::Vector3 CalcPositionFromLookAt(const UnityResolve::UnityType::Vector3& target, UnityResolve::UnityType::Vector3 CalcPositionFromLookAt(const UnityResolve::UnityType::Vector3& target,
const UnityResolve::UnityType::Vector3& offset) { const UnityResolve::UnityType::Vector3& offset) {
// offset: z 远近, y 高低, x角度 // offset: z 远近, y 高低, x角度
@ -350,14 +489,50 @@ namespace GKCamera {
bool k = false; bool k = false;
bool j = false; bool j = false;
bool l = false; bool l = false;
float thumb_l_right = 0.0f;
float thumb_l_down = 0.0f;
bool thumb_l_button = false;
float thumb_r_right = 0.0f;
float thumb_r_down = 0.0f;
bool thumb_r_button = false;
bool dpad_up = false;
bool dpad_down = false;
bool dpad_left = false;
bool dpad_right = false;
bool a_button = false;
bool b_button = false;
bool x_button = false;
bool y_button = false;
bool lb_button = false;
float lt_button = 0.0f;
bool rb_button = false;
float rt_button = 0.0f;
bool select_button = false;
bool start_button = false;
bool share_button = false;
bool xbox_button = false;
bool threadRunning = false; bool threadRunning = false;
void resetAll() { void resetAll() {
auto p = reinterpret_cast<bool*>(this); // 获取当前对象的指针并转换为 unsigned char* 类型
const auto numMembers = sizeof(*this) / sizeof(bool); unsigned char* p = reinterpret_cast<unsigned char*>(this);
for (size_t idx = 0; idx < numMembers; ++idx) {
p[idx] = false; // 遍历对象的每个字节
} for (size_t offset = 0; offset < sizeof(*this); ) {
if (offset + sizeof(bool) <= sizeof(*this) && reinterpret_cast<bool*>(p + offset) == reinterpret_cast<bool*>(this) + offset / sizeof(bool)) {
// 如果当前偏移量适用于 bool 类型,则将其设置为 false
*reinterpret_cast<bool*>(p + offset) = false;
offset += sizeof(bool);
} else if (offset + sizeof(float) <= sizeof(*this) && reinterpret_cast<float*>(p + offset) == reinterpret_cast<float*>(this) + offset / sizeof(float)) {
// 如果当前偏移量适用于 float 类型,则将其设置为 0.0
*reinterpret_cast<float*>(p + offset) = 0.0f;
offset += sizeof(float);
} else {
// 处理未定义的情况(例如混合类型数组或其他类型成员)
// 可以根据实际情况调整逻辑或添加更多类型检查
offset += 1; // 跳过一个字节
}
}
} }
} cameraMoveState; } cameraMoveState;
@ -385,6 +560,32 @@ namespace GKCamera {
if (cameraMoveState.k) ChangeLiveFollowCameraOffsetY(-offsetMoveStep); if (cameraMoveState.k) ChangeLiveFollowCameraOffsetY(-offsetMoveStep);
if (cameraMoveState.j) ChangeLiveFollowCameraOffsetX(0.8); if (cameraMoveState.j) ChangeLiveFollowCameraOffsetX(0.8);
if (cameraMoveState.l) ChangeLiveFollowCameraOffsetX(-0.8); if (cameraMoveState.l) ChangeLiveFollowCameraOffsetX(-0.8);
// 手柄操作响应
// 左摇杆
if (std::abs(cameraMoveState.thumb_l_right) > 0.1f)
JLThumbRight(cameraMoveState.thumb_l_right);
if (std::abs(cameraMoveState.thumb_l_down) > 0.1f)
JLThumbDown(cameraMoveState.thumb_l_down);
// 右摇杆
if (std::abs(cameraMoveState.thumb_r_right) > 0.1f)
JRThumbRight(cameraMoveState.thumb_r_right);
if (std::abs(cameraMoveState.thumb_r_down) > 0.1f)
JRThumbDown(cameraMoveState.thumb_r_down);
// 左扳机
if (std::abs(cameraMoveState.lt_button) > 0.1f)
camera_down(cameraMoveState.lt_button * l_sensitivity * baseCamera.fov / 60);
// 右扳机
if (std::abs(cameraMoveState.rt_button) > 0.1f)
camera_up(cameraMoveState.rt_button * l_sensitivity * baseCamera.fov / 60);
// 左肩键
if (cameraMoveState.lb_button) changeCameraFOV(0.5f * r_sensitivity);
// 右肩键
if (cameraMoveState.rb_button) changeCameraFOV(-0.5f * r_sensitivity);
// 十字键
if (cameraMoveState.dpad_up) JDadUp();
// if (cameraMoveState.dpad_down) JDadDown();
if (cameraMoveState.dpad_left) JDadLeft();
if (cameraMoveState.dpad_right) JDadRight();
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
} }
}).detach(); }).detach();
@ -446,11 +647,88 @@ namespace GKCamera {
} break; } break;
case KEY_F: if (message == WM_KEYDOWN) SwitchCameraMode(); break; case KEY_F: if (message == WM_KEYDOWN) SwitchCameraMode(); break;
case KEY_V: if (message == WM_KEYDOWN) SwitchCameraSubMode(); break; case KEY_V: if (message == WM_KEYDOWN) SwitchCameraSubMode(); break;
// 手柄操作响应
case BTN_A:
cameraMoveState.a_button = message == WM_KEYDOWN;
if (message == WM_KEYDOWN) JAKeyDown();
break;
case BTN_B:
cameraMoveState.b_button = message == WM_KEYDOWN;
if (message == WM_KEYDOWN) JBKeyDown();
break;
case BTN_X:
cameraMoveState.x_button = message == WM_KEYDOWN;
if (message == WM_KEYDOWN) JXKeyDown();
break;
case BTN_Y:
cameraMoveState.y_button = message == WM_KEYDOWN;
if (message == WM_KEYDOWN) JYKeyDown();
break;
case BTN_LB:
cameraMoveState.lb_button = message == WM_KEYDOWN;
break;
case BTN_RB:
cameraMoveState.rb_button = message == WM_KEYDOWN;
break;
case BTN_THUMBL:
cameraMoveState.thumb_l_button = message == WM_KEYDOWN;
break;
case BTN_THUMBR:
cameraMoveState.thumb_r_button = message == WM_KEYDOWN;
break;
case BTN_SELECT:
cameraMoveState.select_button = message == WM_KEYDOWN;
if (message == WM_KEYDOWN) JSelectKeyDown();
break;
case BTN_START:
cameraMoveState.start_button = message == WM_KEYDOWN;
if (message == WM_KEYDOWN) JStartKeyDown();
break;
case BTN_SHARE:
cameraMoveState.share_button = message == WM_KEYDOWN;
break;
case BTN_XBOX:
cameraMoveState.xbox_button = message == WM_KEYDOWN;
break;
default: break; default: break;
} }
} }
} }
void
on_cam_rawinput_joystick(JoystickEvent event) {
int message = event.getMessage();
float leftStickX = event.getLeftStickX();
float leftStickY = event.getLeftStickY();
float rightStickX = event.getRightStickX();
float rightStickY = event.getRightStickY();
float leftTrigger = event.getLeftTrigger();
float rightTrigger = event.getRightTrigger();
float hatX = event.getHatX();
float hatY = event.getHatY();
cameraMoveState.thumb_l_right = (std::abs(leftStickX) > 0.1f) ? leftStickX : 0;
cameraMoveState.thumb_l_down = (std::abs(leftStickY) > 0.1f) ? leftStickY : 0;
cameraMoveState.thumb_r_right = (std::abs(rightStickX) > 0.1f) ? rightStickX : 0;
cameraMoveState.thumb_r_down = (std::abs(rightStickY) > 0.1f) ? rightStickY : 0;
cameraMoveState.lt_button = (std::abs(leftTrigger) > 0.1f) ? leftTrigger : 0;
cameraMoveState.rt_button = (std::abs(rightTrigger) > 0.1f) ? rightTrigger : 0;
cameraMoveState.dpad_up = hatY == -1.0f;
cameraMoveState.dpad_down = hatY == 1.0f;
cameraMoveState.dpad_left = hatX == -1.0f;
cameraMoveState.dpad_right = hatX == 1.0f;
if (cameraMoveState.dpad_down) {
JDadDown();
}
// GakumasLocal::Log::InfoFmt(
// "Motion event: action=%d, leftStickX=%.2f, leftStickY=%.2f, rightStickX=%.2f, rightStickY=%.2f, leftTrigger=%.2f, rightTrigger=%.2f, hatX=%.2f, hatY=%.2f",
// message, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger,
// rightTrigger, hatX, hatY);
}
void initCameraSettings() { void initCameraSettings() {
reset_camera(); reset_camera();
cameraRawInputThread(); cameraRawInputThread();

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "baseCamera.hpp" #include "baseCamera.hpp"
#include "Joystick/JoystickEvent.h"
namespace GKCamera { namespace GKCamera {
enum class CameraMode { enum class CameraMode {
@ -44,5 +45,6 @@ namespace GKCamera {
const bool recordY = false); const bool recordY = false);
void on_cam_rawinput_keyboard(int message, int key); void on_cam_rawinput_keyboard(int message, int key);
void on_cam_rawinput_joystick(JoystickEvent event);
void initCameraSettings(); void initCameraSettings();
} }

View File

@ -0,0 +1,67 @@
//
// Created by RanKaeder on 2024/6/18.
//
#ifndef GAKUMAS_LOCALIFY_JOYSTICKEVENT_H
#define GAKUMAS_LOCALIFY_JOYSTICKEVENT_H
class JoystickEvent {
public:
JoystickEvent(int message, float leftStickX, float leftStickY, float rightStickX,
float rightStickY, float leftTrigger, float rightTrigger,
float hatX, float hatY)
: message(message), leftStickX(leftStickX), leftStickY(leftStickY),
rightStickX(rightStickX), rightStickY(rightStickY), leftTrigger(leftTrigger),
rightTrigger(rightTrigger), hatX(hatX), hatY(hatY) {
}
// Getter 方法
int getMessage() const {
return message;
}
float getLeftStickX() const {
return leftStickX;
}
float getLeftStickY() const {
return leftStickY;
}
float getRightStickX() const {
return rightStickX;
}
float getRightStickY() const {
return rightStickY;
}
float getLeftTrigger() const {
return leftTrigger;
}
float getRightTrigger() const {
return rightTrigger;
}
float getHatX() const {
return hatX;
}
float getHatY() const {
return hatY;
}
private:
int message;
float leftStickX;
float leftStickY;
float rightStickX;
float rightStickY;
float leftTrigger;
float rightTrigger;
float hatX;
float hatY;
};
#endif //GAKUMAS_LOCALIFY_JOYSTICKEVENT_H

View File

@ -9,6 +9,7 @@
#include "xdl.h" #include "xdl.h"
#include "GakumasLocalify/camera/camera.hpp" #include "GakumasLocalify/camera/camera.hpp"
#include "GakumasLocalify/config/Config.hpp" #include "GakumasLocalify/config/Config.hpp"
#include "Joystick/JoystickEvent.h"
JavaVM* g_javaVM = nullptr; JavaVM* g_javaVM = nullptr;
jclass g_gakumasHookMainClass = nullptr; jclass g_gakumasHookMainClass = nullptr;
@ -87,6 +88,22 @@ Java_io_github_chinosk_gakumas_localify_GakumasHookMain_keyboardEvent(JNIEnv *en
} }
extern "C"
JNIEXPORT void JNICALL
Java_io_github_chinosk_gakumas_localify_GakumasHookMain_joystickEvent(JNIEnv *env, jclass clazz,
jint action,
jfloat leftStickX,
jfloat leftStickY,
jfloat rightStickX,
jfloat rightStickY,
jfloat leftTrigger,
jfloat rightTrigger,
jfloat hatX,
jfloat hatY) {
JoystickEvent event(action, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger, hatX, hatY);
GKCamera::on_cam_rawinput_joystick(event);
}
extern "C" extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_io_github_chinosk_gakumas_localify_GakumasHookMain_loadConfig(JNIEnv *env, jclass clazz, Java_io_github_chinosk_gakumas_localify_GakumasHookMain_loadConfig(JNIEnv *env, jclass clazz,
@ -94,4 +111,11 @@ Java_io_github_chinosk_gakumas_localify_GakumasHookMain_loadConfig(JNIEnv *env,
const auto configJsonStrChars = env->GetStringUTFChars(config_json_str, nullptr); const auto configJsonStrChars = env->GetStringUTFChars(config_json_str, nullptr);
const std::string configJson = configJsonStrChars; const std::string configJson = configJsonStrChars;
GakumasLocal::Config::LoadConfig(configJson); GakumasLocal::Config::LoadConfig(configJson);
}
extern "C"
JNIEXPORT void JNICALL
Java_io_github_chinosk_gakumas_localify_GakumasHookMain_pluginCallbackLooper(JNIEnv *env,
jclass clazz) {
GakumasLocal::Log::ToastLoop(env, clazz);
} }

View File

@ -0,0 +1,105 @@
package io.github.chinosk.gakumas.localify
import android.app.Activity
import android.content.Intent
import android.widget.Toast
import androidx.core.content.FileProvider
import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfigSerializer
import kotlinx.serialization.SerializationException
import java.io.File
interface IHasConfigItems {
var config: GakumasConfig
var programConfig: ProgramConfig
fun saveConfig() {} // do nothing
}
interface IConfigurableActivity<T : Activity> : IHasConfigItems
fun <T> T.getConfigContent(): String where T : Activity {
val configFile = File(filesDir, "gkms-config.json")
return if (configFile.exists()) {
configFile.readText()
} else {
Toast.makeText(this, "检测到第一次启动,初始化配置文件...", Toast.LENGTH_SHORT).show()
configFile.writeText("{}")
"{}"
}
}
fun <T> T.getProgramConfigContent(
excludes: List<String> = emptyList(),
origProgramConfig: ProgramConfig? = null
): String where T : Activity {
val configFile = File(filesDir, "localify-config.json")
if (excludes.isEmpty()) {
return if (configFile.exists()) {
configFile.readText()
} else {
"{}"
}
} else {
return if (origProgramConfig == null) {
if (configFile.exists()) {
val parsedConfig = json.decodeFromString<ProgramConfig>(configFile.readText())
json.encodeToString(ProgramConfigSerializer(excludes), parsedConfig)
} else {
"{}"
}
} else {
json.encodeToString(ProgramConfigSerializer(excludes), origProgramConfig)
}
}
}
fun <T> T.loadConfig() where T : Activity, T : IHasConfigItems {
val configStr = getConfigContent()
config = try {
json.decodeFromString<GakumasConfig>(configStr)
} catch (e: SerializationException) {
Toast.makeText(this, "配置文件异常: $e", Toast.LENGTH_SHORT).show()
GakumasConfig()
}
saveConfig()
val programConfigStr = getProgramConfigContent()
programConfig = try {
json.decodeFromString<ProgramConfig>(programConfigStr)
} catch (e: SerializationException) {
ProgramConfig()
}
}
fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
val intent = Intent().apply {
setClassName(
"com.bandainamcoent.idolmaster_gakuen",
"com.google.firebase.MessagingUnityPlayerActivity"
)
putExtra("gkmsData", getConfigContent())
putExtra(
"localData",
getProgramConfigContent(listOf("transRemoteZipUrl", "p"), programConfig)
)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val updateFile = File(filesDir, "update_trans.zip")
if (updateFile.exists()) {
val dirUri = FileProvider.getUriForFile(
this,
"io.github.chinosk.gakumas.localify.fileprovider",
File(updateFile.absolutePath)
)
intent.setDataAndType(dirUri, "resource/file")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
startActivity(intent)
}

View File

@ -1,11 +1,18 @@
package io.github.chinosk.gakumas.localify package io.github.chinosk.gakumas.localify
import android.view.KeyEvent import android.view.KeyEvent
import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModel
import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModelFactory
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
interface ConfigListener { interface ConfigListener {
fun onClickStartGame()
fun onEnabledChanged(value: Boolean) fun onEnabledChanged(value: Boolean)
fun onForceExportResourceChanged(value: Boolean) fun onForceExportResourceChanged(value: Boolean)
fun onTextTestChanged(value: Boolean) fun onTextTestChanged(value: Boolean)
@ -45,47 +52,75 @@ interface ConfigListener {
fun onBUseArmCorrectionChanged(value: Boolean) fun onBUseArmCorrectionChanged(value: Boolean)
fun onBUseScaleChanged(value: Boolean) fun onBUseScaleChanged(value: Boolean)
fun onBClickPresetChanged(index: Int) fun onBClickPresetChanged(index: Int)
fun onPCheckBuiltInAssetsChanged(value: Boolean)
fun onPUseRemoteAssetsChanged(value: Boolean)
fun onPCleanLocalAssetsChanged(value: Boolean)
fun onPDelRemoteAfterUpdateChanged(value: Boolean)
fun onPTransRemoteZipUrlChanged(s: CharSequence, start: Int, before: Int, count: Int)
fun mainPageAssetsViewDataUpdate(downloadAbleState: Boolean? = null,
downloadProgressState: Float? = null,
localResourceVersionState: String? = null,
errorString: String? = null)
}
class UserConfigViewModelFactory(private val initialValue: GakumasConfig) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UserConfigViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return UserConfigViewModel(initialValue) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
class UserConfigViewModel(initValue: GakumasConfig) : ViewModel() {
val configState = MutableStateFlow(initValue)
val config: StateFlow<GakumasConfig> = configState.asStateFlow()
} }
interface ConfigUpdateListener: ConfigListener { interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
var binding: ActivityMainBinding var factory: UserConfigViewModelFactory
var viewModel: UserConfigViewModel
var programConfigFactory: ProgramConfigViewModelFactory
var programConfigViewModel: ProgramConfigViewModel
fun pushKeyEvent(event: KeyEvent): Boolean fun pushKeyEvent(event: KeyEvent): Boolean
fun getConfigContent(): String fun checkConfigAndUpdateView() {} // do nothing
fun checkConfigAndUpdateView() // fun saveConfig()
fun saveConfig() fun saveProgramConfig()
override fun onEnabledChanged(value: Boolean) { override fun onEnabledChanged(value: Boolean) {
binding.config!!.enabled = value config.enabled = value
saveConfig() saveConfig()
pushKeyEvent(KeyEvent(1145, 29)) pushKeyEvent(KeyEvent(1145, 29))
} }
override fun onForceExportResourceChanged(value: Boolean) { override fun onForceExportResourceChanged(value: Boolean) {
binding.config!!.forceExportResource = value config.forceExportResource = value
saveConfig() saveConfig()
pushKeyEvent(KeyEvent(1145, 30)) pushKeyEvent(KeyEvent(1145, 30))
} }
override fun onTextTestChanged(value: Boolean) { override fun onTextTestChanged(value: Boolean) {
binding.config!!.textTest = value config.textTest = value
saveConfig() saveConfig()
} }
override fun onDumpTextChanged(value: Boolean) { override fun onDumpTextChanged(value: Boolean) {
binding.config!!.dumpText = value config.dumpText = value
saveConfig() saveConfig()
} }
override fun onEnableFreeCameraChanged(value: Boolean) { override fun onEnableFreeCameraChanged(value: Boolean) {
binding.config!!.enableFreeCamera = value config.enableFreeCamera = value
saveConfig() saveConfig()
} }
override fun onUnlockAllLiveChanged(value: Boolean) { override fun onUnlockAllLiveChanged(value: Boolean) {
binding.config!!.unlockAllLive = value config.unlockAllLive = value
saveConfig() saveConfig()
} }
@ -98,7 +133,7 @@ interface ConfigUpdateListener: ConfigListener {
} else { } else {
valueStr.toInt() valueStr.toInt()
} }
binding.config!!.targetFrameRate = value config.targetFrameRate = value
saveConfig() saveConfig()
} }
catch (e: Exception) { catch (e: Exception) {
@ -107,22 +142,22 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onLiveCustomeDressChanged(value: Boolean) { override fun onLiveCustomeDressChanged(value: Boolean) {
binding.config!!.enableLiveCustomeDress = value config.enableLiveCustomeDress = value
saveConfig() saveConfig()
} }
override fun onLiveCustomeCostumeIdChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onLiveCustomeCostumeIdChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.liveCustomeCostumeId = s.toString() config.liveCustomeCostumeId = s.toString()
saveConfig() saveConfig()
} }
override fun onUseCustomeGraphicSettingsChanged(value: Boolean) { override fun onUseCustomeGraphicSettingsChanged(value: Boolean) {
binding.config!!.useCustomeGraphicSettings = value config.useCustomeGraphicSettings = value
saveConfig() saveConfig()
} }
override fun onRenderScaleChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onRenderScaleChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.renderScale = try { config.renderScale = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -132,7 +167,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onQualitySettingsLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onQualitySettingsLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.qualitySettingsLevel = try { config.qualitySettingsLevel = try {
s.toString().toInt() s.toString().toInt()
} }
catch (e: Exception) { catch (e: Exception) {
@ -142,7 +177,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onVolumeIndexChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onVolumeIndexChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.volumeIndex = try { config.volumeIndex = try {
s.toString().toInt() s.toString().toInt()
} }
catch (e: Exception) { catch (e: Exception) {
@ -152,7 +187,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onMaxBufferPixelChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onMaxBufferPixelChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.maxBufferPixel = try { config.maxBufferPixel = try {
s.toString().toInt() s.toString().toInt()
} }
catch (e: Exception) { catch (e: Exception) {
@ -162,12 +197,12 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onLiveCustomeHeadIdChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onLiveCustomeHeadIdChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.liveCustomeHeadId = s.toString() config.liveCustomeHeadId = s.toString()
saveConfig() saveConfig()
} }
override fun onReflectionQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onReflectionQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.reflectionQualityLevel = try { config.reflectionQualityLevel = try {
val value = s.toString().toInt() val value = s.toString().toInt()
if (value > 5) 5 else value if (value > 5) 5 else value
} }
@ -178,7 +213,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onLodQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onLodQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.lodQualityLevel = try { config.lodQualityLevel = try {
val value = s.toString().toInt() val value = s.toString().toInt()
if (value > 5) 5 else value if (value > 5) 5 else value
} }
@ -191,44 +226,44 @@ interface ConfigUpdateListener: ConfigListener {
override fun onChangePresetQuality(level: Int) { override fun onChangePresetQuality(level: Int) {
when (level) { when (level) {
0 -> { 0 -> {
binding.config!!.renderScale = 0.5f config.renderScale = 0.5f
binding.config!!.qualitySettingsLevel = 1 config.qualitySettingsLevel = 1
binding.config!!.volumeIndex = 0 config.volumeIndex = 0
binding.config!!.maxBufferPixel = 1024 config.maxBufferPixel = 1024
binding.config!!.lodQualityLevel = 1 config.lodQualityLevel = 1
binding.config!!.reflectionQualityLevel = 1 config.reflectionQualityLevel = 1
} }
1 -> { 1 -> {
binding.config!!.renderScale = 0.59f config.renderScale = 0.59f
binding.config!!.qualitySettingsLevel = 1 config.qualitySettingsLevel = 1
binding.config!!.volumeIndex = 1 config.volumeIndex = 1
binding.config!!.maxBufferPixel = 1440 config.maxBufferPixel = 1440
binding.config!!.lodQualityLevel = 2 config.lodQualityLevel = 2
binding.config!!.reflectionQualityLevel = 2 config.reflectionQualityLevel = 2
} }
2 -> { 2 -> {
binding.config!!.renderScale = 0.67f config.renderScale = 0.67f
binding.config!!.qualitySettingsLevel = 2 config.qualitySettingsLevel = 2
binding.config!!.volumeIndex = 2 config.volumeIndex = 2
binding.config!!.maxBufferPixel = 2538 config.maxBufferPixel = 2538
binding.config!!.lodQualityLevel = 3 config.lodQualityLevel = 3
binding.config!!.reflectionQualityLevel = 3 config.reflectionQualityLevel = 3
} }
3 -> { 3 -> {
binding.config!!.renderScale = 0.77f config.renderScale = 0.77f
binding.config!!.qualitySettingsLevel = 3 config.qualitySettingsLevel = 3
binding.config!!.volumeIndex = 3 config.volumeIndex = 3
binding.config!!.maxBufferPixel = 3384 config.maxBufferPixel = 3384
binding.config!!.lodQualityLevel = 4 config.lodQualityLevel = 4
binding.config!!.reflectionQualityLevel = 4 config.reflectionQualityLevel = 4
} }
4 -> { 4 -> {
binding.config!!.renderScale = 1.0f config.renderScale = 1.0f
binding.config!!.qualitySettingsLevel = 5 config.qualitySettingsLevel = 5
binding.config!!.volumeIndex = 4 config.volumeIndex = 4
binding.config!!.maxBufferPixel = 8190 config.maxBufferPixel = 8190
binding.config!!.lodQualityLevel = 5 config.lodQualityLevel = 5
binding.config!!.reflectionQualityLevel = 5 config.reflectionQualityLevel = 5
} }
} }
checkConfigAndUpdateView() checkConfigAndUpdateView()
@ -236,33 +271,31 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onGameOrientationChanged(checkedId: Int) { override fun onGameOrientationChanged(checkedId: Int) {
when (checkedId) { if (checkedId in listOf(0, 1, 2)) {
R.id.radioButtonGameDefault -> binding.config!!.gameOrientation = 0 config.gameOrientation = checkedId
R.id.radioButtonGamePortrait -> binding.config!!.gameOrientation = 1
R.id.radioButtonGameLandscape -> binding.config!!.gameOrientation = 2
} }
saveConfig() saveConfig()
} }
override fun onEnableBreastParamChanged(value: Boolean) { override fun onEnableBreastParamChanged(value: Boolean) {
binding.config!!.enableBreastParam = value config.enableBreastParam = value
saveConfig() saveConfig()
checkConfigAndUpdateView() checkConfigAndUpdateView()
} }
override fun onBUseArmCorrectionChanged(value: Boolean) { override fun onBUseArmCorrectionChanged(value: Boolean) {
binding.config!!.bUseArmCorrection = value config.bUseArmCorrection = value
saveConfig() saveConfig()
} }
override fun onBUseScaleChanged(value: Boolean) { override fun onBUseScaleChanged(value: Boolean) {
binding.config!!.bUseScale = value config.bUseScale = value
saveConfig() saveConfig()
checkConfigAndUpdateView() checkConfigAndUpdateView()
} }
override fun onBDampingChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onBDampingChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.bDamping = try { config.bDamping = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -272,7 +305,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBStiffnessChanged(s: CharSequence, start: Int, before: Int, count: Int){ override fun onBStiffnessChanged(s: CharSequence, start: Int, before: Int, count: Int){
binding.config!!.bStiffness = try { config.bStiffness = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -282,7 +315,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBSpringChanged(s: CharSequence, start: Int, before: Int, count: Int){ override fun onBSpringChanged(s: CharSequence, start: Int, before: Int, count: Int){
binding.config!!.bSpring = try { config.bSpring = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -292,7 +325,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBPendulumChanged(s: CharSequence, start: Int, before: Int, count: Int){ override fun onBPendulumChanged(s: CharSequence, start: Int, before: Int, count: Int){
binding.config!!.bPendulum = try { config.bPendulum = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -302,7 +335,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBPendulumRangeChanged(s: CharSequence, start: Int, before: Int, count: Int){ override fun onBPendulumRangeChanged(s: CharSequence, start: Int, before: Int, count: Int){
binding.config!!.bPendulumRange = try { config.bPendulumRange = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -312,7 +345,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBAverageChanged(s: CharSequence, start: Int, before: Int, count: Int){ override fun onBAverageChanged(s: CharSequence, start: Int, before: Int, count: Int){
binding.config!!.bAverage = try { config.bAverage = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -322,7 +355,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBRootWeightChanged(s: CharSequence, start: Int, before: Int, count: Int){ override fun onBRootWeightChanged(s: CharSequence, start: Int, before: Int, count: Int){
binding.config!!.bRootWeight = try { config.bRootWeight = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -332,13 +365,13 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBUseLimitChanged(value: Boolean){ override fun onBUseLimitChanged(value: Boolean){
binding.config!!.bUseLimit = value config.bUseLimit = value
saveConfig() saveConfig()
checkConfigAndUpdateView() checkConfigAndUpdateView()
} }
override fun onBLimitXxChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onBLimitXxChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.bLimitXx = try { config.bLimitXx = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -348,7 +381,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBLimitXyChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onBLimitXyChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.bLimitXy = try { config.bLimitXy = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -358,7 +391,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBLimitYxChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onBLimitYxChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.bLimitYx = try { config.bLimitYx = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -368,7 +401,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBLimitYyChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onBLimitYyChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.bLimitYy = try { config.bLimitYy = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -378,7 +411,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBLimitZxChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onBLimitZxChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.bLimitZx = try { config.bLimitZx = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -388,7 +421,7 @@ interface ConfigUpdateListener: ConfigListener {
} }
override fun onBLimitZyChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onBLimitZyChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.bLimitZy = try { config.bLimitZy = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -399,7 +432,7 @@ interface ConfigUpdateListener: ConfigListener {
override fun onBScaleChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onBScaleChanged(s: CharSequence, start: Int, before: Int, count: Int) {
binding.config!!.bScale = try { config.bScale = try {
s.toString().toFloat() s.toString().toFloat()
} }
catch (e: Exception) { catch (e: Exception) {
@ -429,30 +462,62 @@ interface ConfigUpdateListener: ConfigListener {
1f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f) 1f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f)
} }
binding.config!!.bDamping = setData[0] config.bDamping = setData[0]
binding.config!!.bStiffness = setData[1] config.bStiffness = setData[1]
binding.config!!.bSpring = setData[2] config.bSpring = setData[2]
binding.config!!.bPendulum = setData[3] config.bPendulum = setData[3]
binding.config!!.bPendulumRange = setData[4] config.bPendulumRange = setData[4]
binding.config!!.bAverage = setData[5] config.bAverage = setData[5]
binding.config!!.bRootWeight = setData[6] config.bRootWeight = setData[6]
binding.config!!.bUseLimit = if (setData[7] == 0f) { config.bUseLimit = if (setData[7] == 0f) {
false false
} }
else { else {
binding.config!!.bLimitXx = setData[8] config.bLimitXx = setData[8]
binding.config!!.bLimitXy = setData[9] config.bLimitXy = setData[9]
binding.config!!.bLimitYx = setData[10] config.bLimitYx = setData[10]
binding.config!!.bLimitYy = setData[11] config.bLimitYy = setData[11]
binding.config!!.bLimitZx = setData[12] config.bLimitZx = setData[12]
binding.config!!.bLimitZy = setData[13] config.bLimitZy = setData[13]
true true
} }
binding.config!!.bUseArmCorrection = true config.bUseArmCorrection = true
checkConfigAndUpdateView() checkConfigAndUpdateView()
saveConfig() saveConfig()
} }
} override fun onPCheckBuiltInAssetsChanged(value: Boolean) {
programConfig.checkBuiltInAssets = value
saveProgramConfig()
}
override fun onPUseRemoteAssetsChanged(value: Boolean) {
programConfig.useRemoteAssets = value
saveProgramConfig()
}
override fun onPCleanLocalAssetsChanged(value: Boolean) {
programConfig.cleanLocalAssets = value
saveProgramConfig()
}
override fun onPDelRemoteAfterUpdateChanged(value: Boolean) {
programConfig.delRemoteAfterUpdate = value
saveProgramConfig()
}
override fun onPTransRemoteZipUrlChanged(s: CharSequence, start: Int, before: Int, count: Int) {
programConfig.transRemoteZipUrl = s.toString()
saveProgramConfig()
}
override fun mainPageAssetsViewDataUpdate(downloadAbleState: Boolean?, downloadProgressState: Float?,
localResourceVersionState: String?, errorString: String?) {
downloadAbleState?.let { programConfigViewModel.downloadAbleState.value = downloadAbleState }
downloadProgressState?.let{ programConfigViewModel.downloadProgressState.value = downloadProgressState }
localResourceVersionState?.let{ programConfigViewModel.localResourceVersionState.value = localResourceVersionState }
errorString?.let{ programConfigViewModel.errorStringState.value = errorString }
}
}

View File

@ -11,21 +11,30 @@ import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.view.KeyEvent
import android.view.MotionEvent
import android.widget.Toast
import com.bytedance.shadowhook.ShadowHook import com.bytedance.shadowhook.ShadowHook
import com.bytedance.shadowhook.ShadowHook.ConfigBuilder import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.IXposedHookZygoteInit import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage import de.robv.android.xposed.callbacks.XC_LoadPackage
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
import android.view.KeyEvent
import android.widget.Toast
import com.google.gson.Gson
import de.robv.android.xposed.XposedBridge
import io.github.chinosk.gakumas.localify.models.GakumasConfig import io.github.chinosk.gakumas.localify.models.GakumasConfig
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import kotlin.system.measureTimeMillis
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.ProgramConfig
val TAG = "GakumasLocalify" val TAG = "GakumasLocalify"
@ -39,8 +48,23 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private var gkmsDataInited = false private var gkmsDataInited = false
private var getConfigError: Exception? = null private var getConfigError: Exception? = null
private var externalFilesChecked: Boolean = false
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
// if (lpparam.packageName == "io.github.chinosk.gakumas.localify") {
// XposedHelpers.findAndHookMethod(
// "io.github.chinosk.gakumas.localify.MainActivity",
// lpparam.classLoader,
// "showToast",
// String::class.java,
// object : XC_MethodHook() {
// override fun beforeHookedMethod(param: MethodHookParam) {
// Log.d(TAG, "beforeHookedMethod hooked: ${param.args}")
// }
// }
// )
// }
if (lpparam.packageName != targetPackageName) { if (lpparam.packageName != targetPackageName) {
return return
} }
@ -61,6 +85,50 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
} }
) )
XposedHelpers.findAndHookMethod(
"android.app.Activity",
lpparam.classLoader,
"dispatchGenericMotionEvent",
MotionEvent::class.java,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
val motionEvent = param.args[0] as MotionEvent
val action = motionEvent.action
// 左摇杆的X和Y轴
val leftStickX = motionEvent.getAxisValue(MotionEvent.AXIS_X)
val leftStickY = motionEvent.getAxisValue(MotionEvent.AXIS_Y)
// 右摇杆的X和Y轴
val rightStickX = motionEvent.getAxisValue(MotionEvent.AXIS_Z)
val rightStickY = motionEvent.getAxisValue(MotionEvent.AXIS_RZ)
// 左扳机
val leftTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER)
// 右扳机
val rightTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER)
// 十字键
val hatX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X)
val hatY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y)
// 处理摇杆和扳机事件
joystickEvent(
action,
leftStickX,
leftStickY,
rightStickX,
rightStickY,
leftTrigger,
rightTrigger,
hatX,
hatY
)
}
}
)
val appActivityClass = XposedHelpers.findClass("android.app.Activity", lpparam.classLoader) val appActivityClass = XposedHelpers.findClass("android.app.Activity", lpparam.classLoader)
XposedBridge.hookAllMethods(appActivityClass, "onStart", object : XC_MethodHook() { XposedBridge.hookAllMethods(appActivityClass, "onStart", object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) { override fun beforeHookedMethod(param: MethodHookParam) {
@ -118,7 +186,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
requestConfig(app.applicationContext) requestConfig(app.applicationContext)
} }
FilesChecker.initAndCheck(app.filesDir, modulePath) FilesChecker.initDir(app.filesDir, modulePath)
initHook( initHook(
"${app.applicationInfo.nativeLibraryDir}/libil2cpp.so", "${app.applicationInfo.nativeLibraryDir}/libil2cpp.so",
File( File(
@ -130,23 +198,74 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
alreadyInitialized = true alreadyInitialized = true
} }
}) })
startLoop()
}
@OptIn(DelicateCoroutinesApi::class)
private fun startLoop() {
GlobalScope.launch {
val interval = 1000L / 30
while (isActive) {
val timeTaken = measureTimeMillis {
pluginCallbackLooper()
}
delay(interval - timeTaken)
}
}
} }
fun initGkmsConfig(activity: Activity) { fun initGkmsConfig(activity: Activity) {
val intent = activity.intent val intent = activity.intent
val gkmsData = intent.getStringExtra("gkmsData") val gkmsData = intent.getStringExtra("gkmsData")
val programData = intent.getStringExtra("localData")
if (gkmsData != null) { if (gkmsData != null) {
gkmsDataInited = true gkmsDataInited = true
val initConfig = try { val initConfig = try {
Gson().fromJson(gkmsData, GakumasConfig::class.java) json.decodeFromString<GakumasConfig>(gkmsData)
} }
catch (e: Exception) { catch (e: Exception) {
null null
} }
val programConfig = try {
if (programData == null) {
ProgramConfig()
} else {
json.decodeFromString<ProgramConfig>(programData)
}
}
catch (e: Exception) {
null
}
// 清理本地文件
if (programConfig?.cleanLocalAssets == true) {
FilesChecker.cleanAssets()
}
// 检查 files 版本和 assets 版本并更新
if (programConfig?.checkBuiltInAssets == true) {
FilesChecker.initAndCheck(activity.filesDir, modulePath)
}
// 强制导出 assets 文件
if (initConfig?.forceExportResource == true) { if (initConfig?.forceExportResource == true) {
FilesChecker.updateFiles() FilesChecker.updateFiles()
} }
// 使用热更新文件
if (programConfig?.useRemoteAssets == true) {
val dataUri = intent.data
if (dataUri != null) {
if (!externalFilesChecked) {
externalFilesChecked = true
// Log.d(TAG, "dataUri: $dataUri")
FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir,
programConfig.delRemoteAfterUpdate)
}
}
}
loadConfig(gkmsData) loadConfig(gkmsData)
Log.d(TAG, "gkmsData: $gkmsData") Log.d(TAG, "gkmsData: $gkmsData")
} }
@ -228,7 +347,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
fun requestConfig(activity: Context) { fun requestConfig(activity: Context) {
try { try {
val intent = Intent().apply { val intent = Intent().apply {
setClassName("io.github.chinosk.gakumas.localify", "io.github.chinosk.gakumas.localify.MainActivity") setClassName("io.github.chinosk.gakumas.localify", "io.github.chinosk.gakumas.localify.TranslucentActivity")
putExtra("gkmsData", "requestConfig") putExtra("gkmsData", "requestConfig")
flags = FLAG_ACTIVITY_NEW_TASK flags = FLAG_ACTIVITY_NEW_TASK
} }
@ -256,8 +375,23 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
@JvmStatic @JvmStatic
external fun keyboardEvent(keyCode: Int, action: Int) external fun keyboardEvent(keyCode: Int, action: Int)
@JvmStatic @JvmStatic
external fun joystickEvent(
action: Int,
leftStickX: Float,
leftStickY: Float,
rightStickX: Float,
rightStickY: Float,
leftTrigger: Float,
rightTrigger: Float,
hatX: Float,
hatY: Float
)
@JvmStatic
external fun loadConfig(configJsonStr: String) external fun loadConfig(configJsonStr: String)
// Toast快速切换内容
private var toast: Toast? = null
@JvmStatic @JvmStatic
fun showToast(message: String) { fun showToast(message: String) {
val app = AndroidAppHelper.currentApplication() val app = AndroidAppHelper.currentApplication()
@ -265,13 +399,21 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
if (context != null) { if (context != null) {
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
handler.post { handler.post {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show() // 取消之前的 Toast
toast?.cancel()
// 创建新的 Toast
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
// 展示新的 Toast
toast?.show()
} }
} }
else { else {
Log.e(TAG, "showToast: $message failed: applicationContext is null") Log.e(TAG, "showToast: $message failed: applicationContext is null")
} }
} }
@JvmStatic
external fun pluginCallbackLooper()
} }
init { init {

View File

@ -1,165 +1,198 @@
package io.github.chinosk.gakumas.localify package io.github.chinosk.gakumas.localify
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.KeyEvent 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 android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.activity.ComponentActivity
import androidx.databinding.DataBindingUtil import androidx.activity.compose.setContent
import com.google.android.material.button.MaterialButton import androidx.compose.runtime.Composable
import com.google.android.material.floatingactionbutton.FloatingActionButton import androidx.compose.runtime.State
import com.google.gson.Gson import androidx.compose.runtime.collectAsState
import com.google.gson.JsonSyntaxException import androidx.lifecycle.ViewModelProvider
import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
import io.github.chinosk.gakumas.localify.hookUtils.MainKeyEventDispatcher import io.github.chinosk.gakumas.localify.hookUtils.MainKeyEventDispatcher
import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.GakumasConfig import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModel
import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModelFactory
import io.github.chinosk.gakumas.localify.ui.pages.MainUI
import io.github.chinosk.gakumas.localify.ui.theme.GakumasLocalifyTheme
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.encodeToString
import java.io.File import java.io.File
class MainActivity : AppCompatActivity(), ConfigUpdateListener { class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableActivity<MainActivity> {
override lateinit var binding: ActivityMainBinding override lateinit var config: GakumasConfig
private val TAG = "GakumasLocalify" override lateinit var programConfig: ProgramConfig
override fun onCreate(savedInstanceState: Bundle?) { override lateinit var factory: UserConfigViewModelFactory
super.onCreate(savedInstanceState) override lateinit var viewModel: UserConfigViewModel
setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main) override lateinit var programConfigFactory: ProgramConfigViewModelFactory
loadConfig() override lateinit var programConfigViewModel: ProgramConfigViewModel
binding.listener = this
val requestData = intent.getStringExtra("gkmsData")
if (requestData != null) {
if (requestData == "requestConfig") {
onClickStartGame()
finish()
}
}
showVersion()
val scrollView: ScrollView = findViewById(R.id.scrollView)
scrollView.viewTreeObserver.addOnScrollChangedListener { onScrollChanged() }
onScrollChanged()
val coordinatorLayout = findViewById<View>(R.id.coordinatorLayout)
coordinatorLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
onScrollChanged()
coordinatorLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})
}
override fun onClickStartGame() {
val intent = Intent().apply {
setClassName("com.bandainamcoent.idolmaster_gakuen", "com.google.firebase.MessagingUnityPlayerActivity")
putExtra("gkmsData", getConfigContent())
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
}
private fun onScrollChanged() {
val fab: FloatingActionButton = findViewById(R.id.fabStartGame)
val startGameButton: MaterialButton = findViewById(R.id.StartGameButton)
val scrollView: ScrollView = findViewById(R.id.scrollView)
val location = IntArray(2)
startGameButton.getLocationOnScreen(location)
val buttonTop = location[1]
val buttonBottom = buttonTop + startGameButton.height
val scrollViewLocation = IntArray(2)
scrollView.getLocationOnScreen(scrollViewLocation)
val scrollViewTop = scrollViewLocation[1]
val scrollViewBottom = scrollViewTop + scrollView.height
val isButtonVisible = buttonTop >= scrollViewTop && buttonBottom <= scrollViewBottom
if (isButtonVisible) {
fab.hide()
} else {
fab.show()
}
}
private fun showToast(message: String) { private fun showToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
} }
override fun getConfigContent(): String {
val configFile = File(filesDir, "gkms-config.json")
return if (configFile.exists()) {
configFile.readText()
}
else {
showToast("Initializing configuration file...")
"{}"
}
}
override fun saveConfig() { override fun saveConfig() {
try {
config.pf = false
viewModel.configState.value = config.copy( pf = true ) // 更新 UI
}
catch (e: RuntimeException) {
Log.d(TAG, e.toString())
}
val configFile = File(filesDir, "gkms-config.json") val configFile = File(filesDir, "gkms-config.json")
configFile.writeText(Gson().toJson(binding.config!!)) configFile.writeText(json.encodeToString(config))
} }
@SuppressLint("SetTextI18n") override fun saveProgramConfig() {
private fun showVersion() { try {
val titleLabel = findViewById<TextView>(R.id.textViewTitle) programConfig.p = false
val versionLabel = findViewById<TextView>(R.id.textViewResVersion) programConfigViewModel.configState.value = programConfig.copy( p = true ) // 更新 UI
var versionText = "unknown" }
catch (e: RuntimeException) {
Log.d(TAG, e.toString())
}
val configFile = File(filesDir, "localify-config.json")
configFile.writeText(json.encodeToString(programConfig))
}
fun getVersion(): List<String> {
var versionText = ""
var resVersionText = "unknown"
try { try {
val stream = assets.open("${FilesChecker.localizationFilesDir}/version.txt") val stream = assets.open("${FilesChecker.localizationFilesDir}/version.txt")
versionText = FilesChecker.convertToString(stream) resVersionText = FilesChecker.convertToString(stream)
val packInfo = packageManager.getPackageInfo(packageName, 0) val packInfo = packageManager.getPackageInfo(packageName, 0)
val version = packInfo.versionName val version = packInfo.versionName
titleLabel.text = "${titleLabel.text} $version" val versionCode = packInfo.longVersionCode
versionText = "$version ($versionCode)"
} }
catch (_: Exception) {} catch (_: Exception) {}
versionLabel.text = "Assets Version: $versionText"
return listOf(versionText, resVersionText)
} }
private fun loadConfig() { fun openUrl(url: String) {
val configStr = getConfigContent() val webpage = Uri.parse(url)
binding.config = try { val intent = Intent(Intent.ACTION_VIEW, webpage)
Gson().fromJson(configStr, GakumasConfig::class.java) startActivity(intent)
}
catch (e: JsonSyntaxException) {
showToast("Configuration file has been reset: $e")
Gson().fromJson("{}", GakumasConfig::class.java)
}
saveConfig()
}
override fun checkConfigAndUpdateView() {
binding.config = binding.config
binding.notifyChange()
} }
override fun pushKeyEvent(event: KeyEvent): Boolean { override fun pushKeyEvent(event: KeyEvent): Boolean {
return dispatchKeyEvent(event) return dispatchKeyEvent(event)
} }
@SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
// Log.d(TAG, "${event.keyCode}, ${event.action}") // Log.d(TAG, "${event.keyCode}, ${event.action}")
if (MainKeyEventDispatcher.checkDbgKey(event.keyCode, event.action)) { if (MainKeyEventDispatcher.checkDbgKey(event.keyCode, event.action)) {
val origDbg = binding.config?.dbgMode val origDbg = config.dbgMode
if (origDbg != null) { config.dbgMode = !origDbg
binding.config!!.dbgMode = !origDbg checkConfigAndUpdateView()
checkConfigAndUpdateView() saveConfig()
saveConfig() showToast("Test Mode: ${!origDbg}")
showToast("Test Mode: ${!origDbg}")
}
} }
return if (event.action == 1145) true else super.dispatchKeyEvent(event) return if (event.action == 1145) true else super.dispatchKeyEvent(event)
} }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loadConfig()
factory = UserConfigViewModelFactory(config)
viewModel = ViewModelProvider(this, factory)[UserConfigViewModel::class.java]
programConfigFactory = ProgramConfigViewModelFactory(programConfig,
FileHotUpdater.getZipResourceVersion(File(filesDir, "update_trans.zip").absolutePath).toString()
)
programConfigViewModel = ViewModelProvider(this, programConfigFactory)[ProgramConfigViewModel::class.java]
setContent {
GakumasLocalifyTheme(dynamicColor = false, darkTheme = false) {
MainUI(context = this)
}
}
}
}
@Composable
fun getConfigState(context: MainActivity?, previewData: GakumasConfig?): State<GakumasConfig> {
return if (context != null) {
context.viewModel.config.collectAsState()
}
else {
val configMSF = MutableStateFlow(previewData!!)
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramConfigState(context: MainActivity?, previewData: ProgramConfig? = null): State<ProgramConfig> {
return if (context != null) {
context.programConfigViewModel.config.collectAsState()
}
else {
val configMSF = MutableStateFlow(previewData ?: ProgramConfig())
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramDownloadState(context: MainActivity?): State<Float> {
return if (context != null) {
context.programConfigViewModel.downloadProgress.collectAsState()
}
else {
val configMSF = MutableStateFlow(0f)
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramDownloadAbleState(context: MainActivity?): State<Boolean> {
return if (context != null) {
context.programConfigViewModel.downloadAble.collectAsState()
}
else {
val configMSF = MutableStateFlow(true)
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramLocalResourceVersionState(context: MainActivity?): State<String> {
return if (context != null) {
context.programConfigViewModel.localResourceVersion.collectAsState()
}
else {
val configMSF = MutableStateFlow("null")
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramDownloadErrorStringState(context: MainActivity?): State<String> {
return if (context != null) {
context.programConfigViewModel.errorString.collectAsState()
}
else {
val configMSF = MutableStateFlow("")
configMSF.asStateFlow().collectAsState()
}
}

View File

@ -0,0 +1,24 @@
package io.github.chinosk.gakumas.localify
import android.os.Bundle
import androidx.activity.ComponentActivity
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfig
class TranslucentActivity : ComponentActivity(), IConfigurableActivity<TranslucentActivity> {
override lateinit var config: GakumasConfig
override lateinit var programConfig: ProgramConfig
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loadConfig()
val requestData = intent.getStringExtra("gkmsData")
if (requestData != null) {
if (requestData == "requestConfig") {
onClickStartGame()
finish()
}
}
}
}

View File

@ -0,0 +1,181 @@
package io.github.chinosk.gakumas.localify.hookUtils
import android.app.Activity
import android.net.Uri
import android.util.Log
import io.github.chinosk.gakumas.localify.GakumasHookMain
import io.github.chinosk.gakumas.localify.TAG
import java.io.BufferedReader
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.InputStreamReader
import java.util.zip.ZipInputStream
object FileHotUpdater {
private fun unzip(zipFile: InputStream, destDir: String, matchNamePrefix: String = "",
replaceMatchNamePrefix: String? = null) {
val buffer = ByteArray(1024)
try {
val folder = File(destDir)
if (!folder.exists()) {
folder.mkdir()
}
val zipIn = ZipInputStream(zipFile)
var entry = zipIn.nextEntry
while (entry != null) {
var writeEntryName = entry.name
if (matchNamePrefix.isNotEmpty()) {
if (!entry.name.startsWith(matchNamePrefix)) {
zipIn.closeEntry()
entry = zipIn.nextEntry
continue
}
replaceMatchNamePrefix?.let {
writeEntryName = replaceMatchNamePrefix + writeEntryName.substring(
matchNamePrefix.length, writeEntryName.length
)
}
}
val filePath = destDir + File.separator + writeEntryName
if (!entry.isDirectory) {
extractFile(zipIn, filePath, buffer)
} else {
val dir = File(filePath)
dir.mkdirs()
}
zipIn.closeEntry()
entry = zipIn.nextEntry
}
zipIn.close()
} catch (e: Exception) {
Log.e(TAG, "unzip error: $e")
}
}
private fun unzip(zipFile: String, destDir: String, matchNamePrefix: String = "") {
return unzip(FileInputStream(zipFile), destDir, matchNamePrefix)
}
private fun extractFile(zipIn: ZipInputStream, filePath: String, buffer: ByteArray) {
val fout = FileOutputStream(filePath)
var length: Int
while (zipIn.read(buffer).also { length = it } > 0) {
fout.write(buffer, 0, length)
}
fout.close()
}
private fun getZipResourcePath(zipFile: InputStream): String? {
try {
val zipIn = ZipInputStream(zipFile)
var entry = zipIn.nextEntry
while (entry != null) {
if (entry.isDirectory) {
if (entry.name.endsWith("local-files/")) {
zipIn.close()
var retPath = File(entry.name, "..").canonicalPath
if (retPath.startsWith("/")) retPath = retPath.substring(1)
return retPath
}
}
zipIn.closeEntry()
entry = zipIn.nextEntry
}
zipIn.close()
}
catch (e: Exception) {
Log.e(TAG, "getZipResourcePath error: $e")
}
return null
}
private fun getZipResourceVersion(zipFile: InputStream, basePath: String): String? {
try {
val targetVersionFilePath = File(basePath, "version.txt").canonicalPath
val zipIn = ZipInputStream(zipFile)
var entry = zipIn.nextEntry
while (entry != null) {
if (!entry.isDirectory) {
if ("/${entry.name}" == targetVersionFilePath) {
Log.d(TAG, "targetVersionFilePath: $targetVersionFilePath")
val reader = BufferedReader(InputStreamReader(zipIn))
val versionContent = reader.use { it.readText() }
Log.d(TAG, "versionContent: $versionContent")
zipIn.close()
return versionContent
}
}
zipIn.closeEntry()
entry = zipIn.nextEntry
}
zipIn.close()
}
catch (e: Exception) {
Log.e(TAG, "getZipResourceVersion error: $e")
}
return null
}
private fun getZipResourceVersion(zipFile: String, basePath: String): String? {
return getZipResourceVersion(FileInputStream(zipFile), basePath)
}
fun getZipResourceVersion(zipFile: String): String? {
return try {
val basePath = getZipResourcePath(FileInputStream(zipFile))
basePath?.let { getZipResourceVersion(zipFile, it) }
}
catch (_: Exception) {
null
}
}
fun updateFilesFromZip(activity: Activity, zipFileUri: Uri, filesDir: File, deleteAfterUpdate: Boolean) {
try {
GakumasHookMain.showToast("Updating files from zip...")
var basePath: String?
activity.contentResolver.openInputStream(zipFileUri).use {
basePath = it?.let { getZipResourcePath(it) }
if (basePath == null) {
Log.e(TAG, "getZipResourcePath failed.")
return@updateFilesFromZip
}
}
/*
var resourceVersion: String?
activity.contentResolver.openInputStream(zipFileUri).use {
resourceVersion = it?.let { getZipResourceVersion(it, basePath!!) }
Log.d(TAG, "resourceVersion: $resourceVersion ($basePath)")
}*/
activity.contentResolver.openInputStream(zipFileUri).use {
it?.let {
unzip(it, File(filesDir, FilesChecker.localizationFilesDir).absolutePath,
basePath!!, "../gakumas-local/")
if (deleteAfterUpdate) {
activity.contentResolver.delete(zipFileUri, null, null)
}
GakumasHookMain.showToast("Update success.")
}
}
}
catch (e: java.io.FileNotFoundException) {
Log.i(TAG, "updateFilesFromZip - file not found: $e")
GakumasHookMain.showToast("Update file not found.")
}
catch (e: Exception) {
Log.e(TAG, "updateFilesFromZip failed: $e")
GakumasHookMain.showToast("Updating files failed: $e")
}
}
}

View File

@ -16,12 +16,16 @@ object FilesChecker {
var filesUpdated = false var filesUpdated = false
fun initAndCheck(fileDir: File, modulePath: String) { fun initAndCheck(fileDir: File, modulePath: String) {
this.filesDir = fileDir initDir(fileDir, modulePath)
this.modulePath = modulePath
checkFiles() checkFiles()
} }
fun initDir(fileDir: File, modulePath: String) {
this.filesDir = fileDir
this.modulePath = modulePath
}
fun checkFiles() { fun checkFiles() {
val installedVersion = getInstalledVersion() val installedVersion = getInstalledVersion()
val pluginVersion = getPluginVersion() val pluginVersion = getPluginVersion()
@ -118,4 +122,45 @@ object FilesChecker {
return stringBuilder.toString() return stringBuilder.toString()
} }
private fun deleteRecursively(file: File): Boolean {
if (file.isDirectory) {
val children = file.listFiles()
if (children != null) {
for (child in children) {
val success = deleteRecursively(child)
if (!success) {
return false
}
}
}
}
return file.delete()
}
fun cleanAssets() {
val pluginBasePath = File(filesDir, localizationFilesDir)
val localFilesDir = File(pluginBasePath, "local-files")
val fontFile = File(localFilesDir, "gkamsZHFontMIX.otf")
val resourceDir = File(localFilesDir, "resource")
val genericTransDir = File(localFilesDir, "genericTrans")
val genericTransFile = File(localFilesDir, "generic.json")
val i18nFile = File(localFilesDir, "localization.json")
if (fontFile.exists()) {
fontFile.delete()
}
if (deleteRecursively(resourceDir)) {
resourceDir.mkdirs()
}
if (deleteRecursively(genericTransDir)) {
genericTransDir.mkdirs()
}
if (genericTransFile.exists()) {
genericTransFile.writeText("{}")
}
if (i18nFile.exists()) {
i18nFile.writeText("{}")
}
}
} }

View File

@ -1,11 +1,10 @@
package io.github.chinosk.gakumas.localify.hookUtils package io.github.chinosk.gakumas.localify.hookUtils
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
object MainKeyEventDispatcher { object MainKeyEventDispatcher {
private val targetDbgKeyList: IntArray = intArrayOf(19, 19, 20, 20, 21, 22, 21, 22, 30, 29) private val targetDbgKeyList: IntArray = intArrayOf(19, 19, 20, 20, 21, 22, 21, 22, 30, 29)
private var currentIndex = 0; private var currentIndex = 0
fun checkDbgKey(code: Int, action: Int): Boolean { fun checkDbgKey(code: Int, action: Int): Boolean {
if (action == KeyEvent.ACTION_UP) return false if (action == KeyEvent.ACTION_UP) return false

View File

@ -0,0 +1,137 @@
package io.github.chinosk.gakumas.localify.mainUtils
import okhttp3.*
import java.io.IOException
import java.io.ByteArrayOutputStream
import java.util.concurrent.TimeUnit
object FileDownloader {
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(0, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
.build()
private var call: Call? = null
fun downloadFile(
url: String,
onDownload: (Float, downloaded: Long, size: Long) -> Unit,
onSuccess: (ByteArray) -> Unit,
onFailed: (Int, String) -> Unit,
checkContentTypes: List<String>? = null
) {
try {
if (call != null) {
onFailed(-1, "Another file is downloading.")
return
}
val request = Request.Builder()
.url(url)
.build()
call = client.newCall(request)
call?.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
this@FileDownloader.call = null
if (call.isCanceled()) {
onFailed(-1, "Download canceled")
} else {
onFailed(-1, e.message ?: "Unknown error")
}
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
this@FileDownloader.call = null
onFailed(response.code, response.message)
return
}
if (checkContentTypes != null) {
val contentType = response.header("Content-Type")
if (!checkContentTypes.contains(contentType)) {
onFailed(-1, "Unexpected content type: $contentType")
this@FileDownloader.call = null
return
}
}
response.body?.let { responseBody ->
val contentLength = responseBody.contentLength()
val inputStream = responseBody.byteStream()
val buffer = ByteArray(8 * 1024)
var downloadedBytes = 0L
var read: Int
val outputStream = ByteArrayOutputStream()
try {
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
downloadedBytes += read
val progress = if (contentLength < 0) {
0f
}
else {
downloadedBytes.toFloat() / contentLength
}
onDownload(progress, downloadedBytes, contentLength)
}
onSuccess(outputStream.toByteArray())
} catch (e: IOException) {
if (call.isCanceled()) {
onFailed(-1, "Download canceled")
} else {
onFailed(-1, e.message ?: "Error reading stream")
}
} finally {
this@FileDownloader.call = null
inputStream.close()
outputStream.close()
}
} ?: run {
this@FileDownloader.call = null
onFailed(-1, "Response body is null")
}
}
})
}
catch (e: Exception) {
onFailed(-1, e.toString())
call = null
}
}
fun cancel() {
call?.cancel()
this@FileDownloader.call = null
}
/**
* return: Status, newString
* Status: 0 - not change, 1 - need check, 2 - modified, 3 - checked
**/
fun checkAndChangeDownloadURL(url: String, forceEdit: Boolean = false): Pair<Int, String> {
if (!url.startsWith("https://github.com/")) { // check github only
return Pair(0, url)
}
if (url.endsWith(".zip")) {
return Pair(0, url)
}
// https://github.com/chinosk6/GakumasTranslationData
// https://github.com/chinosk6/GakumasTranslationData.git
// https://github.com/chinosk6/GakumasTranslationData/archive/refs/heads/main.zip
if (url.endsWith(".git")) {
return Pair(2, "${url.substring(0, url.length - 4)}/archive/refs/heads/main.zip")
}
if (forceEdit) {
return Pair(3, "$url/archive/refs/heads/main.zip")
}
return Pair(1, url)
}
}

View File

@ -0,0 +1,7 @@
package io.github.chinosk.gakumas.localify.mainUtils
import kotlinx.serialization.json.Json
val json = Json {
encodeDefaults = true
}

View File

@ -0,0 +1,31 @@
package io.github.chinosk.gakumas.localify.models
import kotlinx.serialization.Serializable
@Serializable
data class AboutPageConfig(
val plugin_repo: String = "https://github.com/chinosk6/gakuen-imas-localify",
val main_contributors: List<MainContributors> = listOf(),
val contrib_img: ContribImg = ContribImg(
"https://contrib.rocks/image?repo=chinosk6/gakuen-imas-localify",
"https://contrib.rocks/image?repo=chinosk6/GakumasTranslationData"
)
)
@Serializable
data class MainContributors(
val name: String,
val links: List<Links>
)
@Serializable
data class ContribImg(
val plugin: String,
val translation: String
)
@Serializable
data class Links(
val name: String,
val link: String
)

View File

@ -1,6 +1,8 @@
package io.github.chinosk.gakumas.localify.models package io.github.chinosk.gakumas.localify.models
import kotlinx.serialization.Serializable
@Serializable
data class GakumasConfig ( data class GakumasConfig (
var dbgMode: Boolean = false, var dbgMode: Boolean = false,
var enabled: Boolean = true, var enabled: Boolean = true,
@ -41,4 +43,6 @@ data class GakumasConfig (
var bLimitYy: Float = 1.0f, var bLimitYy: Float = 1.0f,
var bLimitZx: Float = 1.0f, var bLimitZx: Float = 1.0f,
var bLimitZy: Float = 1.0f, var bLimitZy: Float = 1.0f,
var pf: Boolean = false
) )

View File

@ -0,0 +1,41 @@
package io.github.chinosk.gakumas.localify.models
import io.github.chinosk.gakumas.localify.mainUtils.json
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.encodeStructure
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonObject
@Serializable
data class ProgramConfig(
var checkBuiltInAssets: Boolean = true,
var transRemoteZipUrl: String = "",
var useRemoteAssets: Boolean = false,
var delRemoteAfterUpdate: Boolean = true,
var cleanLocalAssets: Boolean = false,
var p: Boolean = false
)
class ProgramConfigSerializer(
private val excludes: List<String> = emptyList(),
) : KSerializer<ProgramConfig> {
override val descriptor: SerialDescriptor = ProgramConfig.serializer().descriptor
override fun serialize(encoder: Encoder, value: ProgramConfig) {
val jsonObject = json.encodeToJsonElement(value).jsonObject
encoder.encodeStructure(descriptor) {
jsonObject.keys.forEachIndexed { index, k ->
if (k in excludes) return@forEachIndexed
encodeSerializableElement(descriptor, index, JsonElement.serializer(), jsonObject[k]!!)
}
}
}
override fun deserialize(decoder: Decoder): ProgramConfig {
return ProgramConfig.serializer().deserialize(decoder)
}
}

View File

@ -0,0 +1,71 @@
package io.github.chinosk.gakumas.localify.models
import androidx.lifecycle.ViewModel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModelProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
open class CollapsibleBoxViewModel(initiallyBreastExpanded: Boolean = false) : ViewModel() {
open var expanded by mutableStateOf(initiallyBreastExpanded)
}
class BreastCollapsibleBoxViewModel(initiallyBreastExpanded: Boolean = false) : CollapsibleBoxViewModel(initiallyBreastExpanded) {
override var expanded by mutableStateOf(initiallyBreastExpanded)
}
class ResourceCollapsibleBoxViewModel(initiallyBreastExpanded: Boolean = false) : CollapsibleBoxViewModel(initiallyBreastExpanded) {
override var expanded by mutableStateOf(initiallyBreastExpanded)
}
class BreastCollapsibleBoxViewModelFactory(private val initiallyExpanded: Boolean) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(BreastCollapsibleBoxViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return BreastCollapsibleBoxViewModel(initiallyExpanded) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
class ResourceCollapsibleBoxViewModelFactory(private val initiallyExpanded: Boolean) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ResourceCollapsibleBoxViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return ResourceCollapsibleBoxViewModel(initiallyExpanded) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
class ProgramConfigViewModelFactory(private val initialValue: ProgramConfig,
private val localResourceVersion: String) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ProgramConfigViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return ProgramConfigViewModel(initialValue, localResourceVersion) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion: String) : ViewModel() {
val configState = MutableStateFlow(initValue)
val config: StateFlow<ProgramConfig> = configState.asStateFlow()
val downloadProgressState = MutableStateFlow(-1f)
val downloadProgress: StateFlow<Float> = downloadProgressState.asStateFlow()
val downloadAbleState = MutableStateFlow(true)
val downloadAble: StateFlow<Boolean> = downloadAbleState.asStateFlow()
val localResourceVersionState = MutableStateFlow(initLocalResourceVersion)
val localResourceVersion: StateFlow<String> = localResourceVersionState.asStateFlow()
val errorStringState = MutableStateFlow("")
val errorString: StateFlow<String> = errorStringState.asStateFlow()
}

View File

@ -0,0 +1,77 @@
package io.github.chinosk.gakumas.localify.ui.components
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
@Composable
fun GakuButton(
onClick: () -> Unit,
text: String,
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(50.dp), // 用于实现左右两边的半圆角
shadowElevation: Dp = 8.dp, // 阴影的高度
borderWidth: Dp = 1.dp, // 描边的宽度
borderColor: Color = Color.Transparent, // 描边的颜色
enabled: Boolean = true
) {
var buttonSize by remember { mutableStateOf(IntSize.Zero) }
val gradient = remember(buttonSize) {
Brush.linearGradient(
colors = if (enabled) listOf(Color(0xFFFF5F19), Color(0xFFFFA028)) else
listOf(Color(0xFFF9F9F9), Color(0xFFF0F0F0)),
start = Offset(0f, 0f),
end = Offset(buttonSize.width.toFloat(), buttonSize.height.toFloat()) // 动态终点
)
}
Button(
onClick = onClick,
enabled = enabled,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
modifier = modifier
.onGloballyPositioned { layoutCoordinates ->
buttonSize = layoutCoordinates.size
}
.shadow(elevation = shadowElevation, shape = shape)
.clip(shape)
.background(gradient)
.border(borderWidth, borderColor, shape),
contentPadding = PaddingValues(0.dp)
) {
Text(text = text, color = if (enabled) Color.White else Color(0xFF111111))
}
}
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun GakuButtonPreview() {
GakuButton(modifier = Modifier.width(80.dp).height(40.dp), text = "Button", onClick = {},
enabled = true)
}

View File

@ -0,0 +1,110 @@
package io.github.chinosk.gakumas.localify.ui.components
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.github.chinosk.gakumas.localify.R
@Composable
fun GakuGroupBox(
modifier: Modifier = Modifier,
title: String = "Title",
maxWidth: Dp = 500.dp,
contentPadding: Dp = 8.dp,
rightHead: @Composable (() -> Unit)? = null,
onHeadClick: () -> Unit = {},
content: @Composable () -> Unit
) {
Box(
modifier = Modifier
.shadow(4.dp, RoundedCornerShape(
bottomStart = 16.dp,
bottomEnd = 8.dp,
topEnd = 16.dp,
topStart = 0.dp
))
// .background(Color.White, RoundedCornerShape(8.dp))
) {
Column(modifier = modifier.widthIn(max = maxWidth)) {
// Header
Box(
modifier = modifier
.fillMaxWidth()
.background(Color.Transparent)
.height(23.dp)
.clickable {
onHeadClick()
}
) {
Image(
painter = painterResource(id = R.drawable.bg_h1),
contentDescription = null,
// modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.FillBounds
)
Text(
text = title,
style = MaterialTheme.typography.titleSmall,
color = Color.White,
modifier = modifier
.align(Alignment.CenterStart)
.padding(start = (maxWidth.value * 0.043f).dp)
)
if (rightHead != null) {
Box(modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = (maxWidth.value * 0.1f).dp)) {
rightHead()
}
}
}
// Content
Box(
modifier = modifier
.background(
color = Color.White,
shape = RoundedCornerShape(
bottomStart = 16.dp,
bottomEnd = 8.dp
)
)
.padding(contentPadding)
.fillMaxWidth()
) {
content()
}
}
}
}
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun PreviewGakuGroupBox() {
GakuGroupBox(
title = "GroupBox Title"
) {
Column {
Text("This is the content of the GroupBox.")
Text("This is the content of the GroupBox.")
}
}
}

View File

@ -0,0 +1,55 @@
package io.github.chinosk.gakumas.localify.ui.components
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun GakuProgressBar(modifier: Modifier = Modifier, progress: Float, isError: Boolean = false) {
val animatedProgress by animateFloatAsState(targetValue = progress, label = "progressAnime")
Row(
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
modifier = modifier
) {
if (progress <= 0f) {
LinearProgressIndicator(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(4.dp))
.height(8.dp),
color = if (isError) Color(0xFFE2041B) else Color(0xFFF9C114),
)
}
else {
LinearProgressIndicator(
progress = { animatedProgress },
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(4.dp))
.height(8.dp),
color = if (isError) Color(0xFFE2041B) else Color(0xFFF9C114),
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(if (progress > 0f) "${(progress * 100).toInt()}%" else if (isError) "Failed" else "Downloading")
}
}
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun GakuProgressBarPreview() {
GakuProgressBar(progress = 0.25f)
}

View File

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

View File

@ -0,0 +1,50 @@
package io.github.chinosk.gakumas.localify.ui.components
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.Alignment
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import io.github.chinosk.gakumas.localify.ui.components.base.AutoSizeText
@Composable
fun GakuSwitch(modifier: Modifier = Modifier,
text: String = "",
checked: Boolean = false,
leftPart: @Composable (() -> Unit)? = null,
onCheckedChange: (Boolean) -> Unit = {}) {
Row(modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
if (text.isNotEmpty()) {
AutoSizeText(text = text, fontSize = 16.sp)
}
leftPart?.invoke()
Switch(checked = checked,
onCheckedChange = { value -> onCheckedChange(value) },
modifier = Modifier,
colors = SwitchDefaults.colors(
checkedThumbColor = Color(0xFFFFFFFF),
checkedTrackColor = Color(0xFFF9C114),
uncheckedThumbColor = Color(0xFFFFFFFF),
uncheckedTrackColor = Color(0xFFCFD8DC),
uncheckedBorderColor = Color(0xFFCFD8DC),
))
}
}
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun GakuSwitchPreview() {
GakuSwitch(text = "Switch", checked = true)
}

View File

@ -0,0 +1,102 @@
package io.github.chinosk.gakumas.localify.ui.components
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import androidx.compose.material3.Surface
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.runtime.*
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GakuTabRow(
modifier: Modifier = Modifier,
pagerState: PagerState,
tabs: List<String>,
onTabSelected: (index: Int) -> Unit
) {
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(pagerState.currentPage) {
onTabSelected(pagerState.currentPage)
}
Box(
modifier = Modifier
.shadow(4.dp, RoundedCornerShape(16.dp))
) {
Surface(
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.shadow(4.dp),
shape = RoundedCornerShape(16.dp),
) {
Column {
TabRow(
modifier = modifier.background(Color.Transparent),
containerColor = Color.Transparent,
selectedTabIndex = pagerState.currentPage,
indicator = @Composable { tabPositions ->
Box(
Modifier
.tabIndicatorOffset(tabPositions[pagerState.currentPage])
.height(4.dp)
.background(Color(0xFFFFA500))
.padding(horizontal = 4.dp)
)
}
) {
tabs.forEachIndexed { index, title ->
Tab(
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.scrollToPage(index)
// pagerState.animateScrollToPage(
// page = index,
// animationSpec = tween(durationMillis = 250)
// )
}
},
text = {
Text(
text = title,
color = if (pagerState.currentPage == index) Color(0xFFFFA500) else Color.Black
)
}
)
}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun GakuTabRowPreview(modifier: Modifier = Modifier) {
val pagerState = rememberPagerState(initialPage = 1, pageCount = { 3 })
GakuTabRow(modifier, pagerState, listOf("TAB 1", "TAB 2", "TAB 3")) { _ -> }
}

View File

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

View File

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

View File

@ -0,0 +1,99 @@
package io.github.chinosk.gakumas.localify.ui.components.base
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.github.chinosk.gakumas.localify.models.CollapsibleBoxViewModel
@Composable
fun CollapsibleBox(
modifier: Modifier = Modifier,
collapsedHeight: Dp = 28.dp,
viewModel: CollapsibleBoxViewModel = viewModel(),
showExpand: Boolean = true,
expandState: Boolean? = null,
innerPaddingTopBottom: Dp = 0.dp,
innerPaddingLeftRight: Dp = 0.dp,
content: @Composable () -> Unit
) {
val expanded by viewModel::expanded
// var offsetY by remember { mutableFloatStateOf(0f) }
val animatedHeight by animateDpAsState(
targetValue = if (expandState ?: expanded) Dp.Unspecified else collapsedHeight,
label = "CollapsibleBox$collapsedHeight"
)
Box(
modifier = modifier
.animateContentSize()/*
.pointerInput(Unit) {
detectVerticalDragGestures(
onVerticalDrag = { change, dragAmount ->
change.consume()
offsetY += dragAmount
if (expanded && offsetY > 0) {
viewModel.expanded = false
} else if (!expanded && offsetY < 0) {
viewModel.expanded = true
}
},
onDragEnd = {
offsetY = 0f
}
)
}*/
.background(MaterialTheme.colorScheme.background)
) {
Column(
modifier = Modifier
.height(animatedHeight)
.fillMaxWidth()
.padding(start = innerPaddingLeftRight, end = innerPaddingLeftRight,
top = innerPaddingTopBottom, bottom = innerPaddingTopBottom)
// .fillMaxSize()
.clickable {
if (!expanded && showExpand) {
viewModel.expanded = expandState ?: true
}
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
//item {
if (expandState ?: expanded) {
content()
}
else if (showExpand) {
Text(text = "Details ↓", color = Color.Gray)
}
//}
}
}
}
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun CollapsibleBoxPreview() {
CollapsibleBox(showExpand = true) {}
}

View File

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

View File

@ -0,0 +1,90 @@
package io.github.chinosk.gakumas.localify.ui.pages
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.github.chinosk.gakumas.localify.MainActivity
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.onClickStartGame
import io.github.chinosk.gakumas.localify.ui.components.GakuTabRow
import io.github.chinosk.gakumas.localify.ui.pages.subPages.AboutPage
import io.github.chinosk.gakumas.localify.ui.pages.subPages.AdvanceSettingsPage
import io.github.chinosk.gakumas.localify.ui.pages.subPages.HomePage
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SettingsTabs(modifier: Modifier = Modifier,
titles: List<String>,
context: MainActivity? = null,
previewData: GakumasConfig? = null,
screenH: Dp = 1080.dp
) {
val pagerState = rememberPagerState(initialPage = 1, pageCount = { titles.size })
Box {
HorizontalPager(
state = pagerState,
modifier = modifier.fillMaxSize(),
pageSpacing = 10.dp
) { page ->
Column(modifier = modifier
.padding(5.dp)
.fillMaxHeight(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally) {
when (page) {
0 -> AboutPage(modifier, context = context, previewData = previewData, screenH = screenH)
1 -> HomePage(modifier, context = context, previewData = previewData, screenH = screenH)
2 -> AdvanceSettingsPage(modifier, context = context, previewData = previewData, screenH = screenH)
}
}
}
Box(
Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 6.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
FloatingActionButton(
onClick = { context?.onClickStartGame() },
modifier = Modifier.align(Alignment.End),
containerColor = MaterialTheme.colorScheme.primary,
shape = CircleShape
) {
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "StartGame")
}
GakuTabRow(modifier, pagerState, titles) { }
}
}
}
}
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO, heightDp = 760)
@Composable
fun SettingTabsPreview(modifier: Modifier = Modifier) {
SettingsTabs(titles = listOf("TAB 1", "TAB 2", "TAB 3"), previewData = GakumasConfig())
}

View File

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

View File

@ -0,0 +1,214 @@
package io.github.chinosk.gakumas.localify.ui.pages.subPages
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
import coil.decode.SvgDecoder
import coil.request.ImageRequest
import coil.size.Size
import io.github.chinosk.gakumas.localify.MainActivity
import io.github.chinosk.gakumas.localify.R
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.convertToString
import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.AboutPageConfig
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.ui.components.GakuButton
@Composable
fun AboutPage(modifier: Modifier = Modifier,
context: MainActivity? = null,
previewData: GakumasConfig? = null,
bottomSpacerHeight: Dp = 120.dp,
screenH: Dp = 1080.dp) {
// val config = getConfigState(context, previewData)
val contributorInfo = remember {
val dataJsonString = context?.getString(R.string.about_contributors_asset_file)?.let {
convertToString(context.assets?.open(it))
}
dataJsonString?.let { json.decodeFromString<AboutPageConfig>(it) }
?: AboutPageConfig()
}
LazyColumn(modifier = modifier
.sizeIn(maxHeight = screenH)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
}
item {
Text(stringResource(R.string.about_warn_title), fontSize = 24.sp, color = MaterialTheme.colorScheme.error)
Text(stringResource(R.string.about_warn_p1))
Text(stringResource(R.string.about_warn_p2))
}
item {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
}
item {
Text(stringResource(R.string.about_about_title), fontSize = 24.sp, color = MaterialTheme.colorScheme.onPrimaryContainer)
Text(stringResource(R.string.about_about_p1))
Text(stringResource(R.string.about_about_p2))
Row(modifier = Modifier
.fillMaxWidth()
.padding(
start = 8.dp, end = 8.dp, top = 8.dp, bottom = 0.dp
)) {
GakuButton(text = "Github", modifier = modifier
.weight(1f)
.sizeIn(maxWidth = 600.dp), onClick = {
context?.openUrl(contributorInfo.plugin_repo)
})
}
}
item {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
}
item {
LazyColumn(modifier = modifier
.sizeIn(maxHeight = screenH)
.fillMaxWidth()) {
item {
Text(stringResource(R.string.project_contribution), fontSize = 24.sp, color = MaterialTheme.colorScheme.onPrimaryContainer)
}
for (contributor in contributorInfo.main_contributors) {
item {
Row(modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 8.dp, 8.dp, 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically) {
Text(contributor.name, fontSize = 16.sp)
for (link in contributor.links) {
GakuButton(text = link.name, modifier = modifier.height(40.dp),
onClick = {
context?.openUrl(link.link)
})
}
}
}
}
}
}
item {
Text(stringResource(R.string.contributors), fontSize = 24.sp, color = MaterialTheme.colorScheme.onPrimaryContainer)
Text(stringResource(R.string.plugin_code), fontSize = 16.sp)
NetworkSvgImage(
url = contributorInfo.contrib_img.plugin,
contentDescription = "plugin-contrib"
)
Spacer(modifier = Modifier.height(4.dp))
Text(stringResource(R.string.translation_repository), fontSize = 16.sp)
NetworkSvgImage(
url = contributorInfo.contrib_img.translation,
contentDescription = "translation-contrib"
)
}
item {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
}
item {
Spacer(modifier = modifier.height(bottomSpacerHeight))
}
}
}
@Composable
fun NetworkImage(
url: String,
contentDescription: String?,
modifier: Modifier = Modifier
) {
val painter = rememberAsyncImagePainter(model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.size(Size.ORIGINAL)
.build())
Image(
painter = painter,
contentDescription = contentDescription,
modifier = modifier
)
}
@Composable
fun NetworkSvgImage(
url: String,
contentDescription: String?,
modifier: Modifier = Modifier
) {
val imageLoader = ImageLoader.Builder(LocalContext.current)
.components {
add(SvgDecoder.Factory())
}
.build()
val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.size(Size.ORIGINAL)
.build(),
imageLoader = imageLoader
)
Image(
painter = painter,
contentDescription = contentDescription,
modifier = modifier
)
}
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun AboutPagePreview(modifier: Modifier = Modifier, data: GakumasConfig = GakumasConfig()) {
AboutPage(modifier, previewData = data)
}

View File

@ -0,0 +1,397 @@
package io.github.chinosk.gakumas.localify.ui.pages.subPages
import io.github.chinosk.gakumas.localify.ui.components.GakuGroupBox
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.github.chinosk.gakumas.localify.MainActivity
import io.github.chinosk.gakumas.localify.R
import io.github.chinosk.gakumas.localify.getConfigState
import io.github.chinosk.gakumas.localify.models.BreastCollapsibleBoxViewModel
import io.github.chinosk.gakumas.localify.models.BreastCollapsibleBoxViewModelFactory
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.ui.components.base.CollapsibleBox
import io.github.chinosk.gakumas.localify.ui.components.GakuButton
import io.github.chinosk.gakumas.localify.ui.components.GakuSwitch
import io.github.chinosk.gakumas.localify.ui.components.GakuTextInput
@Composable
fun AdvanceSettingsPage(modifier: Modifier = Modifier,
context: MainActivity? = null,
previewData: GakumasConfig? = null,
bottomSpacerHeight: Dp = 120.dp,
screenH: Dp = 1080.dp) {
val config = getConfigState(context, previewData)
// val scrollState = rememberScrollState()
val breastParamViewModel: BreastCollapsibleBoxViewModel =
viewModel(factory = BreastCollapsibleBoxViewModelFactory(initiallyExpanded = false))
val keyBoardOptionsDecimal = remember {
KeyboardOptions(keyboardType = KeyboardType.Decimal)
}
LazyColumn(modifier = modifier
.sizeIn(maxHeight = screenH)
// .fillMaxHeight()
// .verticalScroll(scrollState)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
GakuGroupBox(modifier, stringResource(R.string.camera_settings)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
GakuSwitch(modifier, stringResource(R.string.enable_free_camera), checked = config.value.enableFreeCamera) {
v -> context?.onEnableFreeCameraChanged(v)
}
}
}
Spacer(Modifier.height(6.dp))
}
item {
GakuGroupBox(modifier, stringResource(R.string.debug_settings)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
GakuSwitch(modifier, stringResource(R.string.text_hook_test_mode), checked = config.value.textTest) {
v -> context?.onTextTestChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.export_text), checked = config.value.dumpText) {
v -> context?.onDumpTextChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.force_export_resource), checked = config.value.forceExportResource) {
v -> context?.onForceExportResourceChanged(v)
}
}
}
Spacer(Modifier.height(6.dp))
}
item {
GakuGroupBox(modifier, stringResource(R.string.breast_param),
contentPadding = 0.dp,
onHeadClick = {
breastParamViewModel.expanded = !breastParamViewModel.expanded
}) {
CollapsibleBox(modifier = modifier,
viewModel = breastParamViewModel
) {
LazyColumn(modifier = modifier
.padding(8.dp)
.sizeIn(maxHeight = screenH),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
GakuSwitch(modifier = modifier,
checked = config.value.enableBreastParam,
text = stringResource(R.string.enable_breast_param)
) { v -> context?.onEnableBreastParamChanged(v) }
}
item {
Row(modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(2.dp)) {
val buttonModifier = remember {
modifier
.height(40.dp)
.weight(1f)
}
GakuButton(modifier = buttonModifier,
text = "??", onClick = { context?.onBClickPresetChanged(5) })
GakuButton(modifier = buttonModifier,
text = "+5", onClick = { context?.onBClickPresetChanged(4) })
GakuButton(modifier = buttonModifier,
text = "+4", onClick = { context?.onBClickPresetChanged(3) })
GakuButton(modifier = buttonModifier,
text = "+3", onClick = { context?.onBClickPresetChanged(2) })
GakuButton(modifier = buttonModifier,
text = "+2", onClick = { context?.onBClickPresetChanged(1) })
GakuButton(modifier = buttonModifier,
text = "+1", onClick = { context?.onBClickPresetChanged(0) })
}
}
item {
Row(modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
GakuTextInput(modifier = modifier
.height(45.dp)
.weight(1f),
fontSize = 14f,
value = config.value.bDamping.toString(),
onValueChange = { c -> context?.onBDampingChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.damping)) },
keyboardOptions = keyBoardOptionsDecimal
)
GakuTextInput(modifier = modifier
.height(45.dp)
.weight(1f),
fontSize = 14f,
value = config.value.bStiffness.toString(),
onValueChange = { c -> context?.onBStiffnessChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.stiffness)) },
keyboardOptions = keyBoardOptionsDecimal)
}
}
item {
Row(modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
GakuTextInput(modifier = modifier
.height(45.dp)
.weight(1f),
fontSize = 14f,
value = config.value.bSpring.toString(),
onValueChange = { c -> context?.onBSpringChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.spring)) },
keyboardOptions = keyBoardOptionsDecimal
)
GakuTextInput(modifier = modifier
.height(45.dp)
.weight(1f),
fontSize = 14f,
value = config.value.bPendulum.toString(),
onValueChange = { c -> context?.onBPendulumChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.pendulum)) },
keyboardOptions = keyBoardOptionsDecimal)
}
}
item {
Row(modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
GakuTextInput(modifier = modifier
.height(45.dp)
.weight(1f),
fontSize = 14f,
value = config.value.bPendulumRange.toString(),
onValueChange = { c -> context?.onBPendulumRangeChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.pendulumrange)) },
keyboardOptions = keyBoardOptionsDecimal
)
GakuTextInput(modifier = modifier
.height(45.dp)
.weight(1f),
fontSize = 14f,
value = config.value.bAverage.toString(),
onValueChange = { c -> context?.onBAverageChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.average)) },
keyboardOptions = keyBoardOptionsDecimal)
}
}
item {
GakuTextInput(modifier = modifier
.height(45.dp)
.fillMaxWidth(),
fontSize = 14f,
value = config.value.bRootWeight.toString(),
onValueChange = { c -> context?.onBRootWeightChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.rootweight)) },
keyboardOptions = keyBoardOptionsDecimal
)
}
item {
GakuSwitch(modifier = modifier,
checked = config.value.bUseScale,
leftPart = {
GakuTextInput(modifier = modifier
.height(45.dp),
fontSize = 14f,
value = config.value.bScale.toString(),
onValueChange = { c -> context?.onBScaleChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.breast_scale)) },
keyboardOptions = keyBoardOptionsDecimal
)
}
) { v -> context?.onBUseScaleChanged(v) }
}
item {
GakuSwitch(modifier = modifier,
checked = config.value.bUseArmCorrection,
text = stringResource(R.string.usearmcorrection)
) { v -> context?.onBUseArmCorrectionChanged(v) }
}
item {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
}
item {
GakuSwitch(modifier = modifier,
checked = config.value.bUseLimit,
text = stringResource(R.string.uselimit_0_1)
) { v ->
context?.onBUseLimitChanged(v)
}
}
item {
CollapsibleBox(modifier = modifier,
expandState = config.value.bUseLimit,
collapsedHeight = 0.dp,
showExpand = false
){
Row(modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
val textInputModifier = remember {
modifier
.height(45.dp)
.weight(1f)
}
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.bLimitXx.toString(),
onValueChange = { c -> context?.onBLimitXxChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.axisx_x)) },
keyboardOptions = keyBoardOptionsDecimal
)
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.bLimitYx.toString(),
onValueChange = { c -> context?.onBLimitYxChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.axisy_x)) },
keyboardOptions = keyBoardOptionsDecimal
)
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.bLimitZx.toString(),
onValueChange = { c -> context?.onBLimitZxChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.axisz_x)) },
keyboardOptions = keyBoardOptionsDecimal
)
}
Row(modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
val textInputModifier = remember {
modifier
.height(45.dp)
.weight(1f)
}
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.bLimitXy.toString(),
onValueChange = { c -> context?.onBLimitXyChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.axisx_y)) },
keyboardOptions = keyBoardOptionsDecimal
)
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.bLimitYy.toString(),
onValueChange = { c -> context?.onBLimitYyChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.axisy_y)) },
keyboardOptions = keyBoardOptionsDecimal
)
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.bLimitZy.toString(),
onValueChange = { c -> context?.onBLimitZyChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.axisz_y)) },
keyboardOptions = keyBoardOptionsDecimal
)
}
}
}
}
}
}
}
item {
if (config.value.dbgMode) {
Spacer(Modifier.height(6.dp))
GakuGroupBox(modifier, stringResource(R.string.test_mode_live)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
GakuSwitch(modifier, stringResource(R.string.unlockAllLive),
checked = config.value.unlockAllLive) {
v -> context?.onUnlockAllLiveChanged(v)
}
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
GakuSwitch(modifier, stringResource(R.string.liveUseCustomeDress),
checked = config.value.enableLiveCustomeDress) {
v -> context?.onLiveCustomeDressChanged(v)
}
GakuTextInput(modifier = modifier
.height(45.dp)
.fillMaxWidth(),
fontSize = 14f,
value = config.value.liveCustomeHeadId,
onValueChange = { c -> context?.onLiveCustomeHeadIdChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.live_costume_head_id),
fontSize = 12.sp) }
)
GakuTextInput(modifier = modifier
.height(45.dp)
.fillMaxWidth(),
fontSize = 14f,
value = config.value.liveCustomeCostumeId,
onValueChange = { c -> context?.onLiveCustomeCostumeIdChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.live_custome_dress_id)) }
)
}
}
}
}
item {
Spacer(modifier = modifier.height(bottomSpacerHeight))
}
}
}
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun AdvanceSettingsPagePreview(modifier: Modifier = Modifier, data: GakumasConfig = GakumasConfig()) {
AdvanceSettingsPage(modifier, previewData = data)
}

View File

@ -0,0 +1,476 @@
package io.github.chinosk.gakumas.localify.ui.pages.subPages
import io.github.chinosk.gakumas.localify.ui.components.GakuGroupBox
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.github.chinosk.gakumas.localify.MainActivity
import io.github.chinosk.gakumas.localify.R
import io.github.chinosk.gakumas.localify.getConfigState
import io.github.chinosk.gakumas.localify.getProgramConfigState
import io.github.chinosk.gakumas.localify.getProgramDownloadAbleState
import io.github.chinosk.gakumas.localify.getProgramDownloadErrorStringState
import io.github.chinosk.gakumas.localify.getProgramDownloadState
import io.github.chinosk.gakumas.localify.getProgramLocalResourceVersionState
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.mainUtils.FileDownloader
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.ResourceCollapsibleBoxViewModel
import io.github.chinosk.gakumas.localify.models.ResourceCollapsibleBoxViewModelFactory
import io.github.chinosk.gakumas.localify.ui.components.base.CollapsibleBox
import io.github.chinosk.gakumas.localify.ui.components.GakuButton
import io.github.chinosk.gakumas.localify.ui.components.GakuProgressBar
import io.github.chinosk.gakumas.localify.ui.components.GakuRadio
import io.github.chinosk.gakumas.localify.ui.components.GakuSwitch
import io.github.chinosk.gakumas.localify.ui.components.GakuTextInput
import java.io.File
@Composable
fun HomePage(modifier: Modifier = Modifier,
context: MainActivity? = null,
previewData: GakumasConfig? = null,
bottomSpacerHeight: Dp = 120.dp,
screenH: Dp = 1080.dp) {
val config = getConfigState(context, previewData)
val programConfig = getProgramConfigState(context)
val downloadProgress by getProgramDownloadState(context)
val downloadAble by getProgramDownloadAbleState(context)
val localResourceVersion by getProgramLocalResourceVersionState(context)
val downloadErrorString by getProgramDownloadErrorStringState(context)
// val scrollState = rememberScrollState()
val keyboardOptionsNumber = remember {
KeyboardOptions(keyboardType = KeyboardType.Number)
}
val keyBoardOptionsDecimal = remember {
KeyboardOptions(keyboardType = KeyboardType.Decimal)
}
val resourceSettingsViewModel: ResourceCollapsibleBoxViewModel =
viewModel(factory = ResourceCollapsibleBoxViewModelFactory(initiallyExpanded = false))
fun onClickDownload() {
context?.mainPageAssetsViewDataUpdate(
downloadAbleState = false,
errorString = "",
downloadProgressState = -1f
)
val (_, newUrl) = FileDownloader.checkAndChangeDownloadURL(programConfig.value.transRemoteZipUrl)
context?.onPTransRemoteZipUrlChanged(newUrl, 0, 0, 0)
FileDownloader.downloadFile(
newUrl,
checkContentTypes = listOf("application/zip", "application/octet-stream"),
onDownload = { progress, _, _ ->
context?.mainPageAssetsViewDataUpdate(downloadProgressState = progress)
},
onSuccess = { byteArray ->
context?.mainPageAssetsViewDataUpdate(
downloadAbleState = true,
errorString = "",
downloadProgressState = -1f
)
val file = File(context?.filesDir, "update_trans.zip")
file.writeBytes(byteArray)
val newFileVersion = FileHotUpdater.getZipResourceVersion(file.absolutePath)
if (newFileVersion != null) {
context?.mainPageAssetsViewDataUpdate(
localResourceVersionState = newFileVersion
)
}
else {
context?.mainPageAssetsViewDataUpdate(
localResourceVersionState = context.getString(
R.string.invalid_zip_file
),
errorString = context.getString(R.string.invalid_zip_file_warn)
)
}
},
onFailed = { code, reason ->
context?.mainPageAssetsViewDataUpdate(
downloadAbleState = true,
errorString = reason,
)
})
}
LazyColumn(modifier = modifier
.sizeIn(maxHeight = screenH)
// .fillMaxHeight()
// .verticalScroll(scrollState)
// .width(IntrinsicSize.Max)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
GakuGroupBox(modifier = modifier, stringResource(R.string.basic_settings)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
GakuSwitch(modifier, stringResource(R.string.enable_plugin), checked = config.value.enabled) {
v -> context?.onEnabledChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.replace_font), checked = config.value.replaceFont) {
v -> context?.onReplaceFontChanged(v)
}
}
}
Spacer(Modifier.height(6.dp))
}
item {
GakuGroupBox(modifier, stringResource(R.string.resource_settings),
contentPadding = 0.dp,
onHeadClick = {
resourceSettingsViewModel.expanded = !resourceSettingsViewModel.expanded
}) {
CollapsibleBox(modifier = modifier,
viewModel = resourceSettingsViewModel
) {
LazyColumn(modifier = modifier
// .padding(8.dp)
.sizeIn(maxHeight = screenH),
// verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
GakuSwitch(modifier = modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp),
checked = programConfig.value.checkBuiltInAssets,
text = stringResource(id = R.string.check_built_in_resource)
) { v -> context?.onPCheckBuiltInAssetsChanged(v) }
}
item {
GakuSwitch(modifier = modifier.padding(start = 8.dp, end = 8.dp),
checked = programConfig.value.cleanLocalAssets,
text = stringResource(id = R.string.delete_plugin_resource)
) { v -> context?.onPCleanLocalAssetsChanged(v) }
}
item {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
}
item {
GakuSwitch(modifier = modifier.padding(start = 8.dp, end = 8.dp),
checked = programConfig.value.useRemoteAssets,
text = stringResource(id = R.string.use_remote_zip_resource)
) { v -> context?.onPUseRemoteAssetsChanged(v) }
CollapsibleBox(modifier = modifier.graphicsLayer(clip = false),
expandState = programConfig.value.useRemoteAssets,
collapsedHeight = 0.dp,
innerPaddingLeftRight = 8.dp,
showExpand = false
) {
GakuSwitch(modifier = modifier,
checked = programConfig.value.delRemoteAfterUpdate,
text = stringResource(id = R.string.del_remote_after_update)
) { v -> context?.onPDelRemoteAfterUpdateChanged(v) }
LazyColumn(modifier = modifier
// .padding(8.dp)
.sizeIn(maxHeight = screenH),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Row(modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically) {
GakuTextInput(modifier = modifier
.height(45.dp)
.padding(end = 8.dp)
.fillMaxWidth()
.weight(1f),
fontSize = 14f,
value = programConfig.value.transRemoteZipUrl,
onValueChange = { c -> context?.onPTransRemoteZipUrlChanged(c, 0, 0, 0)},
label = { Text(stringResource(id = R.string.resource_url)) },
keyboardOptions = keyboardOptionsNumber)
if (downloadAble) {
GakuButton(modifier = modifier
.height(40.dp)
.sizeIn(minWidth = 80.dp),
text = stringResource(id = R.string.download),
onClick = { onClickDownload() })
}
else {
GakuButton(modifier = modifier
.height(40.dp)
.sizeIn(minWidth = 80.dp),
text = stringResource(id = R.string.cancel), onClick = {
FileDownloader.cancel()
})
}
}
}
if (downloadProgress >= 0) {
item {
GakuProgressBar(progress = downloadProgress, isError = downloadErrorString.isNotEmpty())
}
}
if (downloadErrorString.isNotEmpty()) {
item {
Text(text = downloadErrorString, color = Color(0xFFE2041B))
}
}
item {
Text(modifier = Modifier
.fillMaxWidth()
.clickable {
val file =
File(context?.filesDir, "update_trans.zip")
context?.mainPageAssetsViewDataUpdate(
localResourceVersionState = FileHotUpdater
.getZipResourceVersion(file.absolutePath)
.toString()
)
}, text = "${stringResource(R.string.downloaded_resource_version)}: $localResourceVersion")
}
item {
Spacer(Modifier.height(0.dp))
}
}
}
}
}
}
}
Spacer(Modifier.height(6.dp))
}
item {
GakuGroupBox(modifier = modifier, contentPadding = 0.dp, title = stringResource(R.string.graphic_settings)) {
LazyColumn(modifier = Modifier
.sizeIn(maxHeight = screenH),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Spacer(modifier = Modifier.height(8.dp))
GakuTextInput(modifier = modifier
.padding(start = 4.dp, end = 4.dp)
.height(45.dp)
.fillMaxWidth(),
fontSize = 14f,
value = config.value.targetFrameRate.toString(),
onValueChange = { c -> context?.onTargetFpsChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.setFpsTitle)) },
keyboardOptions = keyboardOptionsNumber)
}
item {
Column(modifier = Modifier.padding(start = 8.dp, end = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(stringResource(R.string.orientation_lock))
Row(modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)) {
val radioModifier = remember {
modifier
.height(40.dp)
.weight(1f)
}
GakuRadio(modifier = radioModifier,
text = stringResource(R.string.orientation_orig), selected = config.value.gameOrientation == 0,
onClick = { context?.onGameOrientationChanged(0) })
GakuRadio(modifier = radioModifier,
text = stringResource(R.string.orientation_portrait), selected = config.value.gameOrientation == 1,
onClick = { context?.onGameOrientationChanged(1) })
GakuRadio(modifier = radioModifier,
text = stringResource(R.string.orientation_landscape), selected = config.value.gameOrientation == 2,
onClick = { context?.onGameOrientationChanged(2) })
}
}
}
item {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
}
item {
GakuSwitch(modifier.padding(start = 8.dp, end = 8.dp),
stringResource(R.string.useCustomeGraphicSettings),
checked = config.value.useCustomeGraphicSettings) {
v -> context?.onUseCustomeGraphicSettingsChanged(v)
}
CollapsibleBox(modifier = modifier,
expandState = config.value.useCustomeGraphicSettings,
collapsedHeight = 0.dp,
showExpand = false
) {
LazyColumn(modifier = modifier
.padding(8.dp)
.sizeIn(maxHeight = screenH)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Row(modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
val buttonModifier = remember {
modifier
.height(40.dp)
.weight(1f)
}
GakuButton(modifier = buttonModifier,
text = stringResource(R.string.max_high), onClick = { context?.onChangePresetQuality(4) })
GakuButton(modifier = buttonModifier,
text = stringResource(R.string.very_high), onClick = { context?.onChangePresetQuality(3) })
GakuButton(modifier = buttonModifier,
text = stringResource(R.string.hign), onClick = { context?.onChangePresetQuality(2) })
GakuButton(modifier = buttonModifier,
text = stringResource(R.string.middle), onClick = { context?.onChangePresetQuality(1) })
GakuButton(modifier = buttonModifier,
text = stringResource(R.string.low), onClick = { context?.onChangePresetQuality(0) })
}
}
item {
Row(modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
val textInputModifier = remember {
modifier
.height(45.dp)
.weight(1f)
}
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.renderScale.toString(),
onValueChange = { c -> context?.onRenderScaleChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.renderscale)) },
keyboardOptions = keyBoardOptionsDecimal)
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.qualitySettingsLevel.toString(),
onValueChange = { c -> context?.onQualitySettingsLevelChanged(c, 0, 0, 0)},
label = { Text("QualityLevel (1/1/2/3/5)") },
keyboardOptions = keyboardOptionsNumber)
}
}
item {
Row(modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
val textInputModifier = remember {
modifier
.height(45.dp)
.weight(1f)
}
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.volumeIndex.toString(),
onValueChange = { c -> context?.onVolumeIndexChanged(c, 0, 0, 0)},
label = { Text("VolumeIndex (0/1/2/3/4)") },
keyboardOptions = keyboardOptionsNumber)
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.maxBufferPixel.toString(),
onValueChange = { c -> context?.onMaxBufferPixelChanged(c, 0, 0, 0)},
label = { Text("MaxBufferPixel (1024/1440/2538/3384/8190)", fontSize = 10.sp) },
keyboardOptions = keyboardOptionsNumber)
}
}
item {
Row(modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
val textInputModifier = remember {
modifier
.height(45.dp)
.weight(1f)
}
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.reflectionQualityLevel.toString(),
onValueChange = { c -> context?.onReflectionQualityLevelChanged(c, 0, 0, 0)},
label = { Text( text = "ReflectionLevel (0~5)") },
keyboardOptions = keyboardOptionsNumber)
GakuTextInput(modifier = textInputModifier,
fontSize = 14f,
value = config.value.lodQualityLevel.toString(),
onValueChange = { c -> context?.onLodQualityLevelChanged(c, 0, 0, 0)},
label = { Text("LOD Level (0~5)") },
keyboardOptions = keyboardOptionsNumber)
}
}
}
}
}
}
}
}
item {
Spacer(modifier = modifier.height(bottomSpacerHeight))
}
}
}
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun HomePagePreview(modifier: Modifier = Modifier, data: GakumasConfig = GakumasConfig()) {
HomePage(modifier, previewData = data)
}

View File

@ -0,0 +1,72 @@
package io.github.chinosk.gakumas.localify.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Color(0xFFF89400),
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun GakumasLocalifyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/white" />
</shape>
</item>
<item>
<bitmap android:src="@drawable/splash"
android:gravity="center"
android:tileMode="disabled"/>
</item>
</layer-list>

File diff suppressed because it is too large Load Diff

View File

@ -43,5 +43,36 @@
<string name="axisx_y">axisX.y</string> <string name="axisx_y">axisX.y</string>
<string name="axisy_y">axisY.y</string> <string name="axisy_y">axisY.y</string>
<string name="axisz_y">axisZ.y</string> <string name="axisz_y">axisZ.y</string>
<string name="basic_settings">Basic Ssettings</string>
<string name="graphic_settings">Graphic Settings</string>
<string name="camera_settings">Camera Settings</string>
<string name="test_mode_live">Test Mode - LIVE</string>
<string name="debug_settings">Debug Settings</string>
<string name="breast_param">Breast Parameters</string>
<string name="about">About</string>
<string name="home">Home</string>
<string name="advanced_settings">Advanced</string>
<string name="about_warn_title">WARNING</string>
<string name="about_warn_p1">This plugin is for learning and communication only.</string>
<string name="about_warn_p2">Using external plugin against the relevant TOS so proceed at your own risk.</string>
<string name="about_about_title">About This Plugin</string>
<string name="about_about_p1">This plugin is completely free. If you paid for this plugin, please report the seller.</string>
<string name="about_about_p2">Plugin QQ group: 975854705</string>
<string name="project_contribution">Project Contribution</string>
<string name="plugin_code">Plugin Code</string>
<string name="contributors">Contributors</string>
<string name="translation_repository">Translation Repository</string>
<string name="resource_settings">Resource Settings</string>
<string name="check_built_in_resource">Check Built-in Assets Update</string>
<string name="delete_plugin_resource">Delete Plugin Resource</string>
<string name="use_remote_zip_resource">Use Remote ZIP Resource</string>
<string name="resource_url">Resource URL</string>
<string name="download">Download</string>
<string name="invalid_zip_file">Invalid file</string>
<string name="invalid_zip_file_warn">This file is not a valid ZIP translation resource pack.</string>
<string name="cancel">Cancel</string>
<string name="downloaded_resource_version">Downloaded Version</string>
<string name="del_remote_after_update">Delete Cache File After Update</string>
<string name="about_contributors_asset_file">about_contributors_en.json</string>
</resources> </resources>

View File

@ -1,5 +1,30 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.GakumasLocalify" parent="@style/Theme.Material3.DynamicColors.DayNight.NoActionBar" /> <style name="Theme.GakumasLocalify" parent="@style/Theme.MaterialComponents.Light.NoActionBar" >
<item name="android:windowBackground">@drawable/splash_style</item>
</style>
</resources> <style name="Theme.GakumasLocalify.NoDisplay" parent="@style/Theme.MaterialComponents.Light.NoActionBar" >
<item name="android:windowFrame">@null</item>
<!-- 设置是否可滑动 -->
<item name="android:windowIsFloating">true</item>
<!-- 设置是否透明 -->
<item name="android:windowIsTranslucent">true</item>
<!-- 无标题 -->
<item name="android:windowNoTitle">true</item>
<!-- 背景 -->
<item name="android:background">@null</item>
<!-- 窗口背景 -->
<item name="android:windowBackground">@android:color/transparent</item>
<!-- 是否变暗 -->
<item name="android:backgroundDimEnabled">false</item>
<!-- 点击空白部分activity不消失 -->
<item name="android:windowCloseOnTouchOutside">true</item>
<!-- 无标题 有的手机设置这行代码-->
<item name="windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowNoDisplay">true</item>
</style>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="files" path="." />
</paths>

View File

@ -1,5 +1,4 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '8.4.2' apply false alias(libs.plugins.androidApplication) apply false
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false alias(libs.plugins.kotlinAndroid) apply false
} }

54
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,54 @@
[versions]
accompanistPager = "0.30.0"
activityCompose = "1.9.0"
okhttpBom = "4.12.0"
xposedApi = "82"
appcompat = "1.7.0"
coil = "2.6.0"
composeBom = "2024.06.00"
agp = "8.5.0"
coreKtx = "1.13.1"
kotlin = "2.0.0"
lifecycle = "2.8.2"
material = "1.12.0"
navigationCompose = "2.7.7"
xdl = "2.1.1"
shadowhook = "1.0.9"
serialization="1.7.1"
[libraries]
accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanistPager" }
accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanistPager" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
androidx-animation = { module = "androidx.compose.animation:animation" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
androidx-foundation = { module = "androidx.compose.foundation:foundation" }
androidx-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-material = { module = "androidx.compose.material:material" }
androidx-material3 = { module = "androidx.compose.material3:material3" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-runtime = { module = "androidx.compose.runtime:runtime" }
androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
okhttp = { module = "com.squareup.okhttp3:okhttp" }
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" }
xposed-api = { module = "de.robv.android.xposed:api", version.ref = "xposedApi" }
coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
material = { module = "com.google.android.material:material", version.ref = "material" }
shadowhook = { module = "com.bytedance.android:shadowhook", version.ref = "shadowhook" }
xdl = { module = "io.github.hexhacking:xdl", version.ref = "xdl" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

Binary file not shown.

View File

@ -1,6 +1,7 @@
#Fri May 17 13:19:52 CST 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

294
gradlew vendored Normal file → Executable file
View File

@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/bin/sh
# #
# Copyright 2015 the original author or authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -17,67 +17,99 @@
# #
############################################################################## ##############################################################################
## #
## Gradle start up script for UN*X # Gradle start up script for POSIX generated by Gradle.
## #
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" # This is normally unused
APP_BASE_NAME=`basename "$0"` # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -98,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
MAX_FD="$MAX_FD_LIMIT" # shellcheck disable=SC2039,SC3045
fi MAX_FD=$( ulimit -H -n ) ||
ulimit -n $MAX_FD warn "Could not query maximum file descriptor limit"
if [ $? -ne 0 ] ; then esac
warn "Could not set maximum file descriptor limit: $MAX_FD" case $MAX_FD in #(
fi '' | soft) :;; #(
else *)
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
fi # shellcheck disable=SC2039,SC3045
fi ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=`save "$@"` # * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

35
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

@ -1,10 +1,17 @@
pluginManagement { pluginManagement {
repositories { repositories {
google() google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
} }
} }
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {