From dbb7c8d8f69ebee27928e6498c7e3a54c37748c1 Mon Sep 17 00:00:00 2001 From: chinosk <74499927+chinosk6@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:54:17 -0500 Subject: [PATCH] Feature: Hot Update Translation (#32) * update translation data from repo --- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 11 + app/src/main/assets/gakumas-local | 2 +- app/src/main/cpp/GakumasLocalify/Hook.cpp | 11 +- .../gakumas/localify/ConfigUpdateListener.kt | 50 +++++ .../gakumas/localify/GakumasHookMain.kt | 56 ++++- .../chinosk/gakumas/localify/MainActivity.kt | 149 +++++++++++-- .../localify/hookUtils/FileHotUpdater.kt | 181 +++++++++++++++ .../localify/hookUtils/FilesChecker.kt | 49 ++++- .../localify/mainUtils/FileDownloader.kt | 139 ++++++++++++ .../gakumas/localify/models/ProgramConfig.kt | 12 + .../gakumas/localify/models/ViewModels.kt | 63 +++++- .../localify/ui/components/GakuButton.kt | 12 +- .../localify/ui/components/GakuProgressBar.kt | 55 +++++ .../localify/ui/components/GakuSwitch.kt | 2 +- .../ui/components/base/CollapsibleBox.kt | 5 + .../ui/pages/subPages/AdvancedSettingsPage.kt | 8 +- .../localify/ui/pages/subPages/HomePage.kt | 208 +++++++++++++++++- app/src/main/res/values-zh-rCN/strings.xml | 11 + app/src/main/res/values/strings.xml | 11 + app/src/main/res/xml/file_paths.xml | 4 + 21 files changed, 999 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FileHotUpdater.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/ui/components/GakuProgressBar.kt create mode 100644 app/src/main/res/xml/file_paths.xml 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 @@ + + + +