mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2025-03-13 09:12:27 +01:00
android: Enhance shortcut customization with a custom dialog
Adds ability to customize game shortcuts with: - Custom name input - Editable icon via image picker - Ability to stretch to fit or zoom to fit the shortcut icon
This commit is contained in:
parent
43dbe42b29
commit
eafe406153
@ -19,6 +19,8 @@ import android.graphics.drawable.BitmapDrawable
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.content.pm.ShortcutInfo
|
import android.content.pm.ShortcutInfo
|
||||||
import android.content.pm.ShortcutManager
|
import android.content.pm.ShortcutManager
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
@ -41,17 +43,30 @@ import org.citra.citra_emu.CitraApplication
|
|||||||
import org.citra.citra_emu.R
|
import org.citra.citra_emu.R
|
||||||
import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
|
import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
|
||||||
import org.citra.citra_emu.databinding.CardGameBinding
|
import org.citra.citra_emu.databinding.CardGameBinding
|
||||||
|
import org.citra.citra_emu.databinding.DialogShortcutBinding
|
||||||
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.utils.SettingsFile
|
|
||||||
import org.citra.citra_emu.model.Game
|
import org.citra.citra_emu.model.Game
|
||||||
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
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.graphics.scale
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
class GameAdapter(private val activity: AppCompatActivity, private val inflater: LayoutInflater) :
|
class GameAdapter(private val activity: AppCompatActivity, private val inflater: LayoutInflater, private val openImageLauncher: ActivityResultLauncher<String>?) :
|
||||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||||
View.OnClickListener, View.OnLongClickListener {
|
View.OnClickListener, View.OnLongClickListener {
|
||||||
private var lastClickTime = 0L
|
private var lastClickTime = 0L
|
||||||
|
private var imagePath: String? = null
|
||||||
|
private var dialogShortcutBinding: DialogShortcutBinding? = null
|
||||||
|
|
||||||
|
fun handleShortcutImageResult(uri: Uri?) {
|
||||||
|
val path = uri?.toString()
|
||||||
|
if (path != null) {
|
||||||
|
imagePath = path
|
||||||
|
dialogShortcutBinding!!.imageScaleSwitch.isEnabled = imagePath != null
|
||||||
|
refreshShortcutDialogIcon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||||
// Create a new view.
|
// Create a new view.
|
||||||
@ -222,23 +237,70 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bottomSheetView.findViewById<MaterialButton>(R.id.game_shortcut).setOnClickListener {
|
bottomSheetView.findViewById<MaterialButton>(R.id.game_shortcut).setOnClickListener {
|
||||||
val shortcutManager = activity.getSystemService(ShortcutManager::class.java)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
// Default to false for zoomed in shortcut icons
|
||||||
val bitmap = (bottomSheetView.findViewById<ImageView>(R.id.game_icon).drawable as BitmapDrawable).bitmap
|
preferences.edit() {
|
||||||
val icon = Icon.createWithBitmap(bitmap)
|
putBoolean(
|
||||||
|
"shouldStretchIcon",
|
||||||
val shortcut = ShortcutInfo.Builder(context, game.title)
|
false
|
||||||
.setShortLabel(game.title)
|
)
|
||||||
.setIcon(icon)
|
|
||||||
.setIntent(game.launchIntent.apply {
|
|
||||||
putExtra("launched_from_shortcut", true)
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
shortcutManager.requestPinShortcut(shortcut, null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialogShortcutBinding = DialogShortcutBinding.inflate(activity.layoutInflater)
|
||||||
|
|
||||||
|
dialogShortcutBinding!!.shortcutNameInput.setText(game.title)
|
||||||
|
GameIconUtils.loadGameIcon(activity, game, dialogShortcutBinding!!.shortcutIcon)
|
||||||
|
|
||||||
|
dialogShortcutBinding!!.shortcutIcon.setOnClickListener {
|
||||||
|
openImageLauncher?.launch("image/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogShortcutBinding!!.imageScaleSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
preferences.edit {
|
||||||
|
putBoolean(
|
||||||
|
"shouldStretchIcon",
|
||||||
|
isChecked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
refreshShortcutDialogIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(R.string.create_shortcut)
|
||||||
|
.setView(dialogShortcutBinding!!.root)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val shortcutName = dialogShortcutBinding!!.shortcutNameInput.text.toString()
|
||||||
|
if (shortcutName.isEmpty()) {
|
||||||
|
Toast.makeText(context, R.string.shortcut_name_empty, Toast.LENGTH_LONG).show()
|
||||||
|
return@setPositiveButton
|
||||||
|
}
|
||||||
|
val iconBitmap = (dialogShortcutBinding!!.shortcutIcon.drawable as BitmapDrawable).bitmap
|
||||||
|
val shortcutManager = activity.getSystemService(ShortcutManager::class.java)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val icon = Icon.createWithBitmap(iconBitmap)
|
||||||
|
val shortcut = ShortcutInfo.Builder(context, shortcutName)
|
||||||
|
.setShortLabel(shortcutName)
|
||||||
|
.setIcon(icon)
|
||||||
|
.setIntent(game.launchIntent.apply {
|
||||||
|
putExtra("launchedFromShortcut", true)
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
|
||||||
|
shortcutManager?.requestPinShortcut(shortcut, null)
|
||||||
|
imagePath = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
imagePath = null
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
|
||||||
|
bottomSheetDialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bottomSheetView.findViewById<MaterialButton>(R.id.cheats).setOnClickListener {
|
bottomSheetView.findViewById<MaterialButton>(R.id.cheats).setOnClickListener {
|
||||||
val action = CheatsFragmentDirections.actionGlobalCheatsFragment(holder.game.titleId)
|
val action = CheatsFragmentDirections.actionGlobalCheatsFragment(holder.game.titleId)
|
||||||
view.findNavController().navigate(action)
|
view.findNavController().navigate(action)
|
||||||
@ -252,6 +314,48 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
|||||||
bottomSheetDialog.show()
|
bottomSheetDialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun refreshShortcutDialogIcon() {
|
||||||
|
if (imagePath != null) {
|
||||||
|
val originalBitmap = BitmapFactory.decodeStream(
|
||||||
|
CitraApplication.appContext.contentResolver.openInputStream(
|
||||||
|
imagePath!!.toUri()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val scaledBitmap = {
|
||||||
|
val preferences =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
if (preferences.getBoolean("shouldStretchIcon", true)) {
|
||||||
|
// stretch to fit
|
||||||
|
originalBitmap.scale(108, 108)
|
||||||
|
} else {
|
||||||
|
// Zoom in to fit the bitmap while keeping the aspect ratio
|
||||||
|
val width = originalBitmap.width
|
||||||
|
val height = originalBitmap.height
|
||||||
|
val targetSize = 108
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
// Landscape orientation
|
||||||
|
val scaleFactor = targetSize.toFloat() / height
|
||||||
|
val scaledWidth = (width * scaleFactor).toInt()
|
||||||
|
val scaledBmp = originalBitmap.scale(scaledWidth, targetSize)
|
||||||
|
|
||||||
|
val startX = (scaledWidth - targetSize) / 2
|
||||||
|
Bitmap.createBitmap(scaledBmp, startX, 0, targetSize, targetSize)
|
||||||
|
} else {
|
||||||
|
val scaleFactor = targetSize.toFloat() / width
|
||||||
|
val scaledHeight = (height * scaleFactor).toInt()
|
||||||
|
val scaledBmp = originalBitmap.scale(targetSize, scaledHeight)
|
||||||
|
|
||||||
|
val startY = (scaledHeight - targetSize) / 2
|
||||||
|
Bitmap.createBitmap(scaledBmp, 0, startY, targetSize, targetSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
dialogShortcutBinding!!.shortcutIcon.setImageBitmap(scaledBitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun isValidGame(extension: String): Boolean {
|
private fun isValidGame(extension: String): Boolean {
|
||||||
return Game.badExtensions.stream()
|
return Game.badExtensions.stream()
|
||||||
.noneMatch { extension == it.lowercase() }
|
.noneMatch { extension == it.lowercase() }
|
||||||
|
@ -10,6 +10,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
@ -33,6 +34,7 @@ import org.citra.citra_emu.features.settings.model.Settings
|
|||||||
import org.citra.citra_emu.model.Game
|
import org.citra.citra_emu.model.Game
|
||||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
class GamesFragment : Fragment() {
|
class GamesFragment : Fragment() {
|
||||||
private var _binding: FragmentGamesBinding? = null
|
private var _binding: FragmentGamesBinding? = null
|
||||||
@ -40,6 +42,13 @@ class GamesFragment : Fragment() {
|
|||||||
|
|
||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private lateinit var gameAdapter: GameAdapter
|
||||||
|
|
||||||
|
private val openImageLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.GetContent()
|
||||||
|
) { uri: Uri? ->
|
||||||
|
gameAdapter.handleShortcutImageResult(uri)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -63,12 +72,18 @@ class GamesFragment : Fragment() {
|
|||||||
|
|
||||||
val inflater = LayoutInflater.from(requireContext())
|
val inflater = LayoutInflater.from(requireContext())
|
||||||
|
|
||||||
|
gameAdapter = GameAdapter(
|
||||||
|
requireActivity() as AppCompatActivity,
|
||||||
|
inflater,
|
||||||
|
openImageLauncher
|
||||||
|
)
|
||||||
|
|
||||||
binding.gridGames.apply {
|
binding.gridGames.apply {
|
||||||
layoutManager = GridLayoutManager(
|
layoutManager = GridLayoutManager(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
resources.getInteger(R.integer.game_grid_columns)
|
resources.getInteger(R.integer.game_grid_columns)
|
||||||
)
|
)
|
||||||
adapter = GameAdapter(requireActivity() as AppCompatActivity, inflater)
|
adapter = this@GamesFragment.gameAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.swipeRefresh.apply {
|
binding.swipeRefresh.apply {
|
||||||
|
@ -36,6 +36,8 @@ import org.citra.citra_emu.viewmodel.GamesViewModel
|
|||||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
import java.time.temporal.ChronoField
|
import java.time.temporal.ChronoField
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
|
||||||
class SearchFragment : Fragment() {
|
class SearchFragment : Fragment() {
|
||||||
private var _binding: FragmentSearchBinding? = null
|
private var _binding: FragmentSearchBinding? = null
|
||||||
@ -43,6 +45,13 @@ class SearchFragment : Fragment() {
|
|||||||
|
|
||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private lateinit var gameAdapter: GameAdapter
|
||||||
|
|
||||||
|
private val openImageLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.GetContent()
|
||||||
|
) { uri: Uri? ->
|
||||||
|
gameAdapter.handleShortcutImageResult(uri)
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var preferences: SharedPreferences
|
private lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
@ -73,12 +82,18 @@ class SearchFragment : Fragment() {
|
|||||||
|
|
||||||
val inflater = LayoutInflater.from(requireContext())
|
val inflater = LayoutInflater.from(requireContext())
|
||||||
|
|
||||||
|
gameAdapter = GameAdapter(
|
||||||
|
requireActivity() as AppCompatActivity,
|
||||||
|
inflater,
|
||||||
|
openImageLauncher
|
||||||
|
)
|
||||||
|
|
||||||
binding.gridGamesSearch.apply {
|
binding.gridGamesSearch.apply {
|
||||||
layoutManager = GridLayoutManager(
|
layoutManager = GridLayoutManager(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
resources.getInteger(R.integer.game_grid_columns)
|
resources.getInteger(R.integer.game_grid_columns)
|
||||||
)
|
)
|
||||||
adapter = GameAdapter(requireActivity() as AppCompatActivity, inflater)
|
adapter = this@SearchFragment.gameAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
|
binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@android:color/black"/>
|
||||||
|
<corners android:radius="21dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@android:color/black"/>
|
||||||
|
<corners android:radius="21dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
61
src/android/app/src/main/res/layout/dialog_shortcut.xml
Normal file
61
src/android/app/src/main/res/layout/dialog_shortcut.xml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/shortcut_icon"
|
||||||
|
android:layout_width="96dp"
|
||||||
|
android:layout_height="96dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Medium"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="42dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@android:drawable/ic_menu_edit"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:contentDescription="@string/edit_icon"
|
||||||
|
app:tint="?attr/colorAccent"
|
||||||
|
android:background="@drawable/shortcut_edit_background"
|
||||||
|
android:alpha="0.8"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/image_scale_switch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:enabled="false"
|
||||||
|
android:text="@string/shortcut_image_stretch_toggle"/>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/shortcut_name">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/shortcut_name_input"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLines="1"/>
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -477,6 +477,12 @@
|
|||||||
<!-- 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="shortcut_name">Shortcut Name</string>
|
||||||
|
<string name="edit_icon">Edit icon</string>
|
||||||
|
<string name="create_shortcut">Create Shortcut</string>
|
||||||
|
<string name="shortcut_name_empty">Shortcut name cannot be empty</string>
|
||||||
|
<string name="shortcut_image_stretch_toggle">Stretch to fit image</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Cheats -->
|
<!-- Cheats -->
|
||||||
<string name="cheats">Cheats</string>
|
<string name="cheats">Cheats</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user