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.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
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.adapters.GameAdapter.GameViewHolder
|
||||
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.settings.ui.SettingsActivity
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.utils.GameIconUtils
|
||||
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()),
|
||||
View.OnClickListener, View.OnLongClickListener {
|
||||
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 {
|
||||
// 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 {
|
||||
val shortcutManager = activity.getSystemService(ShortcutManager::class.java)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val bitmap = (bottomSheetView.findViewById<ImageView>(R.id.game_icon).drawable as BitmapDrawable).bitmap
|
||||
val icon = Icon.createWithBitmap(bitmap)
|
||||
|
||||
val shortcut = ShortcutInfo.Builder(context, game.title)
|
||||
.setShortLabel(game.title)
|
||||
.setIcon(icon)
|
||||
.setIntent(game.launchIntent.apply {
|
||||
putExtra("launched_from_shortcut", true)
|
||||
})
|
||||
.build()
|
||||
shortcutManager.requestPinShortcut(shortcut, null)
|
||||
// Default to false for zoomed in shortcut icons
|
||||
preferences.edit() {
|
||||
putBoolean(
|
||||
"shouldStretchIcon",
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
val action = CheatsFragmentDirections.actionGlobalCheatsFragment(holder.game.titleId)
|
||||
view.findNavController().navigate(action)
|
||||
@ -252,6 +314,48 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
||||
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 {
|
||||
return Game.badExtensions.stream()
|
||||
.noneMatch { extension == it.lowercase() }
|
||||
|
@ -10,6 +10,7 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
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.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
import android.net.Uri
|
||||
|
||||
class GamesFragment : Fragment() {
|
||||
private var _binding: FragmentGamesBinding? = null
|
||||
@ -40,6 +42,13 @@ class GamesFragment : Fragment() {
|
||||
|
||||
private val gamesViewModel: GamesViewModel 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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -63,12 +72,18 @@ class GamesFragment : Fragment() {
|
||||
|
||||
val inflater = LayoutInflater.from(requireContext())
|
||||
|
||||
gameAdapter = GameAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
inflater,
|
||||
openImageLauncher
|
||||
)
|
||||
|
||||
binding.gridGames.apply {
|
||||
layoutManager = GridLayoutManager(
|
||||
requireContext(),
|
||||
resources.getInteger(R.integer.game_grid_columns)
|
||||
)
|
||||
adapter = GameAdapter(requireActivity() as AppCompatActivity, inflater)
|
||||
adapter = this@GamesFragment.gameAdapter
|
||||
}
|
||||
|
||||
binding.swipeRefresh.apply {
|
||||
|
@ -36,6 +36,8 @@ import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
import java.time.temporal.ChronoField
|
||||
import java.util.Locale
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
private var _binding: FragmentSearchBinding? = null
|
||||
@ -43,6 +45,13 @@ class SearchFragment : Fragment() {
|
||||
|
||||
private val gamesViewModel: GamesViewModel 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
|
||||
|
||||
@ -73,12 +82,18 @@ class SearchFragment : Fragment() {
|
||||
|
||||
val inflater = LayoutInflater.from(requireContext())
|
||||
|
||||
gameAdapter = GameAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
inflater,
|
||||
openImageLauncher
|
||||
)
|
||||
|
||||
binding.gridGamesSearch.apply {
|
||||
layoutManager = GridLayoutManager(
|
||||
requireContext(),
|
||||
resources.getInteger(R.integer.game_grid_columns)
|
||||
)
|
||||
adapter = GameAdapter(requireActivity() as AppCompatActivity, inflater)
|
||||
adapter = this@SearchFragment.gameAdapter
|
||||
}
|
||||
|
||||
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 -->
|
||||
<string name="play">Play</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 -->
|
||||
<string name="cheats">Cheats</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user