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
|
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'
|
||||||
|
@ -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
|
@ -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);
|
||||||
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
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("{}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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.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()
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
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),
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
@ -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>
|
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