1
0

Feature: Hot Update Translation (#32)

* update translation data from repo
This commit is contained in:
chinosk 2024-06-27 10:54:17 -05:00 committed by GitHub
parent 91dea41ca2
commit dbb7c8d8f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 999 additions and 46 deletions

View File

@ -14,7 +14,7 @@ android {
minSdk 29 minSdk 29
targetSdk 34 targetSdk 34
versionCode 2 versionCode 2
versionName "v1.1" versionName "v1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -112,6 +112,10 @@ dependencies {
implementation "io.coil-kt:coil-compose:2.6.0" implementation "io.coil-kt:coil-compose:2.6.0"
implementation "io.coil-kt:coil-svg: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 'io.github.hexhacking:xdl:2.1.1'
implementation 'com.bytedance.android:shadowhook:1.0.9' implementation 'com.bytedance.android:shadowhook:1.0.9'
compileOnly 'de.robv.android.xposed:api:82' compileOnly 'de.robv.android.xposed:api:82'

View File

@ -7,6 +7,7 @@
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:usesCleartextTraffic="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@ -42,6 +43,16 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

@ -1 +1 @@
Subproject commit cdd0ad064cf6d3f13107e19b5d08c582d8d0664e Subproject commit a60a171b40b22b04d567ab39a8fd7f571c7921f5

View File

@ -299,6 +299,11 @@ namespace GakumasLocal::HookMain {
void* fontCache = nullptr; void* fontCache = nullptr;
void* GetReplaceFont() { void* GetReplaceFont() {
static std::string fontName = Local::GetBasePath() / "local-files" / "gkamsZHFontMIX.otf";
if (!std::filesystem::exists(fontName)) {
return nullptr;
}
static auto CreateFontFromPath = reinterpret_cast<void (*)(void* self, Il2cppString* path)>( static auto CreateFontFromPath = reinterpret_cast<void (*)(void* self, Il2cppString* path)>(
Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Font::Internal_CreateFontFromPath(UnityEngine.Font,System.String)") 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<void*>(); const auto newFont = Font_klass->New<void*>();
Font_ctor->Invoke<void>(newFont); Font_ctor->Invoke<void>(newFont);
static std::string fontName = Local::GetBasePath() / "local-files" / "gkamsZHFontMIX.otf";
CreateFontFromPath(newFont, Il2cppString::New(fontName)); CreateFontFromPath(newFont, Il2cppString::New(fontName));
fontCache = newFont; fontCache = newFont;
return newFont; return newFont;
@ -334,9 +338,10 @@ namespace GakumasLocal::HookMain {
static auto UpdateFontAssetData = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro", static auto UpdateFontAssetData = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro",
"TMP_FontAsset", "UpdateFontAssetData"); "TMP_FontAsset", "UpdateFontAssetData");
auto fontAsset = get_font->Invoke<void*>(TMP_Textself);
auto newFont = GetReplaceFont(); auto newFont = GetReplaceFont();
if (fontAsset && newFont) { if (!newFont) return;
auto fontAsset = get_font->Invoke<void*>(TMP_Textself);
if (fontAsset) {
set_sourceFontFile->Invoke<void>(fontAsset, newFont); set_sourceFontFile->Invoke<void>(fontAsset, newFont);
if (!updatedFontPtrs.contains(fontAsset)) { if (!updatedFontPtrs.contains(fontAsset)) {
updatedFontPtrs.emplace(fontAsset); updatedFontPtrs.emplace(fontAsset);

View File

@ -8,6 +8,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding
import io.github.chinosk.gakumas.localify.models.GakumasConfig import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModel
import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModelFactory
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -55,6 +58,15 @@ interface ConfigListener {
fun onBUseArmCorrectionChanged(value: Boolean) fun onBUseArmCorrectionChanged(value: Boolean)
fun onBUseScaleChanged(value: Boolean) fun onBUseScaleChanged(value: Boolean)
fun onBClickPresetChanged(index: Int) fun onBClickPresetChanged(index: Int)
fun onPCheckBuiltInAssetsChanged(value: Boolean)
fun onPUseRemoteAssetsChanged(value: Boolean)
fun onPCleanLocalAssetsChanged(value: Boolean)
fun onPDelRemoteAfterUpdateChanged(value: Boolean)
fun onPTransRemoteZipUrlChanged(s: CharSequence, start: Int, before: Int, count: Int)
fun mainPageAssetsViewDataUpdate(downloadAbleState: Boolean? = null,
downloadProgressState: Float? = null,
localResourceVersionState: String? = null,
errorString: String? = null)
} }
class UserConfigViewModelFactory(private val initialValue: GakumasConfig) : ViewModelProvider.Factory { class UserConfigViewModelFactory(private val initialValue: GakumasConfig) : ViewModelProvider.Factory {
@ -78,10 +90,15 @@ interface ConfigUpdateListener: ConfigListener {
var factory: UserConfigViewModelFactory var factory: UserConfigViewModelFactory
var viewModel: UserConfigViewModel var viewModel: UserConfigViewModel
var programConfig: ProgramConfig
var programConfigFactory: ProgramConfigViewModelFactory
var programConfigViewModel: ProgramConfigViewModel
fun pushKeyEvent(event: KeyEvent): Boolean fun pushKeyEvent(event: KeyEvent): Boolean
fun getConfigContent(): String fun getConfigContent(): String
fun checkConfigAndUpdateView() fun checkConfigAndUpdateView()
fun saveConfig() fun saveConfig()
fun saveProgramConfig()
override fun onEnabledChanged(value: Boolean) { override fun onEnabledChanged(value: Boolean) {
@ -493,4 +510,37 @@ interface ConfigUpdateListener: ConfigListener {
saveConfig() saveConfig()
} }
override fun onPCheckBuiltInAssetsChanged(value: Boolean) {
programConfig.checkBuiltInAssets = value
saveProgramConfig()
}
override fun onPUseRemoteAssetsChanged(value: Boolean) {
programConfig.useRemoteAssets = value
saveProgramConfig()
}
override fun onPCleanLocalAssetsChanged(value: Boolean) {
programConfig.cleanLocalAssets = value
saveProgramConfig()
}
override fun onPDelRemoteAfterUpdateChanged(value: Boolean) {
programConfig.delRemoteAfterUpdate = value
saveProgramConfig()
}
override fun onPTransRemoteZipUrlChanged(s: CharSequence, start: Int, before: Int, count: Int) {
programConfig.transRemoteZipUrl = s.toString()
saveProgramConfig()
}
override fun mainPageAssetsViewDataUpdate(downloadAbleState: Boolean?, downloadProgressState: Float?,
localResourceVersionState: String?, errorString: String?) {
downloadAbleState?.let { programConfigViewModel.downloadAbleState.value = downloadAbleState }
downloadProgressState?.let{ programConfigViewModel.downloadProgressState.value = downloadProgressState }
localResourceVersionState?.let{ programConfigViewModel.localResourceVersionState.value = localResourceVersionState }
errorString?.let{ programConfigViewModel.errorStringState.value = errorString }
}
} }

View File

@ -11,19 +11,19 @@ import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import 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.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.Toast import android.widget.Toast
import com.bytedance.shadowhook.ShadowHook
import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
import com.google.gson.Gson 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.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 io.github.chinosk.gakumas.localify.models.GakumasConfig
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -33,6 +33,11 @@ import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import kotlin.system.measureTimeMillis import 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" val TAG = "GakumasLocalify"
@ -46,6 +51,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private var gkmsDataInited = false private var gkmsDataInited = false
private var getConfigError: Exception? = null private var getConfigError: Exception? = null
private var externalFilesChecked: Boolean = false
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
// if (lpparam.packageName == "io.github.chinosk.gakumas.localify") { // if (lpparam.packageName == "io.github.chinosk.gakumas.localify") {
@ -183,7 +189,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
requestConfig(app.applicationContext) requestConfig(app.applicationContext)
} }
FilesChecker.initAndCheck(app.filesDir, modulePath) FilesChecker.initDir(app.filesDir, modulePath)
initHook( initHook(
"${app.applicationInfo.nativeLibraryDir}/libil2cpp.so", "${app.applicationInfo.nativeLibraryDir}/libil2cpp.so",
File( File(
@ -215,6 +221,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
fun initGkmsConfig(activity: Activity) { fun initGkmsConfig(activity: Activity) {
val intent = activity.intent val intent = activity.intent
val gkmsData = intent.getStringExtra("gkmsData") val gkmsData = intent.getStringExtra("gkmsData")
val programData = intent.getStringExtra("localData")
if (gkmsData != null) { if (gkmsData != null) {
gkmsDataInited = true gkmsDataInited = true
val initConfig = try { val initConfig = try {
@ -223,10 +230,41 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
catch (e: Exception) { catch (e: Exception) {
null 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) { if (initConfig?.forceExportResource == true) {
FilesChecker.updateFiles() FilesChecker.updateFiles()
} }
// 使用热更新文件
if (programConfig?.useRemoteAssets == true) {
val dataUri = intent.data
if (dataUri != null) {
if (!externalFilesChecked) {
externalFilesChecked = true
// Log.d(TAG, "dataUri: $dataUri")
FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir,
programConfig.delRemoteAfterUpdate)
}
}
}
loadConfig(gkmsData) loadConfig(gkmsData)
Log.d(TAG, "gkmsData: $gkmsData") Log.d(TAG, "gkmsData: $gkmsData")
} }

View File

@ -1,6 +1,5 @@
package io.github.chinosk.gakumas.localify package io.github.chinosk.gakumas.localify
import SplashScreen
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -8,41 +7,59 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable 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.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.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
import io.github.chinosk.gakumas.localify.hookUtils.MainKeyEventDispatcher import io.github.chinosk.gakumas.localify.hookUtils.MainKeyEventDispatcher
import io.github.chinosk.gakumas.localify.models.GakumasConfig import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.ui.theme.GakumasLocalifyTheme import io.github.chinosk.gakumas.localify.models.ProgramConfig
import java.io.File import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModel
import androidx.activity.ComponentActivity import io.github.chinosk.gakumas.localify.models.ProgramConfigViewModelFactory
import androidx.activity.compose.setContent
import kotlinx.coroutines.flow.MutableStateFlow
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import io.github.chinosk.gakumas.localify.ui.pages.MainUI import io.github.chinosk.gakumas.localify.ui.pages.MainUI
import io.github.chinosk.gakumas.localify.ui.theme.GakumasLocalifyTheme
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import java.io.File
class MainActivity : ComponentActivity(), ConfigUpdateListener { class MainActivity : ComponentActivity(), ConfigUpdateListener {
override lateinit var binding: ActivityMainBinding override lateinit var binding: ActivityMainBinding
override lateinit var programConfig: ProgramConfig
override lateinit var factory: UserConfigViewModelFactory override lateinit var factory: UserConfigViewModelFactory
override lateinit var viewModel: UserConfigViewModel override lateinit var viewModel: UserConfigViewModel
override lateinit var programConfigFactory: ProgramConfigViewModelFactory
override lateinit var programConfigViewModel: ProgramConfigViewModel
override fun onClickStartGame() { override fun onClickStartGame() {
val intent = Intent().apply { val intent = Intent().apply {
setClassName("com.bandainamcoent.idolmaster_gakuen", "com.google.firebase.MessagingUnityPlayerActivity") setClassName("com.bandainamcoent.idolmaster_gakuen", "com.google.firebase.MessagingUnityPlayerActivity")
putExtra("gkmsData", getConfigContent()) putExtra("gkmsData", getConfigContent())
putExtra("localData", getProgramConfigContent(listOf("transRemoteZipUrl", "p")))
flags = Intent.FLAG_ACTIVITY_NEW_TASK 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) startActivity(intent)
} }
@ -61,6 +78,32 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener {
} }
} }
private fun getProgramConfigContent(excludes: List<String>? = 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() { override fun saveConfig() {
try { try {
binding.config!!.pf = false binding.config!!.pf = false
@ -73,6 +116,18 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener {
configFile.writeText(Gson().toJson(binding.config!!)) 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<String> { fun getVersion(): List<String> {
var versionText = "" var versionText = ""
var resVersionText = "unknown" var resVersionText = "unknown"
@ -107,6 +162,14 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener {
Gson().fromJson("{}", GakumasConfig::class.java) Gson().fromJson("{}", GakumasConfig::class.java)
} }
saveConfig() saveConfig()
val programConfigStr = getProgramConfigContent()
programConfig = try {
Gson().fromJson(programConfigStr, ProgramConfig::class.java)
}
catch (e: JsonSyntaxException) {
Gson().fromJson("{}", ProgramConfig::class.java)
}
} }
override fun checkConfigAndUpdateView() { override fun checkConfigAndUpdateView() {
@ -151,8 +214,13 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener {
factory = UserConfigViewModelFactory(binding.config!!) factory = UserConfigViewModelFactory(binding.config!!)
viewModel = ViewModelProvider(this, factory)[UserConfigViewModel::class.java] 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 { setContent {
GakumasLocalifyTheme(dynamicColor = false) { GakumasLocalifyTheme(dynamicColor = false, darkTheme = false) {
MainUI(context = this) MainUI(context = this)
/* /*
val navController = rememberNavController() val navController = rememberNavController()
@ -182,6 +250,61 @@ fun getConfigState(context: MainActivity?, previewData: GakumasConfig?): State<G
} }
} }
@Composable
fun getProgramConfigState(context: MainActivity?, previewData: ProgramConfig? = null): State<ProgramConfig> {
return if (context != null) {
context.programConfigViewModel.config.collectAsState()
}
else {
val configMSF = MutableStateFlow(previewData ?: ProgramConfig())
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramDownloadState(context: MainActivity?): State<Float> {
return if (context != null) {
context.programConfigViewModel.downloadProgress.collectAsState()
}
else {
val configMSF = MutableStateFlow(0f)
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramDownloadAbleState(context: MainActivity?): State<Boolean> {
return if (context != null) {
context.programConfigViewModel.downloadAble.collectAsState()
}
else {
val configMSF = MutableStateFlow(true)
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramLocalResourceVersionState(context: MainActivity?): State<String> {
return if (context != null) {
context.programConfigViewModel.localResourceVersion.collectAsState()
}
else {
val configMSF = MutableStateFlow("null")
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramDownloadErrorStringState(context: MainActivity?): State<String> {
return if (context != null) {
context.programConfigViewModel.errorString.collectAsState()
}
else {
val configMSF = MutableStateFlow("")
configMSF.asStateFlow().collectAsState()
}
}
/* /*
class OldActivity : AppCompatActivity(), ConfigUpdateListener { class OldActivity : AppCompatActivity(), ConfigUpdateListener {
override lateinit var binding: ActivityMainBinding override lateinit var binding: ActivityMainBinding

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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
)

View File

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

View File

@ -34,13 +34,15 @@ fun GakuButton(
shape: Shape = RoundedCornerShape(50.dp), // 用于实现左右两边的半圆角 shape: Shape = RoundedCornerShape(50.dp), // 用于实现左右两边的半圆角
shadowElevation: Dp = 8.dp, // 阴影的高度 shadowElevation: Dp = 8.dp, // 阴影的高度
borderWidth: Dp = 1.dp, // 描边的宽度 borderWidth: Dp = 1.dp, // 描边的宽度
borderColor: Color = Color.Transparent // 描边的颜色 borderColor: Color = Color.Transparent, // 描边的颜色
enabled: Boolean = true
) { ) {
var buttonSize by remember { mutableStateOf(IntSize.Zero) } var buttonSize by remember { mutableStateOf(IntSize.Zero) }
val gradient = remember(buttonSize) { val gradient = remember(buttonSize) {
Brush.linearGradient( 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), start = Offset(0f, 0f),
end = Offset(buttonSize.width.toFloat(), buttonSize.height.toFloat()) // 动态终点 end = Offset(buttonSize.width.toFloat(), buttonSize.height.toFloat()) // 动态终点
) )
@ -48,6 +50,7 @@ fun GakuButton(
Button( Button(
onClick = onClick, onClick = onClick,
enabled = enabled,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent containerColor = Color.Transparent
), ),
@ -61,7 +64,7 @@ fun GakuButton(
.border(borderWidth, borderColor, shape), .border(borderWidth, borderColor, shape),
contentPadding = PaddingValues(0.dp) 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) @Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Composable @Composable
fun GakuButtonPreview() { 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)
} }

View File

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

View File

@ -34,7 +34,7 @@ fun GakuSwitch(modifier: Modifier = Modifier,
modifier = Modifier, modifier = Modifier,
colors = SwitchDefaults.colors( colors = SwitchDefaults.colors(
checkedThumbColor = Color(0xFFFFFFFF), checkedThumbColor = Color(0xFFFFFFFF),
checkedTrackColor = Color(0xFFF89400), checkedTrackColor = Color(0xFFF9C114),
uncheckedThumbColor = Color(0xFFFFFFFF), uncheckedThumbColor = Color(0xFFFFFFFF),
uncheckedTrackColor = Color(0xFFCFD8DC), uncheckedTrackColor = Color(0xFFCFD8DC),

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -29,6 +30,8 @@ fun CollapsibleBox(
viewModel: CollapsibleBoxViewModel = viewModel(), viewModel: CollapsibleBoxViewModel = viewModel(),
showExpand: Boolean = true, showExpand: Boolean = true,
expandState: Boolean? = null, expandState: Boolean? = null,
innerPaddingTopBottom: Dp = 0.dp,
innerPaddingLeftRight: Dp = 0.dp,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val expanded by viewModel::expanded val expanded by viewModel::expanded
@ -65,6 +68,8 @@ fun CollapsibleBox(
modifier = Modifier modifier = Modifier
.height(animatedHeight) .height(animatedHeight)
.fillMaxWidth() .fillMaxWidth()
.padding(start = innerPaddingLeftRight, end = innerPaddingLeftRight,
top = innerPaddingTopBottom, bottom = innerPaddingTopBottom)
// .fillMaxSize() // .fillMaxSize()
.clickable { .clickable {
if (!expanded && showExpand) { if (!expanded && showExpand) {

View File

@ -29,8 +29,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import io.github.chinosk.gakumas.localify.MainActivity import io.github.chinosk.gakumas.localify.MainActivity
import io.github.chinosk.gakumas.localify.R import io.github.chinosk.gakumas.localify.R
import io.github.chinosk.gakumas.localify.getConfigState import io.github.chinosk.gakumas.localify.getConfigState
import io.github.chinosk.gakumas.localify.models.CollapsibleBoxViewModel import io.github.chinosk.gakumas.localify.models.BreastCollapsibleBoxViewModel
import io.github.chinosk.gakumas.localify.models.CollapsibleBoxViewModelFactory import io.github.chinosk.gakumas.localify.models.BreastCollapsibleBoxViewModelFactory
import io.github.chinosk.gakumas.localify.models.GakumasConfig 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.base.CollapsibleBox
import io.github.chinosk.gakumas.localify.ui.components.GakuButton import io.github.chinosk.gakumas.localify.ui.components.GakuButton
@ -47,8 +47,8 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
val config = getConfigState(context, previewData) val config = getConfigState(context, previewData)
// val scrollState = rememberScrollState() // val scrollState = rememberScrollState()
val breastParamViewModel: CollapsibleBoxViewModel = val breastParamViewModel: BreastCollapsibleBoxViewModel =
viewModel(factory = CollapsibleBoxViewModelFactory(initiallyExpanded = false)) viewModel(factory = BreastCollapsibleBoxViewModelFactory(initiallyExpanded = false))
val keyBoardOptionsDecimal = remember { val keyBoardOptionsDecimal = remember {
KeyboardOptions(keyboardType = KeyboardType.Decimal) KeyboardOptions(keyboardType = KeyboardType.Decimal)
} }

View File

@ -2,6 +2,7 @@ package io.github.chinosk.gakumas.localify.ui.pages.subPages
import GakuGroupBox import GakuGroupBox
import android.content.res.Configuration.UI_MODE_NIGHT_NO 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -16,24 +17,39 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.MainActivity
import io.github.chinosk.gakumas.localify.R import io.github.chinosk.gakumas.localify.R
import io.github.chinosk.gakumas.localify.getConfigState 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.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.base.CollapsibleBox
import io.github.chinosk.gakumas.localify.ui.components.GakuButton 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.GakuRadio
import io.github.chinosk.gakumas.localify.ui.components.GakuSwitch import io.github.chinosk.gakumas.localify.ui.components.GakuSwitch
import io.github.chinosk.gakumas.localify.ui.components.GakuTextInput import io.github.chinosk.gakumas.localify.ui.components.GakuTextInput
import java.io.File
@Composable @Composable
@ -43,6 +59,13 @@ fun HomePage(modifier: Modifier = Modifier,
bottomSpacerHeight: Dp = 120.dp, bottomSpacerHeight: Dp = 120.dp,
screenH: Dp = 1080.dp) { screenH: Dp = 1080.dp) {
val config = getConfigState(context, previewData) 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 scrollState = rememberScrollState()
val keyboardOptionsNumber = remember { val keyboardOptionsNumber = remember {
KeyboardOptions(keyboardType = KeyboardType.Number) KeyboardOptions(keyboardType = KeyboardType.Number)
@ -51,6 +74,57 @@ fun HomePage(modifier: Modifier = Modifier,
KeyboardOptions(keyboardType = KeyboardType.Decimal) 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 LazyColumn(modifier = modifier
.sizeIn(maxHeight = screenH) .sizeIn(maxHeight = screenH)
@ -75,6 +149,138 @@ fun HomePage(modifier: Modifier = Modifier,
Spacer(Modifier.height(6.dp)) 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 { item {
GakuGroupBox(modifier = modifier, contentPadding = 0.dp, title = stringResource(R.string.graphic_settings)) { GakuGroupBox(modifier = modifier, contentPadding = 0.dp, title = stringResource(R.string.graphic_settings)) {
LazyColumn(modifier = Modifier 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 @Composable
fun HomePagePreview(modifier: Modifier = Modifier, data: GakumasConfig = GakumasConfig()) { fun HomePagePreview(modifier: Modifier = Modifier, data: GakumasConfig = GakumasConfig()) {
HomePage(modifier, previewData = data) HomePage(modifier, previewData = data)

View File

@ -63,6 +63,17 @@
<string name="plugin_code">插件本体</string> <string name="plugin_code">插件本体</string>
<string name="contributors">贡献者列表</string> <string name="contributors">贡献者列表</string>
<string name="translation_repository">译文仓库</string> <string name="translation_repository">译文仓库</string>
<string name="resource_settings">资源设置</string>
<string name="check_built_in_resource">检查内置数据更新</string>
<string name="delete_plugin_resource">清除游戏目录内的插件资源</string>
<string name="use_remote_zip_resource">使用远程 ZIP 数据</string>
<string name="resource_url">资源地址</string>
<string name="download">下载</string>
<string name="invalid_zip_file">文件解析失败</string>
<string name="invalid_zip_file_warn">此文件不是一个有效的 ZIP 翻译资源包</string>
<string name="cancel">取消</string>
<string name="downloaded_resource_version">已下载资源版本</string>
<string name="del_remote_after_update">替换文件后删除下载缓存</string>
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string> <string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
</resources> </resources>

View File

@ -63,6 +63,17 @@
<string name="plugin_code">Plugin Code</string> <string name="plugin_code">Plugin Code</string>
<string name="contributors">Contributors</string> <string name="contributors">Contributors</string>
<string name="translation_repository">Translation Repository</string> <string name="translation_repository">Translation Repository</string>
<string name="resource_settings">Resource Settings</string>
<string name="check_built_in_resource">Check Built-in Assets Update</string>
<string name="delete_plugin_resource">Delete Plugin Resource</string>
<string name="use_remote_zip_resource">Use Remote ZIP Resource</string>
<string name="resource_url">Resource URL</string>
<string name="download">Download</string>
<string name="invalid_zip_file">Invalid file</string>
<string name="invalid_zip_file_warn">This file is not a valid ZIP translation resource pack.</string>
<string name="cancel">Cancel</string>
<string name="downloaded_resource_version">Downloaded Version</string>
<string name="del_remote_after_update">Delete Cache File After Update</string>
<string name="about_contributors_asset_file">about_contributors_en.json</string> <string name="about_contributors_asset_file">about_contributors_en.json</string>
</resources> </resources>

View File

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