diff --git a/app/build.gradle b/app/build.gradle
index 0838e55..5d34550 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -14,7 +14,7 @@ android {
minSdk 29
targetSdk 34
versionCode 2
- versionName "v1.1"
+ versionName "v1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -112,6 +112,10 @@ dependencies {
implementation "io.coil-kt:coil-compose:2.6.0"
implementation "io.coil-kt:coil-svg:2.6.0"
+ implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0"))
+ implementation "com.squareup.okhttp3:okhttp"
+ implementation "com.squareup.okhttp3:logging-interceptor"
+
implementation 'io.github.hexhacking:xdl:2.1.1'
implementation 'com.bytedance.android:shadowhook:1.0.9'
compileOnly 'de.robv.android.xposed:api:82'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4481f0b..dac5051 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/gakumas-local b/app/src/main/assets/gakumas-local
index cdd0ad0..a60a171 160000
--- a/app/src/main/assets/gakumas-local
+++ b/app/src/main/assets/gakumas-local
@@ -1 +1 @@
-Subproject commit cdd0ad064cf6d3f13107e19b5d08c582d8d0664e
+Subproject commit a60a171b40b22b04d567ab39a8fd7f571c7921f5
diff --git a/app/src/main/cpp/GakumasLocalify/Hook.cpp b/app/src/main/cpp/GakumasLocalify/Hook.cpp
index 7ad488a..5694fb6 100644
--- a/app/src/main/cpp/GakumasLocalify/Hook.cpp
+++ b/app/src/main/cpp/GakumasLocalify/Hook.cpp
@@ -299,6 +299,11 @@ namespace GakumasLocal::HookMain {
void* fontCache = nullptr;
void* GetReplaceFont() {
+ static std::string fontName = Local::GetBasePath() / "local-files" / "gkamsZHFontMIX.otf";
+ if (!std::filesystem::exists(fontName)) {
+ return nullptr;
+ }
+
static auto CreateFontFromPath = reinterpret_cast(
Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Font::Internal_CreateFontFromPath(UnityEngine.Font,System.String)")
);
@@ -315,7 +320,6 @@ namespace GakumasLocal::HookMain {
const auto newFont = Font_klass->New();
Font_ctor->Invoke(newFont);
- static std::string fontName = Local::GetBasePath() / "local-files" / "gkamsZHFontMIX.otf";
CreateFontFromPath(newFont, Il2cppString::New(fontName));
fontCache = newFont;
return newFont;
@@ -334,9 +338,10 @@ namespace GakumasLocal::HookMain {
static auto UpdateFontAssetData = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro",
"TMP_FontAsset", "UpdateFontAssetData");
- auto fontAsset = get_font->Invoke(TMP_Textself);
auto newFont = GetReplaceFont();
- if (fontAsset && newFont) {
+ if (!newFont) return;
+ auto fontAsset = get_font->Invoke(TMP_Textself);
+ if (fontAsset) {
set_sourceFontFile->Invoke(fontAsset, newFont);
if (!updatedFontPtrs.contains(fontAsset)) {
updatedFontPtrs.emplace(fontAsset);
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt
index 9606238..a123491 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt
@@ -8,6 +8,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding
import io.github.chinosk.gakumas.localify.models.GakumasConfig
+import 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
@@ -55,6 +58,15 @@ interface ConfigListener {
fun onBUseArmCorrectionChanged(value: Boolean)
fun onBUseScaleChanged(value: Boolean)
fun onBClickPresetChanged(index: Int)
+ fun onPCheckBuiltInAssetsChanged(value: Boolean)
+ fun onPUseRemoteAssetsChanged(value: Boolean)
+ fun onPCleanLocalAssetsChanged(value: Boolean)
+ fun onPDelRemoteAfterUpdateChanged(value: Boolean)
+ fun onPTransRemoteZipUrlChanged(s: CharSequence, start: Int, before: Int, count: Int)
+ fun mainPageAssetsViewDataUpdate(downloadAbleState: Boolean? = null,
+ downloadProgressState: Float? = null,
+ localResourceVersionState: String? = null,
+ errorString: String? = null)
}
class UserConfigViewModelFactory(private val initialValue: GakumasConfig) : ViewModelProvider.Factory {
@@ -78,10 +90,15 @@ interface ConfigUpdateListener: ConfigListener {
var factory: UserConfigViewModelFactory
var viewModel: UserConfigViewModel
+ var programConfig: ProgramConfig
+ var programConfigFactory: ProgramConfigViewModelFactory
+ var programConfigViewModel: ProgramConfigViewModel
+
fun pushKeyEvent(event: KeyEvent): Boolean
fun getConfigContent(): String
fun checkConfigAndUpdateView()
fun saveConfig()
+ fun saveProgramConfig()
override fun onEnabledChanged(value: Boolean) {
@@ -493,4 +510,37 @@ interface ConfigUpdateListener: ConfigListener {
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 }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt
index 31fcbd1..0a84172 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt
@@ -11,19 +11,19 @@ import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
-import com.bytedance.shadowhook.ShadowHook
-import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
-import de.robv.android.xposed.IXposedHookLoadPackage
-import de.robv.android.xposed.IXposedHookZygoteInit
-import de.robv.android.xposed.XC_MethodHook
-import de.robv.android.xposed.XposedHelpers
-import de.robv.android.xposed.callbacks.XC_LoadPackage
-import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
import android.view.KeyEvent
import android.view.MotionEvent
import android.widget.Toast
+import com.bytedance.shadowhook.ShadowHook
+import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
import com.google.gson.Gson
+import de.robv.android.xposed.IXposedHookLoadPackage
+import de.robv.android.xposed.IXposedHookZygoteInit
+import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
+import de.robv.android.xposed.XposedHelpers
+import de.robv.android.xposed.callbacks.XC_LoadPackage
+import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
@@ -33,6 +33,11 @@ import kotlinx.coroutines.launch
import java.io.File
import java.util.Locale
import kotlin.system.measureTimeMillis
+import android.content.ContentResolver
+import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
+import io.github.chinosk.gakumas.localify.models.ProgramConfig
+import java.io.BufferedReader
+import java.io.InputStreamReader
val TAG = "GakumasLocalify"
@@ -46,6 +51,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private var gkmsDataInited = false
private var getConfigError: Exception? = null
+ private var externalFilesChecked: Boolean = false
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
// if (lpparam.packageName == "io.github.chinosk.gakumas.localify") {
@@ -183,7 +189,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
requestConfig(app.applicationContext)
}
- FilesChecker.initAndCheck(app.filesDir, modulePath)
+ FilesChecker.initDir(app.filesDir, modulePath)
initHook(
"${app.applicationInfo.nativeLibraryDir}/libil2cpp.so",
File(
@@ -215,6 +221,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
fun initGkmsConfig(activity: Activity) {
val intent = activity.intent
val gkmsData = intent.getStringExtra("gkmsData")
+ val programData = intent.getStringExtra("localData")
if (gkmsData != null) {
gkmsDataInited = true
val initConfig = try {
@@ -223,10 +230,41 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
catch (e: Exception) {
null
}
+ val programConfig = try {
+ Gson().fromJson(programData, ProgramConfig::class.java)
+ }
+ catch (e: Exception) {
+ null
+ }
+
+ // 清理本地文件
+ if (programConfig?.cleanLocalAssets == true) {
+ FilesChecker.cleanAssets()
+ }
+
+ // 检查 files 版本和 assets 版本并更新
+ if (programConfig?.checkBuiltInAssets == true) {
+ FilesChecker.initAndCheck(activity.filesDir, modulePath)
+ }
+
+ // 强制导出 assets 文件
if (initConfig?.forceExportResource == true) {
FilesChecker.updateFiles()
}
+ // 使用热更新文件
+ if (programConfig?.useRemoteAssets == true) {
+ val dataUri = intent.data
+ if (dataUri != null) {
+ if (!externalFilesChecked) {
+ externalFilesChecked = true
+ // Log.d(TAG, "dataUri: $dataUri")
+ FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir,
+ programConfig.delRemoteAfterUpdate)
+ }
+ }
+ }
+
loadConfig(gkmsData)
Log.d(TAG, "gkmsData: $gkmsData")
}
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt
index cedd43a..ce9286e 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt
@@ -1,6 +1,5 @@
package io.github.chinosk.gakumas.localify
-import SplashScreen
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
@@ -8,41 +7,59 @@ import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.core.content.FileProvider
import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.ViewModelProvider
+import com.google.gson.ExclusionStrategy
+import com.google.gson.FieldAttributes
import com.google.gson.Gson
+import com.google.gson.GsonBuilder
import com.google.gson.JsonSyntaxException
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.MainKeyEventDispatcher
import io.github.chinosk.gakumas.localify.models.GakumasConfig
-import io.github.chinosk.gakumas.localify.ui.theme.GakumasLocalifyTheme
-import java.io.File
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import kotlinx.coroutines.flow.MutableStateFlow
-import androidx.compose.runtime.State
-import androidx.compose.runtime.collectAsState
-import androidx.lifecycle.ViewModelProvider
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.rememberNavController
+import io.github.chinosk.gakumas.localify.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 java.io.File
class MainActivity : ComponentActivity(), ConfigUpdateListener {
override lateinit var binding: ActivityMainBinding
+ override lateinit var programConfig: ProgramConfig
override lateinit var factory: UserConfigViewModelFactory
override lateinit var viewModel: UserConfigViewModel
+ override lateinit var programConfigFactory: ProgramConfigViewModelFactory
+ override lateinit var programConfigViewModel: ProgramConfigViewModel
+
override fun onClickStartGame() {
val intent = Intent().apply {
setClassName("com.bandainamcoent.idolmaster_gakuen", "com.google.firebase.MessagingUnityPlayerActivity")
putExtra("gkmsData", getConfigContent())
+ putExtra("localData", getProgramConfigContent(listOf("transRemoteZipUrl", "p")))
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)
}
@@ -61,6 +78,32 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener {
}
}
+ private fun getProgramConfigContent(excludes: List? = null): String {
+ if (excludes == null) {
+ val configFile = File(filesDir, "localify-config.json")
+ return if (configFile.exists()) {
+ configFile.readText()
+ }
+ else {
+ "{}"
+ }
+ }
+ else {
+ val gson = GsonBuilder()
+ .setExclusionStrategies(object : ExclusionStrategy {
+ override fun shouldSkipField(f: FieldAttributes): Boolean {
+ return excludes.contains(f.name)
+ }
+
+ override fun shouldSkipClass(clazz: Class<*>): Boolean {
+ return false
+ }
+ })
+ .create()
+ return gson.toJson(programConfig)
+ }
+ }
+
override fun saveConfig() {
try {
binding.config!!.pf = false
@@ -73,6 +116,18 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener {
configFile.writeText(Gson().toJson(binding.config!!))
}
+ override fun saveProgramConfig() {
+ try {
+ programConfig.p = false
+ programConfigViewModel.configState.value = programConfig.copy( p = true ) // 更新 UI
+ }
+ catch (e: RuntimeException) {
+ Log.d(TAG, e.toString())
+ }
+ val configFile = File(filesDir, "localify-config.json")
+ configFile.writeText(Gson().toJson(programConfig))
+ }
+
fun getVersion(): List {
var versionText = ""
var resVersionText = "unknown"
@@ -107,6 +162,14 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener {
Gson().fromJson("{}", GakumasConfig::class.java)
}
saveConfig()
+
+ val programConfigStr = getProgramConfigContent()
+ programConfig = try {
+ Gson().fromJson(programConfigStr, ProgramConfig::class.java)
+ }
+ catch (e: JsonSyntaxException) {
+ Gson().fromJson("{}", ProgramConfig::class.java)
+ }
}
override fun checkConfigAndUpdateView() {
@@ -151,8 +214,13 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener {
factory = UserConfigViewModelFactory(binding.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) {
+ GakumasLocalifyTheme(dynamicColor = false, darkTheme = false) {
MainUI(context = this)
/*
val navController = rememberNavController()
@@ -182,6 +250,61 @@ fun getConfigState(context: MainActivity?, previewData: GakumasConfig?): State {
+ return if (context != null) {
+ context.programConfigViewModel.config.collectAsState()
+ }
+ else {
+ val configMSF = MutableStateFlow(previewData ?: ProgramConfig())
+ configMSF.asStateFlow().collectAsState()
+ }
+}
+
+@Composable
+fun getProgramDownloadState(context: MainActivity?): State {
+ return if (context != null) {
+ context.programConfigViewModel.downloadProgress.collectAsState()
+ }
+ else {
+ val configMSF = MutableStateFlow(0f)
+ configMSF.asStateFlow().collectAsState()
+ }
+}
+
+@Composable
+fun getProgramDownloadAbleState(context: MainActivity?): State {
+ return if (context != null) {
+ context.programConfigViewModel.downloadAble.collectAsState()
+ }
+ else {
+ val configMSF = MutableStateFlow(true)
+ configMSF.asStateFlow().collectAsState()
+ }
+}
+
+@Composable
+fun getProgramLocalResourceVersionState(context: MainActivity?): State {
+ return if (context != null) {
+ context.programConfigViewModel.localResourceVersion.collectAsState()
+ }
+ else {
+ val configMSF = MutableStateFlow("null")
+ configMSF.asStateFlow().collectAsState()
+ }
+}
+
+@Composable
+fun getProgramDownloadErrorStringState(context: MainActivity?): State {
+ return if (context != null) {
+ context.programConfigViewModel.errorString.collectAsState()
+ }
+ else {
+ val configMSF = MutableStateFlow("")
+ configMSF.asStateFlow().collectAsState()
+ }
+}
+
/*
class OldActivity : AppCompatActivity(), ConfigUpdateListener {
override lateinit var binding: ActivityMainBinding
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FileHotUpdater.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FileHotUpdater.kt
new file mode 100644
index 0000000..b96dc99
--- /dev/null
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FileHotUpdater.kt
@@ -0,0 +1,181 @@
+package io.github.chinosk.gakumas.localify.hookUtils
+
+import android.app.Activity
+import android.net.Uri
+import android.util.Log
+import io.github.chinosk.gakumas.localify.GakumasHookMain
+import io.github.chinosk.gakumas.localify.TAG
+import java.io.BufferedReader
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.util.zip.ZipInputStream
+
+object FileHotUpdater {
+ private fun unzip(zipFile: InputStream, destDir: String, matchNamePrefix: String = "",
+ replaceMatchNamePrefix: String? = null) {
+ val buffer = ByteArray(1024)
+ try {
+ val folder = File(destDir)
+ if (!folder.exists()) {
+ folder.mkdir()
+ }
+
+ val zipIn = ZipInputStream(zipFile)
+
+ var entry = zipIn.nextEntry
+ while (entry != null) {
+ var writeEntryName = entry.name
+ if (matchNamePrefix.isNotEmpty()) {
+ if (!entry.name.startsWith(matchNamePrefix)) {
+ zipIn.closeEntry()
+ entry = zipIn.nextEntry
+ continue
+ }
+ replaceMatchNamePrefix?.let {
+ writeEntryName = replaceMatchNamePrefix + writeEntryName.substring(
+ matchNamePrefix.length, writeEntryName.length
+ )
+ }
+ }
+ val filePath = destDir + File.separator + writeEntryName
+ if (!entry.isDirectory) {
+ extractFile(zipIn, filePath, buffer)
+ } else {
+ val dir = File(filePath)
+ dir.mkdirs()
+ }
+ zipIn.closeEntry()
+ entry = zipIn.nextEntry
+ }
+ zipIn.close()
+ } catch (e: Exception) {
+ Log.e(TAG, "unzip error: $e")
+ }
+ }
+
+ private fun unzip(zipFile: String, destDir: String, matchNamePrefix: String = "") {
+ return unzip(FileInputStream(zipFile), destDir, matchNamePrefix)
+ }
+
+ private fun extractFile(zipIn: ZipInputStream, filePath: String, buffer: ByteArray) {
+ val fout = FileOutputStream(filePath)
+ var length: Int
+ while (zipIn.read(buffer).also { length = it } > 0) {
+ fout.write(buffer, 0, length)
+ }
+ fout.close()
+ }
+
+ private fun getZipResourcePath(zipFile: InputStream): String? {
+ try {
+ val zipIn = ZipInputStream(zipFile)
+
+ var entry = zipIn.nextEntry
+ while (entry != null) {
+ if (entry.isDirectory) {
+ if (entry.name.endsWith("local-files/")) {
+ zipIn.close()
+ var retPath = File(entry.name, "..").canonicalPath
+ if (retPath.startsWith("/")) retPath = retPath.substring(1)
+ return retPath
+ }
+ }
+ zipIn.closeEntry()
+ entry = zipIn.nextEntry
+ }
+ zipIn.close()
+ }
+ catch (e: Exception) {
+ Log.e(TAG, "getZipResourcePath error: $e")
+ }
+ return null
+ }
+
+ private fun getZipResourceVersion(zipFile: InputStream, basePath: String): String? {
+ try {
+ val targetVersionFilePath = File(basePath, "version.txt").canonicalPath
+
+ val zipIn = ZipInputStream(zipFile)
+ var entry = zipIn.nextEntry
+ while (entry != null) {
+ if (!entry.isDirectory) {
+ if ("/${entry.name}" == targetVersionFilePath) {
+ Log.d(TAG, "targetVersionFilePath: $targetVersionFilePath")
+ val reader = BufferedReader(InputStreamReader(zipIn))
+ val versionContent = reader.use { it.readText() }
+ Log.d(TAG, "versionContent: $versionContent")
+ zipIn.close()
+ return versionContent
+ }
+ }
+ zipIn.closeEntry()
+ entry = zipIn.nextEntry
+ }
+ zipIn.close()
+ }
+ catch (e: Exception) {
+ Log.e(TAG, "getZipResourceVersion error: $e")
+ }
+ return null
+ }
+
+ private fun getZipResourceVersion(zipFile: String, basePath: String): String? {
+ return getZipResourceVersion(FileInputStream(zipFile), basePath)
+ }
+
+ fun getZipResourceVersion(zipFile: String): String? {
+ return try {
+ val basePath = getZipResourcePath(FileInputStream(zipFile))
+ basePath?.let { getZipResourceVersion(zipFile, it) }
+ }
+ catch (_: Exception) {
+ null
+ }
+ }
+
+ fun updateFilesFromZip(activity: Activity, zipFileUri: Uri, filesDir: File, deleteAfterUpdate: Boolean) {
+ try {
+ GakumasHookMain.showToast("Updating files from zip...")
+
+ var basePath: String?
+ activity.contentResolver.openInputStream(zipFileUri).use {
+ basePath = it?.let { getZipResourcePath(it) }
+ if (basePath == null) {
+ Log.e(TAG, "getZipResourcePath failed.")
+ return@updateFilesFromZip
+ }
+ }
+
+ /*
+ var resourceVersion: String?
+ activity.contentResolver.openInputStream(zipFileUri).use {
+ resourceVersion = it?.let { getZipResourceVersion(it, basePath!!) }
+ Log.d(TAG, "resourceVersion: $resourceVersion ($basePath)")
+ }*/
+
+ activity.contentResolver.openInputStream(zipFileUri).use {
+ it?.let {
+ unzip(it, File(filesDir, FilesChecker.localizationFilesDir).absolutePath,
+ basePath!!, "../gakumas-local/")
+ if (deleteAfterUpdate) {
+ activity.contentResolver.delete(zipFileUri, null, null)
+ }
+ GakumasHookMain.showToast("Update success.")
+ }
+ }
+
+ }
+ catch (e: java.io.FileNotFoundException) {
+ Log.i(TAG, "updateFilesFromZip - file not found: $e")
+ GakumasHookMain.showToast("Update file not found.")
+ }
+ catch (e: Exception) {
+ Log.e(TAG, "updateFilesFromZip failed: $e")
+ GakumasHookMain.showToast("Updating files failed: $e")
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt
index b62e567..61d72e7 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt
@@ -16,12 +16,16 @@ object FilesChecker {
var filesUpdated = false
fun initAndCheck(fileDir: File, modulePath: String) {
- this.filesDir = fileDir
- this.modulePath = modulePath
+ initDir(fileDir, modulePath)
checkFiles()
}
+ fun initDir(fileDir: File, modulePath: String) {
+ this.filesDir = fileDir
+ this.modulePath = modulePath
+ }
+
fun checkFiles() {
val installedVersion = getInstalledVersion()
val pluginVersion = getPluginVersion()
@@ -118,4 +122,45 @@ object FilesChecker {
return stringBuilder.toString()
}
+ private fun deleteRecursively(file: File): Boolean {
+ if (file.isDirectory) {
+ val children = file.listFiles()
+ if (children != null) {
+ for (child in children) {
+ val success = deleteRecursively(child)
+ if (!success) {
+ return false
+ }
+ }
+ }
+ }
+ return file.delete()
+ }
+
+ fun cleanAssets() {
+ val pluginBasePath = File(filesDir, localizationFilesDir)
+ val localFilesDir = File(pluginBasePath, "local-files")
+
+ val fontFile = File(localFilesDir, "gkamsZHFontMIX.otf")
+ val resourceDir = File(localFilesDir, "resource")
+ val genericTransDir = File(localFilesDir, "genericTrans")
+ val genericTransFile = File(localFilesDir, "generic.json")
+ val i18nFile = File(localFilesDir, "localization.json")
+
+ if (fontFile.exists()) {
+ fontFile.delete()
+ }
+ if (deleteRecursively(resourceDir)) {
+ resourceDir.mkdirs()
+ }
+ if (deleteRecursively(genericTransDir)) {
+ genericTransDir.mkdirs()
+ }
+ if (genericTransFile.exists()) {
+ genericTransFile.writeText("{}")
+ }
+ if (i18nFile.exists()) {
+ i18nFile.writeText("{}")
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt
new file mode 100644
index 0000000..0e908b6
--- /dev/null
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt
@@ -0,0 +1,139 @@
+package io.github.chinosk.gakumas.localify.mainUtils
+
+import android.util.Log
+import io.github.chinosk.gakumas.localify.TAG
+import okhttp3.*
+import java.io.IOException
+import java.io.ByteArrayOutputStream
+import java.util.concurrent.TimeUnit
+
+object FileDownloader {
+ private val client = OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(0, TimeUnit.SECONDS)
+ .readTimeout(0, TimeUnit.SECONDS)
+ .build()
+
+ private var call: Call? = null
+
+ fun downloadFile(
+ url: String,
+ onDownload: (Float, downloaded: Long, size: Long) -> Unit,
+ onSuccess: (ByteArray) -> Unit,
+ onFailed: (Int, String) -> Unit,
+ checkContentTypes: List? = null
+ ) {
+ try {
+ if (call != null) {
+ onFailed(-1, "Another file is downloading.")
+ return
+ }
+ val request = Request.Builder()
+ .url(url)
+ .build()
+
+ call = client.newCall(request)
+ call?.enqueue(object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ this@FileDownloader.call = null
+ if (call.isCanceled()) {
+ onFailed(-1, "Download canceled")
+ } else {
+ onFailed(-1, e.message ?: "Unknown error")
+ }
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ if (!response.isSuccessful) {
+ this@FileDownloader.call = null
+ onFailed(response.code, response.message)
+ return
+ }
+
+ if (checkContentTypes != null) {
+ val contentType = response.header("Content-Type")
+ if (!checkContentTypes.contains(contentType)) {
+ onFailed(-1, "Unexpected content type: $contentType")
+ this@FileDownloader.call = null
+ return
+ }
+ }
+
+ response.body?.let { responseBody ->
+ val contentLength = responseBody.contentLength()
+ val inputStream = responseBody.byteStream()
+ val buffer = ByteArray(8 * 1024)
+ var downloadedBytes = 0L
+ var read: Int
+ val outputStream = ByteArrayOutputStream()
+
+ try {
+ while (inputStream.read(buffer).also { read = it } != -1) {
+ outputStream.write(buffer, 0, read)
+ downloadedBytes += read
+ val progress = if (contentLength < 0) {
+ 0f
+ }
+ else {
+ downloadedBytes.toFloat() / contentLength
+ }
+ onDownload(progress, downloadedBytes, contentLength)
+ }
+ onSuccess(outputStream.toByteArray())
+ } catch (e: IOException) {
+ if (call.isCanceled()) {
+ onFailed(-1, "Download canceled")
+ } else {
+ onFailed(-1, e.message ?: "Error reading stream")
+ }
+ } finally {
+ this@FileDownloader.call = null
+ inputStream.close()
+ outputStream.close()
+ }
+ } ?: run {
+ this@FileDownloader.call = null
+ onFailed(-1, "Response body is null")
+ }
+ }
+ })
+ }
+ catch (e: Exception) {
+ onFailed(-1, e.toString())
+ call = null
+ }
+
+ }
+
+ fun cancel() {
+ call?.cancel()
+ this@FileDownloader.call = null
+ }
+
+ /**
+ * return: Status, newString
+ * Status: 0 - not change, 1 - need check, 2 - modified, 3 - checked
+ **/
+ fun checkAndChangeDownloadURL(url: String, forceEdit: Boolean = false): Pair {
+
+ if (!url.startsWith("https://github.com/")) { // check github only
+ return Pair(0, url)
+ }
+ if (url.endsWith(".zip")) {
+ return Pair(0, url)
+ }
+
+ // https://github.com/chinosk6/GakumasTranslationData
+ // https://github.com/chinosk6/GakumasTranslationData.git
+ // https://github.com/chinosk6/GakumasTranslationData/archive/refs/heads/main.zip
+ if (url.endsWith(".git")) {
+ return Pair(2, "${url.substring(0, url.length - 4)}/archive/refs/heads/main.zip")
+ }
+
+ if (forceEdit) {
+ return Pair(3, "$url/archive/refs/heads/main.zip")
+ }
+
+ return Pair(1, url)
+ }
+}
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt
new file mode 100644
index 0000000..04ddb24
--- /dev/null
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt
@@ -0,0 +1,12 @@
+package io.github.chinosk.gakumas.localify.models
+
+
+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
+)
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt
index a6c190f..4a66d9a 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt
@@ -5,18 +5,67 @@ 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
-
-class CollapsibleBoxViewModel(initiallyExpanded: Boolean = false) : ViewModel() {
- var expanded by mutableStateOf(initiallyExpanded)
+open class CollapsibleBoxViewModel(initiallyBreastExpanded: Boolean = false) : ViewModel() {
+ open var expanded by mutableStateOf(initiallyBreastExpanded)
}
-class CollapsibleBoxViewModelFactory(private val initiallyExpanded: Boolean) : ViewModelProvider.Factory {
+class BreastCollapsibleBoxViewModel(initiallyBreastExpanded: Boolean = false) : CollapsibleBoxViewModel(initiallyBreastExpanded) {
+ override var expanded by mutableStateOf(initiallyBreastExpanded)
+}
+
+class ResourceCollapsibleBoxViewModel(initiallyBreastExpanded: Boolean = false) : CollapsibleBoxViewModel(initiallyBreastExpanded) {
+ override var expanded by mutableStateOf(initiallyBreastExpanded)
+}
+
+class BreastCollapsibleBoxViewModelFactory(private val initiallyExpanded: Boolean) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
- if (modelClass.isAssignableFrom(CollapsibleBoxViewModel::class.java)) {
+ if (modelClass.isAssignableFrom(BreastCollapsibleBoxViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
- return CollapsibleBoxViewModel(initiallyExpanded) as T
+ return BreastCollapsibleBoxViewModel(initiallyExpanded) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
-}
\ No newline at end of file
+}
+
+class ResourceCollapsibleBoxViewModelFactory(private val initiallyExpanded: Boolean) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(ResourceCollapsibleBoxViewModel::class.java)) {
+ @Suppress("UNCHECKED_CAST")
+ return ResourceCollapsibleBoxViewModel(initiallyExpanded) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class")
+ }
+}
+
+
+class ProgramConfigViewModelFactory(private val initialValue: ProgramConfig,
+ private val localResourceVersion: String) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(ProgramConfigViewModel::class.java)) {
+ @Suppress("UNCHECKED_CAST")
+ return ProgramConfigViewModel(initialValue, localResourceVersion) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class")
+ }
+}
+
+class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion: String) : ViewModel() {
+ val configState = MutableStateFlow(initValue)
+ val config: StateFlow = configState.asStateFlow()
+
+ val downloadProgressState = MutableStateFlow(-1f)
+ val downloadProgress: StateFlow = downloadProgressState.asStateFlow()
+
+ val downloadAbleState = MutableStateFlow(true)
+ val downloadAble: StateFlow = downloadAbleState.asStateFlow()
+
+ val localResourceVersionState = MutableStateFlow(initLocalResourceVersion)
+ val localResourceVersion: StateFlow = localResourceVersionState.asStateFlow()
+
+ val errorStringState = MutableStateFlow("")
+ val errorString: StateFlow = errorStringState.asStateFlow()
+}
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuButton.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuButton.kt
index 5f9b446..8dc5279 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuButton.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuButton.kt
@@ -34,13 +34,15 @@ fun GakuButton(
shape: Shape = RoundedCornerShape(50.dp), // 用于实现左右两边的半圆角
shadowElevation: Dp = 8.dp, // 阴影的高度
borderWidth: Dp = 1.dp, // 描边的宽度
- borderColor: Color = Color.Transparent // 描边的颜色
+ borderColor: Color = Color.Transparent, // 描边的颜色
+ enabled: Boolean = true
) {
var buttonSize by remember { mutableStateOf(IntSize.Zero) }
val gradient = remember(buttonSize) {
Brush.linearGradient(
- colors = listOf(Color(0xFFFF5F19), Color(0xFFFFA028)),
+ 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()) // 动态终点
)
@@ -48,6 +50,7 @@ fun GakuButton(
Button(
onClick = onClick,
+ enabled = enabled,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
@@ -61,7 +64,7 @@ fun GakuButton(
.border(borderWidth, borderColor, shape),
contentPadding = PaddingValues(0.dp)
) {
- Text(text = text)
+ Text(text = text, color = if (enabled) Color.White else Color(0xFF111111))
}
}
@@ -69,5 +72,6 @@ fun GakuButton(
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun GakuButtonPreview() {
- GakuButton(modifier = Modifier.width(80.dp).height(40.dp), text = "Button", onClick = {})
+ GakuButton(modifier = Modifier.width(80.dp).height(40.dp), text = "Button", onClick = {},
+ enabled = true)
}
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuProgressBar.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuProgressBar.kt
new file mode 100644
index 0000000..3d6ce9b
--- /dev/null
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuProgressBar.kt
@@ -0,0 +1,55 @@
+package io.github.chinosk.gakumas.localify.ui.components
+
+
+import android.content.res.Configuration.UI_MODE_NIGHT_NO
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+
+@Composable
+fun GakuProgressBar(modifier: Modifier = Modifier, progress: Float, isError: Boolean = false) {
+ val animatedProgress by animateFloatAsState(targetValue = progress, label = "progressAnime")
+
+ Row(
+ verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
+ modifier = modifier
+ ) {
+ if (progress <= 0f) {
+ LinearProgressIndicator(
+ modifier = Modifier
+ .weight(1f)
+ .clip(RoundedCornerShape(4.dp))
+ .height(8.dp),
+ color = if (isError) Color(0xFFE2041B) else Color(0xFFF9C114),
+ )
+ }
+ else {
+ LinearProgressIndicator(
+ progress = { animatedProgress },
+ modifier = Modifier
+ .weight(1f)
+ .clip(RoundedCornerShape(4.dp))
+ .height(8.dp),
+ color = if (isError) Color(0xFFE2041B) else Color(0xFFF9C114),
+ )
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(if (progress > 0f) "${(progress * 100).toInt()}%" else if (isError) "Failed" else "Downloading")
+ }
+}
+
+@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
+@Composable
+fun GakuProgressBarPreview() {
+ GakuProgressBar(progress = 0.25f)
+}
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuSwitch.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuSwitch.kt
index aa1b0ef..1355e1a 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuSwitch.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuSwitch.kt
@@ -34,7 +34,7 @@ fun GakuSwitch(modifier: Modifier = Modifier,
modifier = Modifier,
colors = SwitchDefaults.colors(
checkedThumbColor = Color(0xFFFFFFFF),
- checkedTrackColor = Color(0xFFF89400),
+ checkedTrackColor = Color(0xFFF9C114),
uncheckedThumbColor = Color(0xFFFFFFFF),
uncheckedTrackColor = Color(0xFFCFD8DC),
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/CollapsibleBox.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/CollapsibleBox.kt
index ffcd4a7..bbc45aa 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/CollapsibleBox.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/base/CollapsibleBox.kt
@@ -10,6 +10,7 @@ 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.*
@@ -29,6 +30,8 @@ fun CollapsibleBox(
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
@@ -65,6 +68,8 @@ fun CollapsibleBox(
modifier = Modifier
.height(animatedHeight)
.fillMaxWidth()
+ .padding(start = innerPaddingLeftRight, end = innerPaddingLeftRight,
+ top = innerPaddingTopBottom, bottom = innerPaddingTopBottom)
// .fillMaxSize()
.clickable {
if (!expanded && showExpand) {
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt
index 127769a..69972b0 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt
@@ -29,8 +29,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import io.github.chinosk.gakumas.localify.MainActivity
import io.github.chinosk.gakumas.localify.R
import io.github.chinosk.gakumas.localify.getConfigState
-import io.github.chinosk.gakumas.localify.models.CollapsibleBoxViewModel
-import io.github.chinosk.gakumas.localify.models.CollapsibleBoxViewModelFactory
+import io.github.chinosk.gakumas.localify.models.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
@@ -47,8 +47,8 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
val config = getConfigState(context, previewData)
// val scrollState = rememberScrollState()
- val breastParamViewModel: CollapsibleBoxViewModel =
- viewModel(factory = CollapsibleBoxViewModelFactory(initiallyExpanded = false))
+ val breastParamViewModel: BreastCollapsibleBoxViewModel =
+ viewModel(factory = BreastCollapsibleBoxViewModelFactory(initiallyExpanded = false))
val keyBoardOptionsDecimal = remember {
KeyboardOptions(keyboardType = KeyboardType.Decimal)
}
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt
index f818ea6..be25fef 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt
@@ -2,6 +2,7 @@ package io.github.chinosk.gakumas.localify.ui.pages.subPages
import 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
@@ -16,24 +17,39 @@ 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
@@ -43,6 +59,13 @@ fun HomePage(modifier: Modifier = Modifier,
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)
@@ -51,6 +74,57 @@ fun HomePage(modifier: Modifier = Modifier,
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)
@@ -75,6 +149,138 @@ fun HomePage(modifier: Modifier = Modifier,
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
@@ -263,7 +469,7 @@ fun HomePage(modifier: Modifier = Modifier,
}
-@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO, widthDp = 880)
+@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable
fun HomePagePreview(modifier: Modifier = Modifier, data: GakumasConfig = GakumasConfig()) {
HomePage(modifier, previewData = data)
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 2b4dfdf..f53086c 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -63,6 +63,17 @@
插件本体
贡献者列表
译文仓库
+ 资源设置
+ 检查内置数据更新
+ 清除游戏目录内的插件资源
+ 使用远程 ZIP 数据
+ 资源地址
+ 下载
+ 文件解析失败
+ 此文件不是一个有效的 ZIP 翻译资源包
+ 取消
+ 已下载资源版本
+ 替换文件后删除下载缓存
about_contributors_zh_cn.json
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index be1df2f..49e079e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -63,6 +63,17 @@
Plugin Code
Contributors
Translation Repository
+ Resource Settings
+ Check Built-in Assets Update
+ Delete Plugin Resource
+ Use Remote ZIP Resource
+ Resource URL
+ Download
+ Invalid file
+ This file is not a valid ZIP translation resource pack.
+ Cancel
+ Downloaded Version
+ Delete Cache File After Update
about_contributors_en.json
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..4f7310f
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+