mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2025-03-13 09:12:27 +01:00
Merge de5bb69cd5c9d0ee93dcfd7401d923b4832ab348 into 26ce7e4f2844a445bf77b4b14977d62e6434df08
This commit is contained in:
commit
c9f9cc939b
src/android/app/src/main
java/org/citra/citra_emu
res
@ -5,6 +5,7 @@
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.graphics.drawable.Icon
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import android.text.TextUtils
|
||||
@ -28,6 +29,7 @@ import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.widget.PopupMenu
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.button.MaterialButton
|
||||
@ -44,7 +46,9 @@ import org.citra.citra_emu.databinding.CardGameBinding
|
||||
import org.citra.citra_emu.features.cheats.ui.CheatsFragmentDirections
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import org.citra.citra_emu.fragments.IndeterminateProgressDialogFragment
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.utils.FileUtil
|
||||
import org.citra.citra_emu.utils.GameIconUtils
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
|
||||
@ -203,6 +207,121 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
||||
}
|
||||
}
|
||||
|
||||
private data class GameDirectories(
|
||||
val gameDir: String,
|
||||
val saveDir: String,
|
||||
val modsDir: String,
|
||||
val texturesDir: String,
|
||||
val appDir: String,
|
||||
val dlcDir: String,
|
||||
val updatesDir: String,
|
||||
val extraDir: String
|
||||
)
|
||||
private fun getGameDirectories(game: Game): GameDirectories {
|
||||
return GameDirectories(
|
||||
gameDir = game.path.substringBeforeLast("/"),
|
||||
saveDir = "sdmc/Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/${String.format("%016x", game.titleId).lowercase().substring(0, 8)}/${String.format("%016x", game.titleId).lowercase().substring(8)}/data/00000001",
|
||||
modsDir = "load/mods/${String.format("%016X", game.titleId)}",
|
||||
texturesDir = "load/textures/${String.format("%016X", game.titleId)}",
|
||||
appDir = game.path.substringBeforeLast("/").split("/").filter { it.isNotEmpty() }.joinToString("/"),
|
||||
dlcDir = "sdmc/Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/0004008c/${String.format("%016x", game.titleId).lowercase().substring(8)}/content",
|
||||
updatesDir = "sdmc/Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/0004000e/${String.format("%016x", game.titleId).lowercase().substring(8)}/content",
|
||||
extraDir = "sdmc/Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/extdata/00000000/${String.format("%016X", game.titleId).substring(8, 14).padStart(8, '0')}"
|
||||
)
|
||||
}
|
||||
|
||||
private fun showOpenContextMenu(view: View, game: Game) {
|
||||
val dirs = getGameDirectories(game)
|
||||
|
||||
val popup = PopupMenu(view.context, view).apply {
|
||||
menuInflater.inflate(R.menu.game_context_menu_open, menu)
|
||||
listOf(
|
||||
R.id.game_context_open_app to dirs.appDir,
|
||||
R.id.game_context_open_save_dir to dirs.saveDir,
|
||||
R.id.game_context_open_dlc to dirs.dlcDir,
|
||||
R.id.game_context_open_updates to dirs.updatesDir
|
||||
).forEach { (id, dir) ->
|
||||
menu.findItem(id)?.isEnabled =
|
||||
CitraApplication.documentsTree.folderUriHelper(dir)?.let {
|
||||
DocumentFile.fromTreeUri(view.context, it)?.exists()
|
||||
} ?: false
|
||||
}
|
||||
menu.findItem(R.id.game_context_open_extra)?.let { item ->
|
||||
if (CitraApplication.documentsTree.folderUriHelper(dirs.extraDir)?.let {
|
||||
DocumentFile.fromTreeUri(view.context, it)?.exists()
|
||||
} != true) {
|
||||
menu.removeItem(item.itemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setType("*/*")
|
||||
|
||||
val uri = when (menuItem.itemId) {
|
||||
R.id.game_context_open_app -> CitraApplication.documentsTree.folderUriHelper(dirs.appDir)
|
||||
R.id.game_context_open_save_dir -> CitraApplication.documentsTree.folderUriHelper(dirs.saveDir)
|
||||
R.id.game_context_open_dlc -> CitraApplication.documentsTree.folderUriHelper(dirs.dlcDir)
|
||||
R.id.game_context_open_textures -> CitraApplication.documentsTree.folderUriHelper(dirs.texturesDir, true)
|
||||
R.id.game_context_open_mods -> CitraApplication.documentsTree.folderUriHelper(dirs.modsDir, true)
|
||||
R.id.game_context_open_extra -> CitraApplication.documentsTree.folderUriHelper(dirs.extraDir)
|
||||
else -> null
|
||||
}
|
||||
|
||||
uri?.let {
|
||||
intent.data = it
|
||||
view.context.startActivity(intent)
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
|
||||
private fun showUninstallContextMenu(view: View, game: Game, bottomSheetDialog: BottomSheetDialog) {
|
||||
val dirs = getGameDirectories(game)
|
||||
val popup = PopupMenu(view.context, view).apply {
|
||||
menuInflater.inflate(R.menu.game_context_menu_uninstall, menu)
|
||||
listOf(
|
||||
R.id.game_context_uninstall to dirs.gameDir,
|
||||
R.id.game_context_uninstall_dlc to dirs.dlcDir,
|
||||
R.id.game_context_uninstall_updates to dirs.updatesDir
|
||||
).forEach { (id, dir) ->
|
||||
menu.findItem(id)?.isEnabled =
|
||||
CitraApplication.documentsTree.folderUriHelper(dir)?.let {
|
||||
DocumentFile.fromTreeUri(view.context, it)?.exists()
|
||||
} ?: false
|
||||
}
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
val uninstallAction: () -> Unit = {
|
||||
when (menuItem.itemId) {
|
||||
R.id.game_context_uninstall -> CitraApplication.documentsTree.deleteDocument(dirs.gameDir)
|
||||
R.id.game_context_uninstall_dlc -> FileUtil.deleteDocument(CitraApplication.documentsTree.folderUriHelper(dirs.dlcDir)
|
||||
.toString())
|
||||
R.id.game_context_uninstall_updates -> FileUtil.deleteDocument(CitraApplication.documentsTree.folderUriHelper(dirs.updatesDir)
|
||||
.toString())
|
||||
}
|
||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
||||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
|
||||
if (menuItem.itemId in listOf(R.id.game_context_uninstall, R.id.game_context_uninstall_dlc, R.id.game_context_uninstall_updates)) {
|
||||
IndeterminateProgressDialogFragment.newInstance(activity, R.string.uninstalling, false, uninstallAction)
|
||||
.show(activity.supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
|
||||
private fun showAboutGameDialog(context: Context, game: Game, holder: GameViewHolder, view: View) {
|
||||
val bottomSheetView = inflater.inflate(R.layout.dialog_about_game, null)
|
||||
|
||||
@ -245,6 +364,14 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
||||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
|
||||
bottomSheetView.findViewById<MaterialButton>(R.id.menu_button_open).setOnClickListener {
|
||||
showOpenContextMenu(it, game)
|
||||
}
|
||||
|
||||
bottomSheetView.findViewById<MaterialButton>(R.id.menu_button_uninstall).setOnClickListener {
|
||||
showUninstallContextMenu(it, game, bottomSheetDialog)
|
||||
}
|
||||
|
||||
val bottomSheetBehavior = bottomSheetDialog.getBehavior()
|
||||
bottomSheetBehavior.skipCollapsed = true
|
||||
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
|
@ -106,6 +106,40 @@ class DocumentsTree {
|
||||
return node.uri ?: return Uri.EMPTY
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun folderUriHelper(path: String, createIfNotExists: Boolean = false): Uri? {
|
||||
root ?: return null
|
||||
val components = path.split(DELIMITER).filter { it.isNotEmpty() }
|
||||
var current = root
|
||||
|
||||
for (component in components) {
|
||||
if (!current!!.loaded) {
|
||||
structTree(current)
|
||||
}
|
||||
|
||||
var child = current.findChild(component)
|
||||
|
||||
// Create directory if it doesn't exist and creation is enabled
|
||||
if (child == null && createIfNotExists) {
|
||||
try {
|
||||
val createdDir = FileUtil.createDir(current.uri.toString(), component) ?: return null
|
||||
child = DocumentsNode(createdDir, true).apply {
|
||||
parent = current
|
||||
}
|
||||
current.addChild(child)
|
||||
} catch (e: Exception) {
|
||||
error("[DocumentsTree]: Cannot create directory, error: " + e.message)
|
||||
return null
|
||||
}
|
||||
} else if (child == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
current = child
|
||||
}
|
||||
return current?.uri
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun isDirectory(filepath: String): Boolean {
|
||||
val node = resolvePath(filepath) ?: return false
|
||||
|
10
src/android/app/src/main/res/drawable/ic_open.xml
Normal file
10
src/android/app/src/main/res/drawable/ic_open.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M20,6h-8l-2,-2H4C2.89,4 2,4.89 2,6v12c0,1.1 0.89,2 2,2h16c1.1,0 2,-0.9 2,-2V8C22,6.89 21.1,6 20,6zM19,18H5V8h14v10zM12,9l-4,4h3v3h2v-3h3L12,9z"/>
|
||||
</vector>
|
10
src/android/app/src/main/res/drawable/ic_uninstall.xml
Normal file
10
src/android/app/src/main/res/drawable/ic_uninstall.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
@ -148,6 +148,25 @@
|
||||
android:contentDescription="@string/cheats"
|
||||
android:text="@string/cheats" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/menu_button_open"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||
android:layout_width="62dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:icon="@drawable/ic_open"
|
||||
app:iconGravity="textStart" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/menu_button_uninstall"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||
android:layout_width="62dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:icon="@drawable/ic_uninstall"
|
||||
app:iconGravity="textStart" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
24
src/android/app/src/main/res/menu/game_context_menu_open.xml
Normal file
24
src/android/app/src/main/res/menu/game_context_menu_open.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/game_context_open_app"
|
||||
android:title="@string/game_context_open_app" />
|
||||
<item
|
||||
android:id="@+id/game_context_open_save_dir"
|
||||
android:title="@string/game_context_open_save_dir" />
|
||||
<item
|
||||
android:id="@+id/game_context_open_updates"
|
||||
android:title="@string/game_context_open_updates" />
|
||||
<item
|
||||
android:id="@+id/game_context_open_dlc"
|
||||
android:title="@string/game_context_open_dlc" />
|
||||
<item
|
||||
android:id="@+id/game_context_open_extra"
|
||||
android:title="@string/game_context_open_extra" />
|
||||
<item
|
||||
android:id="@+id/game_context_open_textures"
|
||||
android:title="@string/game_context_open_textures" />
|
||||
<item
|
||||
android:id="@+id/game_context_open_mods"
|
||||
android:title="@string/game_context_open_mods" />
|
||||
</menu>
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/game_context_uninstall"
|
||||
android:title="@string/uninstall_cia" />
|
||||
<item
|
||||
android:id="@+id/game_context_uninstall_dlc"
|
||||
android:title="@string/game_context_uninstall_dlc" />
|
||||
<item
|
||||
android:id="@+id/game_context_uninstall_updates"
|
||||
android:title="@string/game_context_uninstall_updates" />
|
||||
</menu>
|
@ -477,6 +477,18 @@
|
||||
<!-- About Game Dialog -->
|
||||
<string name="play">Play</string>
|
||||
<string name="shortcut">Shortcut</string>
|
||||
<string name="uninstall_cia">Uninstall Game</string>
|
||||
<string name="uninstalling">Uninstalling...</string>
|
||||
<string name="game_context_open_save_dir">Open Save Data Folder</string>
|
||||
<string name="game_context_open_app">Open Application Folder</string>
|
||||
<string name="game_context_open_mods">Open Mods Folder</string>
|
||||
<string name="game_context_open_textures">Open Textures Folder</string>
|
||||
<string name="game_context_open_dlc">Open DLC Folder</string>
|
||||
<string name="game_context_open_updates">Open Updates Folder</string>
|
||||
<string name="game_context_open_extra">Open Extra Folder</string>
|
||||
<string name="game_context_uninstall_dlc">Uninstall DLC</string>
|
||||
<string name="game_context_uninstall_updates">Uninstall Updates</string>
|
||||
|
||||
|
||||
<!-- Cheats -->
|
||||
<string name="cheats">Cheats</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user