mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2025-03-13 17:22:30 +01:00
Merge de5bb69cd5c9d0ee93dcfd7401d923b4832ab348 into 26ce7e4f2844a445bf77b4b14977d62e6434df08
This commit is contained in:
commit
c9f9cc939b
@ -5,6 +5,7 @@
|
|||||||
package org.citra.citra_emu.adapters
|
package org.citra.citra_emu.adapters
|
||||||
|
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
@ -28,6 +29,7 @@ import androidx.recyclerview.widget.AsyncDifferConfig
|
|||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import android.widget.PopupMenu
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.button.MaterialButton
|
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.cheats.ui.CheatsFragmentDirections
|
||||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
||||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
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.model.Game
|
||||||
|
import org.citra.citra_emu.utils.FileUtil
|
||||||
import org.citra.citra_emu.utils.GameIconUtils
|
import org.citra.citra_emu.utils.GameIconUtils
|
||||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
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) {
|
private fun showAboutGameDialog(context: Context, game: Game, holder: GameViewHolder, view: View) {
|
||||||
val bottomSheetView = inflater.inflate(R.layout.dialog_about_game, null)
|
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()
|
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()
|
val bottomSheetBehavior = bottomSheetDialog.getBehavior()
|
||||||
bottomSheetBehavior.skipCollapsed = true
|
bottomSheetBehavior.skipCollapsed = true
|
||||||
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
@ -106,6 +106,40 @@ class DocumentsTree {
|
|||||||
return node.uri ?: return Uri.EMPTY
|
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
|
@Synchronized
|
||||||
fun isDirectory(filepath: String): Boolean {
|
fun isDirectory(filepath: String): Boolean {
|
||||||
val node = resolvePath(filepath) ?: return false
|
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:contentDescription="@string/cheats"
|
||||||
android:text="@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>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</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 -->
|
<!-- About Game Dialog -->
|
||||||
<string name="play">Play</string>
|
<string name="play">Play</string>
|
||||||
<string name="shortcut">Shortcut</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 -->
|
<!-- Cheats -->
|
||||||
<string name="cheats">Cheats</string>
|
<string name="cheats">Cheats</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user