diff --git a/src/android/app/src/main/java/org/citron/citron_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/citron/citron_emu/adapters/GameAdapter.kt index 9adf45c14..f853200e2 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/adapters/GameAdapter.kt @@ -23,6 +23,7 @@ import org.citron.citron_emu.HomeNavigationDirections import org.citron.citron_emu.R import org.citron.citron_emu.CitronApplication import org.citron.citron_emu.databinding.CardGameBinding +import org.citron.citron_emu.databinding.CardGameListBinding import org.citron.citron_emu.model.Game import org.citron.citron_emu.model.GamesViewModel import org.citron.citron_emu.utils.GameIconUtils @@ -30,13 +31,41 @@ import org.citron.citron_emu.utils.ViewUtils.marquee import org.citron.citron_emu.viewholder.AbstractViewHolder class GameAdapter(private val activity: AppCompatActivity) : - AbstractDiffAdapter(exact = false) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { - CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) - .also { return GameViewHolder(it) } + AbstractDiffAdapter>(exact = false) { + + companion object { + const val VIEW_TYPE_GRID = 0 + const val VIEW_TYPE_LIST = 1 } - inner class GameViewHolder(val binding: CardGameBinding) : + private var isListView = false + + fun setListView(listView: Boolean) { + if (isListView != listView) { + isListView = listView + notifyDataSetChanged() + } + } + + override fun getItemViewType(position: Int): Int { + return if (isListView) VIEW_TYPE_LIST else VIEW_TYPE_GRID + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder { + return when (viewType) { + VIEW_TYPE_GRID -> { + val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) + GameGridViewHolder(binding) + } + VIEW_TYPE_LIST -> { + val binding = CardGameListBinding.inflate(LayoutInflater.from(parent.context), parent, false) + GameListViewHolder(binding) + } + else -> throw IllegalArgumentException("Invalid view type") + } + } + + inner class GameGridViewHolder(val binding: CardGameBinding) : AbstractViewHolder(binding) { override fun bind(model: Game) { binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP @@ -50,50 +79,79 @@ class GameAdapter(private val activity: AppCompatActivity) : } fun onClick(game: Game) { - val gameExists = DocumentFile.fromSingleUri( - CitronApplication.appContext, - Uri.parse(game.path) - )?.exists() == true - if (!gameExists) { - Toast.makeText( - CitronApplication.appContext, - R.string.loader_error_file_not_found, - Toast.LENGTH_LONG - ).show() - - ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true) - return - } - - val preferences = - PreferenceManager.getDefaultSharedPreferences(CitronApplication.appContext) - preferences.edit() - .putLong( - game.keyLastPlayedTime, - System.currentTimeMillis() - ) - .apply() - - activity.lifecycleScope.launch { - withContext(Dispatchers.IO) { - val shortcut = - ShortcutInfoCompat.Builder(CitronApplication.appContext, game.path) - .setShortLabel(game.title) - .setIcon(GameIconUtils.getShortcutIcon(activity, game)) - .setIntent(game.launchIntent) - .build() - ShortcutManagerCompat.pushDynamicShortcut(CitronApplication.appContext, shortcut) - } - } - - val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true) - binding.root.findNavController().navigate(action) + handleGameClick(game) } fun onLongClick(game: Game): Boolean { - val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game) - binding.root.findNavController().navigate(action) - return true + return handleGameLongClick(game) } } + + inner class GameListViewHolder(val binding: CardGameListBinding) : + AbstractViewHolder(binding) { + override fun bind(model: Game) { + binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP + GameIconUtils.loadGameIcon(model, binding.imageGameScreen) + + binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") + + binding.cardGame.setOnClickListener { onClick(model) } + binding.cardGame.setOnLongClickListener { onLongClick(model) } + } + + fun onClick(game: Game) { + handleGameClick(game) + } + + fun onLongClick(game: Game): Boolean { + return handleGameLongClick(game) + } + } + + private fun handleGameClick(game: Game) { + val gameExists = DocumentFile.fromSingleUri( + CitronApplication.appContext, + Uri.parse(game.path) + )?.exists() == true + if (!gameExists) { + Toast.makeText( + CitronApplication.appContext, + R.string.loader_error_file_not_found, + Toast.LENGTH_LONG + ).show() + + ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true) + return + } + + val preferences = + PreferenceManager.getDefaultSharedPreferences(CitronApplication.appContext) + preferences.edit() + .putLong( + game.keyLastPlayedTime, + System.currentTimeMillis() + ) + .apply() + + activity.lifecycleScope.launch { + withContext(Dispatchers.IO) { + val shortcut = + ShortcutInfoCompat.Builder(CitronApplication.appContext, game.path) + .setShortLabel(game.title) + .setIcon(GameIconUtils.getShortcutIcon(activity, game)) + .setIntent(game.launchIntent) + .build() + ShortcutManagerCompat.pushDynamicShortcut(CitronApplication.appContext, shortcut) + } + } + + val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true) + activity.findNavController(R.id.fragment_container).navigate(action) + } + + private fun handleGameLongClick(game: Game): Boolean { + val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game) + activity.findNavController(R.id.fragment_container).navigate(action) + return true + } } diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/Settings.kt index 279ee1aad..90deb71ca 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/Settings.kt @@ -24,7 +24,8 @@ object Settings { SECTION_INPUT_PLAYER_EIGHT, SECTION_THEME(R.string.preferences_theme), SECTION_DEBUG(R.string.preferences_debug), - SECTION_ZEP_ZONE(R.string.preferences_zep_zone); + SECTION_ZEP_ZONE(R.string.preferences_zep_zone), + SECTION_APPLETS_ANDROID(R.string.preferences_applets_android); } fun getPlayerString(player: Int): String = diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/StringSetting.kt index 34379e445..15f4bd6a3 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/StringSetting.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/StringSetting.kt @@ -7,7 +7,8 @@ import org.citron.citron_emu.utils.NativeConfig enum class StringSetting(override val key: String) : AbstractStringSetting { DRIVER_PATH("driver_path"), - DEVICE_NAME("device_name"); + DEVICE_NAME("device_name"), + LOG_FILTER("log_filter"); override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt index d742ddc42..35811538a 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -100,6 +100,7 @@ class SettingsFragmentPresenter( MenuTag.SECTION_THEME -> addThemeSettings(sl) MenuTag.SECTION_DEBUG -> addDebugSettings(sl) MenuTag.SECTION_ZEP_ZONE -> addZepZoneSettings(sl) + MenuTag.SECTION_APPLETS_ANDROID -> addAppletsAndroidSettings(sl) } settingsList = sl adapter.submitList(settingsList) { @@ -151,6 +152,14 @@ class SettingsFragmentPresenter( menuKey = MenuTag.SECTION_ZEP_ZONE ) ) + add( + SubmenuSetting( + titleId = R.string.preferences_applets_android, + descriptionId = R.string.preferences_applets_android_description, + iconId = R.drawable.ic_applet, + menuKey = MenuTag.SECTION_APPLETS_ANDROID + ) + ) add( RunnableSetting( titleId = R.string.reset_to_default, @@ -981,6 +990,15 @@ class SettingsFragmentPresenter( add(IntSetting.CPU_ACCURACY.key) add(BooleanSetting.CPU_DEBUG_MODE.key) add(SettingsItem.FASTMEM_COMBINED) + + add(HeaderSetting(R.string.logging)) + add( + StringInputSetting( + StringSetting.LOG_FILTER, + titleId = R.string.log_filter, + descriptionId = R.string.log_filter_description + ) + ) } } @@ -1000,7 +1018,11 @@ class SettingsFragmentPresenter( add(HeaderSetting(R.string.frame_skipping_header)) add(IntSetting.FRAME_SKIPPING.key) add(IntSetting.FRAME_SKIPPING_MODE.key) + } + } + private fun addAppletsAndroidSettings(sl: ArrayList) { + sl.apply { add(HeaderSetting(R.string.applet_settings_header)) add(IntSetting.CABINET_APPLET_MODE.key) add(IntSetting.CONTROLLER_APPLET_MODE.key) diff --git a/src/android/app/src/main/java/org/citron/citron_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/citron/citron_emu/ui/GamesFragment.kt index 8c73a8d10..7a2f9c2da 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/ui/GamesFragment.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/ui/GamesFragment.kt @@ -13,6 +13,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.color.MaterialColors import org.citron.citron_emu.R import org.citron.citron_emu.adapters.GameAdapter @@ -31,6 +32,9 @@ class GamesFragment : Fragment() { private val gamesViewModel: GamesViewModel by activityViewModels() private val homeViewModel: HomeViewModel by activityViewModels() + private lateinit var gameAdapter: GameAdapter + private var isListView = false + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -45,12 +49,19 @@ class GamesFragment : Fragment() { homeViewModel.setNavigationVisibility(visible = true, animated = true) homeViewModel.setStatusBarShadeVisibility(true) + gameAdapter = GameAdapter(requireActivity() as AppCompatActivity) + binding.gridGames.apply { layoutManager = AutofitGridLayoutManager( requireContext(), requireContext().resources.getDimensionPixelSize(R.dimen.card_width) ) - adapter = GameAdapter(requireActivity() as AppCompatActivity) + adapter = gameAdapter + } + + // Set up button for view switching + binding.btnViewToggle.setOnClickListener { + toggleViewMode() } binding.swipeRefresh.apply { @@ -90,14 +101,14 @@ class GamesFragment : Fragment() { ) } gamesViewModel.games.collect(viewLifecycleOwner) { - (binding.gridGames.adapter as GameAdapter).submitList(it) + gameAdapter.submitList(it) } gamesViewModel.shouldSwapData.collect( viewLifecycleOwner, resetState = { gamesViewModel.setShouldSwapData(false) } ) { if (it) { - (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value) + gameAdapter.submitList(gamesViewModel.games.value) } } gamesViewModel.shouldScrollToTop.collect( @@ -108,6 +119,27 @@ class GamesFragment : Fragment() { setInsets() } + private fun toggleViewMode() { + isListView = !isListView + + if (isListView) { + // Switch to list view + binding.gridGames.layoutManager = LinearLayoutManager(requireContext()) + binding.btnViewToggle.setIconResource(R.drawable.ic_view_grid) + binding.btnViewToggle.contentDescription = getString(R.string.switch_to_grid_view) + } else { + // Switch to grid view + binding.gridGames.layoutManager = AutofitGridLayoutManager( + requireContext(), + requireContext().resources.getDimensionPixelSize(R.dimen.card_width) + ) + binding.btnViewToggle.setIconResource(R.drawable.ic_view_list) + binding.btnViewToggle.contentDescription = getString(R.string.switch_to_list_view) + } + + gameAdapter.setListView(isListView) + } + override fun onDestroyView() { super.onDestroyView() _binding = null @@ -155,6 +187,14 @@ class GamesFragment : Fragment() { binding.noticeText.updatePadding(bottom = spacingNavigation) + // Update button margins + val buttonSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) + binding.btnViewToggle.updateMargins( + left = leftInsets + buttonSpacing, + right = rightInsets + buttonSpacing, + bottom = barInsets.bottom + spacingNavigation + buttonSpacing + ) + windowInsets } } diff --git a/src/android/app/src/main/res/drawable/ic_view_grid.xml b/src/android/app/src/main/res/drawable/ic_view_grid.xml new file mode 100644 index 000000000..d44b4592b --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_view_grid.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_view_list.xml b/src/android/app/src/main/res/drawable/ic_view_list.xml new file mode 100644 index 000000000..dad2c0c52 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_view_list.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/layout/card_game_list.xml b/src/android/app/src/main/res/layout/card_game_list.xml new file mode 100644 index 000000000..86c015646 --- /dev/null +++ b/src/android/app/src/main/res/layout/card_game_list.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout/fragment_games.xml b/src/android/app/src/main/res/layout/fragment_games.xml index cc280b1ff..1e4c6f6d3 100644 --- a/src/android/app/src/main/res/layout/fragment_games.xml +++ b/src/android/app/src/main/res/layout/fragment_games.xml @@ -2,6 +2,7 @@ + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 7f9e6761d..9842db990 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -33,6 +33,8 @@ Settings No files were found or no game directory has been selected yet. Search and filter games + Switch to list view + Switch to grid view Select games folder Manage game folders Allows citron to populate the games list @@ -256,6 +258,9 @@ Graphics debugging Sets the graphics API to a slow debugging mode. Fastmem + Logging + Log Filter + Configure which log messages to display. Format: <class>:<level>. Example: *:Info Service:Debug Output engine @@ -418,6 +423,8 @@ CPU/GPU debugging, graphics API, fastmem Zep Zone Advanced emulation settings + Applets on Android + System applet configuration settings Memory Layout diff --git a/src/common/settings.h b/src/common/settings.h index 57991654b..0499cb3d6 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -634,7 +634,7 @@ struct Values { Setting perform_vulkan_check{linkage, true, "perform_vulkan_check", Category::Debugging}; // Miscellaneous - Setting log_filter{linkage, "*:Info", "log_filter", Category::Miscellaneous}; + Setting log_filter{linkage, "*:Info", "log_filter", Category::Debugging}; Setting use_dev_keys{linkage, false, "use_dev_keys", Category::Miscellaneous}; // Network