Feature: Hot Update Translation (#32)
* update translation data from repo
This commit is contained in:
parent
91dea41ca2
commit
dbb7c8d8f6
@ -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'
|
||||
|
@ -7,6 +7,7 @@
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@ -42,6 +43,16 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</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>
|
||||
|
||||
</manifest>
|
@ -1 +1 @@
|
||||
Subproject commit cdd0ad064cf6d3f13107e19b5d08c582d8d0664e
|
||||
Subproject commit a60a171b40b22b04d567ab39a8fd7f571c7921f5
|
@ -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<void (*)(void* self, Il2cppString* path)>(
|
||||
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*>();
|
||||
Font_ctor->Invoke<void>(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<void*>(TMP_Textself);
|
||||
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);
|
||||
if (!updatedFontPtrs.contains(fontAsset)) {
|
||||
updatedFontPtrs.emplace(fontAsset);
|
||||
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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<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() {
|
||||
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<String> {
|
||||
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<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 {
|
||||
override lateinit var binding: ActivityMainBinding
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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("{}")
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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 <T : ViewModel> create(modelClass: Class<T>): 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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),
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -63,6 +63,17 @@
|
||||
<string name="plugin_code">插件本体</string>
|
||||
<string name="contributors">贡献者列表</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>
|
||||
</resources>
|
@ -63,6 +63,17 @@
|
||||
<string name="plugin_code">Plugin Code</string>
|
||||
<string name="contributors">Contributors</string>
|
||||
<string name="translation_repository">Translation Repository</string>
|
||||
<string name="resource_settings">Resource Settings</string>
|
||||
<string name="check_built_in_resource">Check Built-in Assets Update</string>
|
||||
<string name="delete_plugin_resource">Delete Plugin Resource</string>
|
||||
<string name="use_remote_zip_resource">Use Remote ZIP Resource</string>
|
||||
<string name="resource_url">Resource URL</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="invalid_zip_file">Invalid file</string>
|
||||
<string name="invalid_zip_file_warn">This file is not a valid ZIP translation resource pack.</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="downloaded_resource_version">Downloaded Version</string>
|
||||
<string name="del_remote_after_update">Delete Cache File After Update</string>
|
||||
|
||||
<string name="about_contributors_asset_file">about_contributors_en.json</string>
|
||||
</resources>
|
4
app/src/main/res/xml/file_paths.xml
Normal file
4
app/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<files-path name="files" path="." />
|
||||
</paths>
|
Loading…
x
Reference in New Issue
Block a user