forked from MeloNX/MeloNX
android - improve game update selection
This commit is contained in:
parent
2240d87902
commit
14d6e280f7
@ -41,6 +41,9 @@ namespace LibRyujinx
|
|||||||
[DllImport("libryujinxjni")]
|
[DllImport("libryujinxjni")]
|
||||||
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
|
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
|
||||||
|
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
private extern static void pushString(string ch);
|
||||||
|
|
||||||
[DllImport("libryujinxjni")]
|
[DllImport("libryujinxjni")]
|
||||||
internal extern static void setRenderingThread();
|
internal extern static void setRenderingThread();
|
||||||
|
|
||||||
@ -511,11 +514,11 @@ namespace LibRyujinx
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetOpenedUser")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetOpenedUser")]
|
||||||
public static JStringLocalRef JniGetOpenedUser(JEnvRef jEnv, JObjectLocalRef jObj)
|
public static void JniGetOpenedUser(JEnvRef jEnv, JObjectLocalRef jObj)
|
||||||
{
|
{
|
||||||
var userId = GetOpenedUser();
|
var userId = GetOpenedUser();
|
||||||
|
|
||||||
return CreateString(jEnv, userId);
|
pushString(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserPicture")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserPicture")]
|
||||||
|
@ -728,6 +728,7 @@ namespace LibRyujinx
|
|||||||
{
|
{
|
||||||
VirtualFileSystem.ReloadKeySet();
|
VirtualFileSystem.ReloadKeySet();
|
||||||
ContentManager = new ContentManager(VirtualFileSystem);
|
ContentManager = new ContentManager(VirtualFileSystem);
|
||||||
|
AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void DisposeContext()
|
internal void DisposeContext()
|
||||||
|
@ -45,5 +45,6 @@ long _currentRenderingThreadId = 0;
|
|||||||
JavaVM* _vm = nullptr;
|
JavaVM* _vm = nullptr;
|
||||||
jobject _mainActivity = nullptr;
|
jobject _mainActivity = nullptr;
|
||||||
jclass _mainActivityClass = nullptr;
|
jclass _mainActivityClass = nullptr;
|
||||||
|
std::string _currentString = "";
|
||||||
|
|
||||||
#endif //RYUJINXNATIVE_RYUIJNX_H
|
#endif //RYUJINXNATIVE_RYUIJNX_H
|
||||||
|
@ -311,3 +311,25 @@ JNIEXPORT jstring JNICALL
|
|||||||
Java_org_ryujinx_android_NativeHelpers_getProgressInfo(JNIEnv *env, jobject thiz) {
|
Java_org_ryujinx_android_NativeHelpers_getProgressInfo(JNIEnv *env, jobject thiz) {
|
||||||
return createStringFromStdString(env, progressInfo);
|
return createStringFromStdString(env, progressInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_popStringJava(JNIEnv *env, jobject thiz) {
|
||||||
|
return createStringFromStdString(env, _currentString);
|
||||||
|
}
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_pushStringJava(JNIEnv *env, jobject thiz, jstring string) {
|
||||||
|
_currentString = getStringPointer(env, string);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
void pushString(char* str){
|
||||||
|
_currentString = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
const char* popString(){
|
||||||
|
return _currentString.c_str();
|
||||||
|
}
|
||||||
|
@ -354,6 +354,7 @@ class GameActivity : BaseActivity() {
|
|||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
|
showBackNotice.value = false
|
||||||
mainViewModel.closeGame()
|
mainViewModel.closeGame()
|
||||||
setFullScreen(false)
|
setFullScreen(false)
|
||||||
finishActivity(0)
|
finishActivity(0)
|
||||||
|
@ -9,7 +9,13 @@ import android.provider.DocumentsContract
|
|||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
|
import com.anggrayudi.storage.callback.FileCallback
|
||||||
|
import com.anggrayudi.storage.file.copyFileTo
|
||||||
import com.anggrayudi.storage.file.openInputStream
|
import com.anggrayudi.storage.file.openInputStream
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import net.lingala.zip4j.io.inputstream.ZipInputStream
|
import net.lingala.zip4j.io.inputstream.ZipInputStream
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -66,6 +72,61 @@ class Helpers {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
fun copyToData(
|
||||||
|
file: DocumentFile, path: String, storageHelper: SimpleStorageHelper,
|
||||||
|
isCopying: MutableState<Boolean>,
|
||||||
|
copyProgress: MutableState<Float>,
|
||||||
|
currentProgressName: MutableState<String>,
|
||||||
|
finish: () -> Unit
|
||||||
|
) {
|
||||||
|
var callback: FileCallback? = object : FileCallback() {
|
||||||
|
override fun onFailed(errorCode: FileCallback.ErrorCode) {
|
||||||
|
super.onFailed(errorCode)
|
||||||
|
File(path).delete()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart(file: Any, workerThread: Thread): Long {
|
||||||
|
copyProgress.value = 0f
|
||||||
|
|
||||||
|
(file as DocumentFile)?.apply {
|
||||||
|
currentProgressName.value = "Copying ${file.name}"
|
||||||
|
}
|
||||||
|
return super.onStart(file, workerThread)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReport(report: Report) {
|
||||||
|
super.onReport(report)
|
||||||
|
|
||||||
|
if(!isCopying.value) {
|
||||||
|
Thread.currentThread().interrupt()
|
||||||
|
}
|
||||||
|
|
||||||
|
copyProgress.value = report.progress / 100f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCompleted(result: Any) {
|
||||||
|
super.onCompleted(result)
|
||||||
|
isCopying.value = false
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val ioScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
isCopying.value = true
|
||||||
|
file.apply {
|
||||||
|
if (!File(path + "/${file.name}").exists()) {
|
||||||
|
val f = this
|
||||||
|
ioScope.launch {
|
||||||
|
f.copyFileTo(
|
||||||
|
storageHelper.storage.context,
|
||||||
|
File(path),
|
||||||
|
callback = callback!!
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getDataColumn(
|
private fun getDataColumn(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -28,4 +28,6 @@ class NativeHelpers {
|
|||||||
external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int
|
external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int
|
||||||
external fun getProgressInfo() : String
|
external fun getProgressInfo() : String
|
||||||
external fun getProgressValue() : Float
|
external fun getProgressValue() : Float
|
||||||
|
external fun pushStringJava(string: String)
|
||||||
|
external fun popStringJava() : String
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ class RyujinxNative {
|
|||||||
external fun deviceSignalEmulationClose()
|
external fun deviceSignalEmulationClose()
|
||||||
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
|
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
|
||||||
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String>
|
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String>
|
||||||
external fun userGetOpenedUser() : String
|
external fun userGetOpenedUser()
|
||||||
external fun userGetUserPicture(userId: String) : String
|
external fun userGetUserPicture(userId: String) : String
|
||||||
external fun userSetUserPicture(userId: String, picture: String)
|
external fun userSetUserPicture(userId: String, picture: String)
|
||||||
external fun userGetUserName(userId: String) : String
|
external fun userGetUserName(userId: String) : String
|
||||||
|
@ -68,7 +68,7 @@ class HomeViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadGameList() {
|
fun reloadGameList(ignoreCache: Boolean = false) {
|
||||||
var storage = activity?.storageHelper ?: return
|
var storage = activity?.storageHelper ?: return
|
||||||
|
|
||||||
if(isLoading)
|
if(isLoading)
|
||||||
@ -77,27 +77,32 @@ class HomeViewModel(
|
|||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
val files = mutableListOf<GameModel>()
|
if(!ignoreCache) {
|
||||||
|
val files = mutableListOf<GameModel>()
|
||||||
|
|
||||||
thread {
|
thread {
|
||||||
try {
|
try {
|
||||||
for (file in folder.search(false, DocumentFileType.FILE)) {
|
for (file in folder.search(false, DocumentFileType.FILE)) {
|
||||||
if (file.extension == "xci" || file.extension == "nsp")
|
if (file.extension == "xci" || file.extension == "nsp")
|
||||||
activity.let {
|
activity.let {
|
||||||
files.add(GameModel(file, it))
|
files.add(GameModel(file, it))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedCache = files.toList()
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
|
||||||
|
applyFilter()
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedCache = files.toList()
|
|
||||||
|
|
||||||
isLoading = false
|
|
||||||
|
|
||||||
applyFilter()
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
isLoading = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else{
|
||||||
|
isLoading = false
|
||||||
|
applyFilter()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyFilter() {
|
private fun applyFilter() {
|
||||||
@ -109,6 +114,10 @@ class HomeViewModel(
|
|||||||
|
|
||||||
fun setViewList(list: SnapshotStateList<GameModel>) {
|
fun setViewList(list: SnapshotStateList<GameModel>) {
|
||||||
gameList = list
|
gameList = list
|
||||||
reloadGameList()
|
reloadGameList(loadedCache.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLoadedCache(){
|
||||||
|
loadedCache = listOf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,6 +169,47 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearPptcCache(titleId :String){
|
||||||
|
if(titleId.isNotEmpty()){
|
||||||
|
val basePath = MainActivity.AppPath + "/games/$titleId/cache/cpu"
|
||||||
|
if(File(basePath).exists()){
|
||||||
|
var caches = mutableListOf<String>()
|
||||||
|
|
||||||
|
val mainCache = basePath + "${File.separator}0"
|
||||||
|
File(mainCache).listFiles()?.forEach {
|
||||||
|
if(it.isFile && it.name.endsWith(".cache"))
|
||||||
|
caches.add(it.absolutePath)
|
||||||
|
}
|
||||||
|
val backupCache = basePath + "${File.separator}1"
|
||||||
|
File(backupCache).listFiles()?.forEach {
|
||||||
|
if(it.isFile && it.name.endsWith(".cache"))
|
||||||
|
caches.add(it.absolutePath)
|
||||||
|
}
|
||||||
|
for(path in caches)
|
||||||
|
File(path).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun purgeShaderCache(titleId :String) {
|
||||||
|
if(titleId.isNotEmpty()){
|
||||||
|
val basePath = MainActivity.AppPath + "/games/$titleId/cache/shader"
|
||||||
|
if(File(basePath).exists()){
|
||||||
|
var caches = mutableListOf<String>()
|
||||||
|
File(basePath).listFiles()?.forEach {
|
||||||
|
if(!it.isFile)
|
||||||
|
it.delete()
|
||||||
|
else{
|
||||||
|
if(it.name.endsWith(".toc") || it.name.endsWith(".data"))
|
||||||
|
caches.add(it.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(path in caches)
|
||||||
|
File(path).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setStatStates(
|
fun setStatStates(
|
||||||
fifo: MutableState<Double>,
|
fifo: MutableState<Double>,
|
||||||
gameFps: MutableState<Double>,
|
gameFps: MutableState<Double>,
|
||||||
|
@ -4,27 +4,17 @@ import androidx.compose.runtime.MutableState
|
|||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.text.toLowerCase
|
import androidx.compose.ui.text.toLowerCase
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import com.anggrayudi.storage.callback.FileCallback
|
|
||||||
import com.anggrayudi.storage.file.DocumentFileCompat
|
|
||||||
import com.anggrayudi.storage.file.DocumentFileType
|
|
||||||
import com.anggrayudi.storage.file.copyFileTo
|
|
||||||
import com.anggrayudi.storage.file.getAbsolutePath
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import org.ryujinx.android.Helpers
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.LinkedList
|
|
||||||
import java.util.Queue
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class TitleUpdateViewModel(val titleId: String) {
|
class TitleUpdateViewModel(val titleId: String) {
|
||||||
|
private var canClose: MutableState<Boolean>? = null
|
||||||
private var basePath: String
|
private var basePath: String
|
||||||
private var updateJsonName = "updates.json"
|
private var updateJsonName = "updates.json"
|
||||||
private var stagingUpdateJsonName = "staging_updates.json"
|
|
||||||
private var storageHelper: SimpleStorageHelper
|
private var storageHelper: SimpleStorageHelper
|
||||||
var pathsState: SnapshotStateList<String>? = null
|
var pathsState: SnapshotStateList<String>? = null
|
||||||
|
|
||||||
@ -37,32 +27,37 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
return
|
return
|
||||||
|
|
||||||
data?.paths?.apply {
|
data?.paths?.apply {
|
||||||
removeAt(index - 1)
|
val removed = removeAt(index - 1)
|
||||||
|
File(removed).deleteRecursively()
|
||||||
pathsState?.clear()
|
pathsState?.clear()
|
||||||
pathsState?.addAll(this)
|
pathsState?.addAll(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Add() {
|
fun Add(
|
||||||
|
isCopying: MutableState<Boolean>,
|
||||||
|
copyProgress: MutableState<Float>,
|
||||||
|
currentProgressName: MutableState<String>
|
||||||
|
) {
|
||||||
val callBack = storageHelper.onFileSelected
|
val callBack = storageHelper.onFileSelected
|
||||||
|
|
||||||
storageHelper.onFileSelected = { requestCode, files ->
|
storageHelper.onFileSelected = { requestCode, files ->
|
||||||
run {
|
run {
|
||||||
storageHelper.onFileSelected = callBack
|
storageHelper.onFileSelected = callBack
|
||||||
if(requestCode == UpdateRequestCode)
|
if (requestCode == UpdateRequestCode) {
|
||||||
{
|
|
||||||
val file = files.firstOrNull()
|
val file = files.firstOrNull()
|
||||||
file?.apply {
|
file?.apply {
|
||||||
val path = file.getAbsolutePath(storageHelper.storage.context)
|
// Copy updates to internal data folder
|
||||||
if(path.isNotEmpty()){
|
val updatePath = "$basePath/update"
|
||||||
data?.apply {
|
File(updatePath).mkdirs()
|
||||||
if(!paths.contains(path)) {
|
Helpers.copyToData(
|
||||||
paths.add(path)
|
this,
|
||||||
pathsState?.clear()
|
updatePath,
|
||||||
pathsState?.addAll(paths)
|
storageHelper,
|
||||||
}
|
isCopying,
|
||||||
}
|
copyProgress,
|
||||||
}
|
currentProgressName, ::refreshPaths
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,128 +65,60 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
storageHelper.openFilePicker(UpdateRequestCode)
|
storageHelper.openFilePicker(UpdateRequestCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshPaths() {
|
||||||
|
data?.apply {
|
||||||
|
val updatePath = "$basePath/update"
|
||||||
|
val existingPaths = mutableListOf<String>()
|
||||||
|
File(updatePath).listFiles()?.forEach { existingPaths.add(it.absolutePath) }
|
||||||
|
|
||||||
|
if (!existingPaths.contains(selected)) {
|
||||||
|
selected = ""
|
||||||
|
}
|
||||||
|
pathsState?.clear()
|
||||||
|
pathsState?.addAll(existingPaths)
|
||||||
|
paths = existingPaths
|
||||||
|
canClose?.apply {
|
||||||
|
value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun save(
|
fun save(
|
||||||
index: Int,
|
index: Int,
|
||||||
isCopying: MutableState<Boolean>,
|
openDialog: MutableState<Boolean>
|
||||||
openDialog: MutableState<Boolean>,
|
|
||||||
copyProgress: MutableState<Float>,
|
|
||||||
currentProgressName: MutableState<String>
|
|
||||||
) {
|
) {
|
||||||
data?.apply {
|
data?.apply {
|
||||||
|
val updatePath = "$basePath/update"
|
||||||
this.selected = ""
|
this.selected = ""
|
||||||
if (paths.isNotEmpty() && index > 0) {
|
if (paths.isNotEmpty() && index > 0) {
|
||||||
val ind = max(index - 1, paths.count() - 1)
|
val ind = max(index - 1, paths.count() - 1)
|
||||||
this.selected = paths[ind]
|
this.selected = paths[ind]
|
||||||
}
|
}
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
var json = gson.toJson(this)
|
|
||||||
File(basePath).mkdirs()
|
File(basePath).mkdirs()
|
||||||
File("$basePath/$stagingUpdateJsonName").writeText(json)
|
|
||||||
|
|
||||||
// Copy updates to internal data folder
|
|
||||||
val updatePath = "$basePath/update"
|
|
||||||
File(updatePath).mkdirs()
|
|
||||||
|
|
||||||
val ioScope = CoroutineScope(Dispatchers.IO)
|
|
||||||
|
|
||||||
var metadata = TitleUpdateMetadata()
|
var metadata = TitleUpdateMetadata()
|
||||||
var queue: Queue<String> = LinkedList()
|
val savedUpdates = mutableListOf<String>()
|
||||||
|
File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
|
||||||
|
metadata.paths = savedUpdates
|
||||||
|
|
||||||
var callback: FileCallback? = null
|
val selectedName = File(selected).name
|
||||||
|
val newSelectedPath = "$updatePath/$selectedName"
|
||||||
fun copy(path: String) {
|
if (File(newSelectedPath).exists()) {
|
||||||
isCopying.value = true
|
metadata.selected = newSelectedPath
|
||||||
val documentFile = DocumentFileCompat.fromFullPath(
|
|
||||||
storageHelper.storage.context,
|
|
||||||
path,
|
|
||||||
DocumentFileType.FILE
|
|
||||||
)
|
|
||||||
documentFile?.apply {
|
|
||||||
val stagedPath = "$basePath/${name}"
|
|
||||||
if (!File(stagedPath).exists()) {
|
|
||||||
var file = this
|
|
||||||
ioScope.launch {
|
|
||||||
file.copyFileTo(
|
|
||||||
storageHelper.storage.context,
|
|
||||||
File(updatePath),
|
|
||||||
callback = callback!!
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata.paths.add(stagedPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun finish() {
|
var json = gson.toJson(metadata)
|
||||||
val savedUpdates = mutableListOf<String>()
|
File("$basePath/$updateJsonName").writeText(json)
|
||||||
File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
|
|
||||||
var missingFiles =
|
|
||||||
savedUpdates.filter { i -> paths.find { it.endsWith(File(i).name) } == null }
|
|
||||||
for (path in missingFiles) {
|
|
||||||
File(path).delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
val selectedName = File(selected).name
|
openDialog.value = false
|
||||||
val newSelectedPath = "$updatePath/$selectedName"
|
|
||||||
if (File(newSelectedPath).exists()) {
|
|
||||||
metadata.selected = newSelectedPath
|
|
||||||
}
|
|
||||||
|
|
||||||
json = gson.toJson(metadata)
|
|
||||||
File("$basePath/$updateJsonName").writeText(json)
|
|
||||||
|
|
||||||
openDialog.value = false
|
|
||||||
isCopying.value = false
|
|
||||||
}
|
|
||||||
callback = object : FileCallback() {
|
|
||||||
override fun onFailed(errorCode: FileCallback.ErrorCode) {
|
|
||||||
super.onFailed(errorCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart(file: Any, workerThread: Thread): Long {
|
|
||||||
copyProgress.value = 0f
|
|
||||||
|
|
||||||
(file as DocumentFile)?.apply {
|
|
||||||
currentProgressName.value = "Copying ${file.name}"
|
|
||||||
}
|
|
||||||
return super.onStart(file, workerThread)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReport(report: Report) {
|
|
||||||
super.onReport(report)
|
|
||||||
|
|
||||||
copyProgress.value = report.progress / 100f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCompleted(result: Any) {
|
|
||||||
super.onCompleted(result)
|
|
||||||
|
|
||||||
if (queue.isNotEmpty())
|
|
||||||
copy(queue.remove())
|
|
||||||
else {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (path in paths) {
|
|
||||||
queue.add(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
ioScope.launch {
|
|
||||||
if (queue.isNotEmpty()) {
|
|
||||||
copy(queue.remove())
|
|
||||||
} else {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPaths(paths: SnapshotStateList<String>) {
|
fun setPaths(paths: SnapshotStateList<String>, canClose: MutableState<Boolean>) {
|
||||||
pathsState = paths
|
pathsState = paths
|
||||||
|
this.canClose = canClose
|
||||||
data?.apply {
|
data?.apply {
|
||||||
pathsState?.clear()
|
pathsState?.clear()
|
||||||
pathsState?.addAll(this.paths)
|
pathsState?.addAll(this.paths)
|
||||||
@ -203,29 +130,14 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
|
basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
|
||||||
val stagingJson = "${basePath}/${stagingUpdateJsonName}"
|
|
||||||
jsonPath = "${basePath}/${updateJsonName}"
|
jsonPath = "${basePath}/${updateJsonName}"
|
||||||
|
|
||||||
data = TitleUpdateMetadata()
|
data = TitleUpdateMetadata()
|
||||||
if (File(stagingJson).exists()) {
|
if (File(jsonPath).exists()) {
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
data = gson.fromJson(File(stagingJson).readText(), TitleUpdateMetadata::class.java)
|
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
|
||||||
|
|
||||||
data?.apply {
|
refreshPaths()
|
||||||
val existingPaths = mutableListOf<String>()
|
|
||||||
for (path in paths) {
|
|
||||||
if (File(path).exists()) {
|
|
||||||
existingPaths.add(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!existingPaths.contains(selected)){
|
|
||||||
selected = ""
|
|
||||||
}
|
|
||||||
pathsState?.clear()
|
|
||||||
pathsState?.addAll(existingPaths)
|
|
||||||
paths = existingPaths
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
storageHelper = MainActivity.StorageHelper!!
|
storageHelper = MainActivity.StorageHelper!!
|
||||||
|
@ -61,6 +61,7 @@ import androidx.navigation.NavHostController
|
|||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.anggrayudi.storage.extension.launchOnUiThread
|
import com.anggrayudi.storage.extension.launchOnUiThread
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
|
import org.ryujinx.android.NativeHelpers
|
||||||
import org.ryujinx.android.RyujinxNative
|
import org.ryujinx.android.RyujinxNative
|
||||||
import org.ryujinx.android.viewmodels.GameModel
|
import org.ryujinx.android.viewmodels.GameModel
|
||||||
import org.ryujinx.android.viewmodels.HomeViewModel
|
import org.ryujinx.android.viewmodels.HomeViewModel
|
||||||
@ -84,6 +85,7 @@ class HomeViews {
|
|||||||
val showAppActions = remember { mutableStateOf(false) }
|
val showAppActions = remember { mutableStateOf(false) }
|
||||||
val showLoading = remember { mutableStateOf(false) }
|
val showLoading = remember { mutableStateOf(false) }
|
||||||
val openTitleUpdateDialog = remember { mutableStateOf(false) }
|
val openTitleUpdateDialog = remember { mutableStateOf(false) }
|
||||||
|
val canClose = remember { mutableStateOf(true) }
|
||||||
val openDlcDialog = remember { mutableStateOf(false) }
|
val openDlcDialog = remember { mutableStateOf(false) }
|
||||||
val query = remember {
|
val query = remember {
|
||||||
mutableStateOf("")
|
mutableStateOf("")
|
||||||
@ -102,10 +104,11 @@ class HomeViews {
|
|||||||
val pic = remember {
|
val pic = remember {
|
||||||
mutableStateOf(ByteArray(0))
|
mutableStateOf(ByteArray(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
if(refreshUser.value){
|
if (refreshUser.value) {
|
||||||
user.value = native.userGetOpenedUser()
|
native.userGetOpenedUser()
|
||||||
if(user.value.isNotEmpty()) {
|
user.value = NativeHelpers().popStringJava()
|
||||||
|
if (user.value.isNotEmpty()) {
|
||||||
val decoder = Base64.getDecoder()
|
val decoder = Base64.getDecoder()
|
||||||
pic.value = decoder.decode(native.userGetUserPicture(user.value))
|
pic.value = decoder.decode(native.userGetUserPicture(user.value))
|
||||||
}
|
}
|
||||||
@ -145,7 +148,7 @@ class HomeViews {
|
|||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
navController?.navigate("user")
|
navController?.navigate("user")
|
||||||
}) {
|
}) {
|
||||||
if(pic.value.isNotEmpty()) {
|
if (pic.value.isNotEmpty()) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = BitmapFactory.decodeByteArray(
|
bitmap = BitmapFactory.decodeByteArray(
|
||||||
pic.value,
|
pic.value,
|
||||||
@ -160,8 +163,7 @@ class HomeViews {
|
|||||||
.size(52.dp)
|
.size(52.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
)
|
)
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Person,
|
Icons.Filled.Person,
|
||||||
contentDescription = "user"
|
contentDescription = "user"
|
||||||
@ -204,29 +206,29 @@ class HomeViews {
|
|||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = showAppMenu.value,
|
expanded = showAppMenu.value,
|
||||||
onDismissRequest = { showAppMenu.value = false }) {
|
onDismissRequest = { showAppMenu.value = false }) {
|
||||||
|
DropdownMenuItem(text = {
|
||||||
|
Text(text = "Clear PPTC Cache")
|
||||||
|
}, onClick = {
|
||||||
|
showAppMenu.value = false
|
||||||
|
viewModel.mainViewModel?.clearPptcCache(viewModel.mainViewModel?.selected?.titleId ?: "")
|
||||||
|
})
|
||||||
|
DropdownMenuItem(text = {
|
||||||
|
Text(text = "Purge Shader Cache")
|
||||||
|
}, onClick = {
|
||||||
|
showAppMenu.value = false
|
||||||
|
viewModel.mainViewModel?.purgeShaderCache(viewModel.mainViewModel?.selected?.titleId ?: "")
|
||||||
|
})
|
||||||
DropdownMenuItem(text = {
|
DropdownMenuItem(text = {
|
||||||
Text(text = "Manage Updates")
|
Text(text = "Manage Updates")
|
||||||
}, onClick = {
|
}, onClick = {
|
||||||
showAppMenu.value = false
|
showAppMenu.value = false
|
||||||
openTitleUpdateDialog.value = true
|
openTitleUpdateDialog.value = true
|
||||||
}, leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = org.ryujinx.android.Icons.gameUpdate(),
|
|
||||||
contentDescription = "Updates",
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
DropdownMenuItem(text = {
|
DropdownMenuItem(text = {
|
||||||
Text(text = "Manage DLC")
|
Text(text = "Manage DLC")
|
||||||
}, onClick = {
|
}, onClick = {
|
||||||
showAppMenu.value = false
|
showAppMenu.value = false
|
||||||
openDlcDialog.value = true
|
openDlcDialog.value = true
|
||||||
}, leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = org.ryujinx.android.Icons.download(),
|
|
||||||
contentDescription = "Dlc",
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -255,11 +257,10 @@ class HomeViews {
|
|||||||
val list = remember {
|
val list = remember {
|
||||||
mutableStateListOf<GameModel>()
|
mutableStateListOf<GameModel>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (refresh.value) {
|
if (refresh.value) {
|
||||||
viewModel.setViewList(list)
|
viewModel.setViewList(list)
|
||||||
refresh.value = false
|
refresh.value = false
|
||||||
|
showAppActions.value = false
|
||||||
}
|
}
|
||||||
val selectedModel = remember {
|
val selectedModel = remember {
|
||||||
mutableStateOf(viewModel.mainViewModel?.selected)
|
mutableStateOf(viewModel.mainViewModel?.selected)
|
||||||
@ -323,7 +324,7 @@ class HomeViews {
|
|||||||
) {
|
) {
|
||||||
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
|
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
|
||||||
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
|
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
|
||||||
TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog)
|
TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog, canClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -325,6 +325,8 @@ class SettingViews {
|
|||||||
AlertDialog(onDismissRequest = {
|
AlertDialog(onDismissRequest = {
|
||||||
showImportCompletion.value = false
|
showImportCompletion.value = false
|
||||||
importFile.value = null
|
importFile.value = null
|
||||||
|
mainViewModel.requestUserRefresh()
|
||||||
|
mainViewModel.homeViewModel.clearLoadedCache()
|
||||||
}) {
|
}) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
|
@ -27,11 +27,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.ryujinx.android.viewmodels.TitleUpdateViewModel
|
import org.ryujinx.android.viewmodels.TitleUpdateViewModel
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class TitleUpdateViews {
|
class TitleUpdateViews {
|
||||||
companion object {
|
companion object {
|
||||||
@Composable
|
@Composable
|
||||||
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>) {
|
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>, canClose: MutableState<Boolean>) {
|
||||||
val viewModel = TitleUpdateViewModel(titleId)
|
val viewModel = TitleUpdateViewModel(titleId)
|
||||||
|
|
||||||
val selected = remember { mutableStateOf(0) }
|
val selected = remember { mutableStateOf(0) }
|
||||||
@ -46,6 +47,9 @@ class TitleUpdateViews {
|
|||||||
val copyProgress = remember {
|
val copyProgress = remember {
|
||||||
mutableStateOf(0.0f)
|
mutableStateOf(0.0f)
|
||||||
}
|
}
|
||||||
|
var currentProgressName = remember {
|
||||||
|
mutableStateOf("Starting Copy")
|
||||||
|
}
|
||||||
Column {
|
Column {
|
||||||
Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
|
Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
|
||||||
Surface(
|
Surface(
|
||||||
@ -77,7 +81,7 @@ class TitleUpdateViews {
|
|||||||
mutableStateListOf<String>()
|
mutableStateListOf<String>()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.setPaths(paths)
|
viewModel.setPaths(paths, canClose)
|
||||||
var index = 1
|
var index = 1
|
||||||
for (path in paths) {
|
for (path in paths) {
|
||||||
val i = index
|
val i = index
|
||||||
@ -86,7 +90,7 @@ class TitleUpdateViews {
|
|||||||
selected = (selected.value == i),
|
selected = (selected.value == i),
|
||||||
onClick = { selected.value = i })
|
onClick = { selected.value = i })
|
||||||
Text(
|
Text(
|
||||||
text = path,
|
text = File(path).name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
@ -111,7 +115,7 @@ class TitleUpdateViews {
|
|||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.Add()
|
viewModel.Add(isCopying, copyProgress, currentProgressName)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -122,22 +126,33 @@ class TitleUpdateViews {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
var currentProgressName = remember {
|
|
||||||
mutableStateOf("Starting Copy")
|
|
||||||
}
|
|
||||||
if (isCopying.value) {
|
if (isCopying.value) {
|
||||||
Text(text = "Copying updates to local storage")
|
Text(text = "Copying updates to local storage")
|
||||||
Text(text = currentProgressName.value)
|
Text(text = currentProgressName.value)
|
||||||
LinearProgressIndicator(
|
Row {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
LinearProgressIndicator(
|
||||||
progress = copyProgress.value
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
progress = copyProgress.value
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
isCopying.value = false
|
||||||
|
canClose.value = true
|
||||||
|
viewModel.refreshPaths()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(18.dp))
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.align(Alignment.End),
|
modifier = Modifier.align(Alignment.End),
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.save(selected.value, isCopying, openDialog, copyProgress, currentProgressName)
|
if (!isCopying.value) {
|
||||||
|
canClose.value = true
|
||||||
|
viewModel.save(selected.value, openDialog)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
|
@ -36,6 +36,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
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.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import org.ryujinx.android.NativeHelpers
|
||||||
import org.ryujinx.android.RyujinxNative
|
import org.ryujinx.android.RyujinxNative
|
||||||
import org.ryujinx.android.viewmodels.MainViewModel
|
import org.ryujinx.android.viewmodels.MainViewModel
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
@ -47,8 +48,9 @@ class UserViews {
|
|||||||
fun Main(viewModel: MainViewModel? = null, navController: NavHostController? = null) {
|
fun Main(viewModel: MainViewModel? = null, navController: NavHostController? = null) {
|
||||||
val ryujinxNative = RyujinxNative()
|
val ryujinxNative = RyujinxNative()
|
||||||
val decoder = Base64.getDecoder()
|
val decoder = Base64.getDecoder()
|
||||||
|
ryujinxNative.userGetOpenedUser()
|
||||||
val openedUser = remember {
|
val openedUser = remember {
|
||||||
mutableStateOf(ryujinxNative.userGetOpenedUser())
|
mutableStateOf(NativeHelpers().popStringJava())
|
||||||
}
|
}
|
||||||
|
|
||||||
val openedUserPic = remember {
|
val openedUserPic = remember {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user