Merge branch 'list-view-and-applets-settings' into 'master'

feat: Add list view toggle and separate applets settings category

See merge request citron/rewrite!50
This commit is contained in:
Zephyron
2025-08-21 06:47:51 +00:00
11 changed files with 268 additions and 52 deletions

View File

@@ -23,6 +23,7 @@ import org.citron.citron_emu.HomeNavigationDirections
import org.citron.citron_emu.R import org.citron.citron_emu.R
import org.citron.citron_emu.CitronApplication import org.citron.citron_emu.CitronApplication
import org.citron.citron_emu.databinding.CardGameBinding 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.Game
import org.citron.citron_emu.model.GamesViewModel import org.citron.citron_emu.model.GamesViewModel
import org.citron.citron_emu.utils.GameIconUtils 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 import org.citron.citron_emu.viewholder.AbstractViewHolder
class GameAdapter(private val activity: AppCompatActivity) : class GameAdapter(private val activity: AppCompatActivity) :
AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) { AbstractDiffAdapter<Game, AbstractViewHolder<Game>>(exact = false) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) companion object {
.also { return GameViewHolder(it) } 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<Game> {
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<Game>(binding) { AbstractViewHolder<Game>(binding) {
override fun bind(model: Game) { override fun bind(model: Game) {
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
@@ -50,6 +79,36 @@ class GameAdapter(private val activity: AppCompatActivity) :
} }
fun onClick(game: Game) { fun onClick(game: Game) {
handleGameClick(game)
}
fun onLongClick(game: Game): Boolean {
return handleGameLongClick(game)
}
}
inner class GameListViewHolder(val binding: CardGameListBinding) :
AbstractViewHolder<Game>(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( val gameExists = DocumentFile.fromSingleUri(
CitronApplication.appContext, CitronApplication.appContext,
Uri.parse(game.path) Uri.parse(game.path)
@@ -87,13 +146,12 @@ class GameAdapter(private val activity: AppCompatActivity) :
} }
val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true) val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true)
binding.root.findNavController().navigate(action) activity.findNavController(R.id.fragment_container).navigate(action)
} }
fun onLongClick(game: Game): Boolean { private fun handleGameLongClick(game: Game): Boolean {
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game) val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
binding.root.findNavController().navigate(action) activity.findNavController(R.id.fragment_container).navigate(action)
return true return true
} }
} }
}

View File

@@ -24,7 +24,8 @@ object Settings {
SECTION_INPUT_PLAYER_EIGHT, SECTION_INPUT_PLAYER_EIGHT,
SECTION_THEME(R.string.preferences_theme), SECTION_THEME(R.string.preferences_theme),
SECTION_DEBUG(R.string.preferences_debug), 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 = fun getPlayerString(player: Int): String =

View File

@@ -7,7 +7,8 @@ import org.citron.citron_emu.utils.NativeConfig
enum class StringSetting(override val key: String) : AbstractStringSetting { enum class StringSetting(override val key: String) : AbstractStringSetting {
DRIVER_PATH("driver_path"), 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) override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)

View File

@@ -100,6 +100,7 @@ class SettingsFragmentPresenter(
MenuTag.SECTION_THEME -> addThemeSettings(sl) MenuTag.SECTION_THEME -> addThemeSettings(sl)
MenuTag.SECTION_DEBUG -> addDebugSettings(sl) MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
MenuTag.SECTION_ZEP_ZONE -> addZepZoneSettings(sl) MenuTag.SECTION_ZEP_ZONE -> addZepZoneSettings(sl)
MenuTag.SECTION_APPLETS_ANDROID -> addAppletsAndroidSettings(sl)
} }
settingsList = sl settingsList = sl
adapter.submitList(settingsList) { adapter.submitList(settingsList) {
@@ -151,6 +152,14 @@ class SettingsFragmentPresenter(
menuKey = MenuTag.SECTION_ZEP_ZONE 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( add(
RunnableSetting( RunnableSetting(
titleId = R.string.reset_to_default, titleId = R.string.reset_to_default,
@@ -981,6 +990,15 @@ class SettingsFragmentPresenter(
add(IntSetting.CPU_ACCURACY.key) add(IntSetting.CPU_ACCURACY.key)
add(BooleanSetting.CPU_DEBUG_MODE.key) add(BooleanSetting.CPU_DEBUG_MODE.key)
add(SettingsItem.FASTMEM_COMBINED) 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(HeaderSetting(R.string.frame_skipping_header))
add(IntSetting.FRAME_SKIPPING.key) add(IntSetting.FRAME_SKIPPING.key)
add(IntSetting.FRAME_SKIPPING_MODE.key) add(IntSetting.FRAME_SKIPPING_MODE.key)
}
}
private fun addAppletsAndroidSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(HeaderSetting(R.string.applet_settings_header)) add(HeaderSetting(R.string.applet_settings_header))
add(IntSetting.CABINET_APPLET_MODE.key) add(IntSetting.CABINET_APPLET_MODE.key)
add(IntSetting.CONTROLLER_APPLET_MODE.key) add(IntSetting.CONTROLLER_APPLET_MODE.key)

View File

@@ -13,6 +13,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import org.citron.citron_emu.R import org.citron.citron_emu.R
import org.citron.citron_emu.adapters.GameAdapter import org.citron.citron_emu.adapters.GameAdapter
@@ -31,6 +32,9 @@ 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 var isListView = false
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -45,12 +49,19 @@ class GamesFragment : Fragment() {
homeViewModel.setNavigationVisibility(visible = true, animated = true) homeViewModel.setNavigationVisibility(visible = true, animated = true)
homeViewModel.setStatusBarShadeVisibility(true) homeViewModel.setStatusBarShadeVisibility(true)
gameAdapter = GameAdapter(requireActivity() as AppCompatActivity)
binding.gridGames.apply { binding.gridGames.apply {
layoutManager = AutofitGridLayoutManager( layoutManager = AutofitGridLayoutManager(
requireContext(), requireContext(),
requireContext().resources.getDimensionPixelSize(R.dimen.card_width) 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 { binding.swipeRefresh.apply {
@@ -90,14 +101,14 @@ class GamesFragment : Fragment() {
) )
} }
gamesViewModel.games.collect(viewLifecycleOwner) { gamesViewModel.games.collect(viewLifecycleOwner) {
(binding.gridGames.adapter as GameAdapter).submitList(it) gameAdapter.submitList(it)
} }
gamesViewModel.shouldSwapData.collect( gamesViewModel.shouldSwapData.collect(
viewLifecycleOwner, viewLifecycleOwner,
resetState = { gamesViewModel.setShouldSwapData(false) } resetState = { gamesViewModel.setShouldSwapData(false) }
) { ) {
if (it) { if (it) {
(binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value) gameAdapter.submitList(gamesViewModel.games.value)
} }
} }
gamesViewModel.shouldScrollToTop.collect( gamesViewModel.shouldScrollToTop.collect(
@@ -108,6 +119,27 @@ class GamesFragment : Fragment() {
setInsets() 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() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
@@ -155,6 +187,14 @@ class GamesFragment : Fragment() {
binding.noticeText.updatePadding(bottom = spacingNavigation) 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 windowInsets
} }
} }

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface">
<path
android:fillColor="@android:color/white"
android:pathData="M3,3v8h8L11,3L3,3zM9,9L5,9L5,5h4v4zM3,13v8h8v-8L3,13zM9,19L5,19v-4h4v4zM13,3v8h8L21,3h-8zM19,9h-4L15,5h4v4zM13,13v8h8v-8h-8zM19,19h-4v-4h4v4z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface">
<path
android:fillColor="@android:color/white"
android:pathData="M3,13h2v-2L3,11v2zM3,17h2v-2L3,15v2zM3,9h2L5,7L3,7v2zM7,13h14v-2L7,11v2zM7,17h14v-2L7,15v2zM7,7v2h14L21,7L7,7z"/>
</vector>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_game"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginVertical="4dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:clipToPadding="false"
android:focusable="true"
android:transitionName="card_game"
app:cardCornerRadius="8dp"
app:cardElevation="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image_game_screen"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="16dp"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Small"
tools:src="@drawable/default_icon" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_game_title"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textAlignment="viewStart"
android:textSize="16sp"
tools:text="The Legend of Zelda: Skyward Sword" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>

View File

@@ -2,6 +2,7 @@
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/swipe_refresh" android:id="@+id/swipe_refresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -30,6 +31,18 @@
android:defaultFocusHighlightEnabled="false" android:defaultFocusHighlightEnabled="false"
tools:listitem="@layout/card_game" /> tools:listitem="@layout/card_game" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_view_toggle"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_margin="16dp"
android:contentDescription="@string/switch_to_list_view"
app:icon="@drawable/ic_view_list"
app:iconSize="24dp" />
</RelativeLayout> </RelativeLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -33,6 +33,8 @@
<string name="home_settings">Settings</string> <string name="home_settings">Settings</string>
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string> <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
<string name="search_and_filter_games">Search and filter games</string> <string name="search_and_filter_games">Search and filter games</string>
<string name="switch_to_list_view">Switch to list view</string>
<string name="switch_to_grid_view">Switch to grid view</string>
<string name="select_games_folder">Select games folder</string> <string name="select_games_folder">Select games folder</string>
<string name="manage_game_folders">Manage game folders</string> <string name="manage_game_folders">Manage game folders</string>
<string name="select_games_folder_description">Allows citron to populate the games list</string> <string name="select_games_folder_description">Allows citron to populate the games list</string>
@@ -256,6 +258,9 @@
<string name="renderer_debug">Graphics debugging</string> <string name="renderer_debug">Graphics debugging</string>
<string name="renderer_debug_description">Sets the graphics API to a slow debugging mode.</string> <string name="renderer_debug_description">Sets the graphics API to a slow debugging mode.</string>
<string name="fastmem">Fastmem</string> <string name="fastmem">Fastmem</string>
<string name="logging">Logging</string>
<string name="log_filter">Log Filter</string>
<string name="log_filter_description">Configure which log messages to display. Format: &lt;class&gt;:&lt;level&gt;. Example: *:Info Service:Debug</string>
<!-- Audio settings strings --> <!-- Audio settings strings -->
<string name="audio_output_engine">Output engine</string> <string name="audio_output_engine">Output engine</string>
@@ -418,6 +423,8 @@
<string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string>
<string name="preferences_zep_zone">Zep Zone</string> <string name="preferences_zep_zone">Zep Zone</string>
<string name="preferences_zep_zone_description">Advanced emulation settings</string> <string name="preferences_zep_zone_description">Advanced emulation settings</string>
<string name="preferences_applets_android">Applets on Android</string>
<string name="preferences_applets_android_description">System applet configuration settings</string>
<!-- Zep Zone Headers --> <!-- Zep Zone Headers -->
<string name="memory_layout_header">Memory Layout</string> <string name="memory_layout_header">Memory Layout</string>

View File

@@ -634,7 +634,7 @@ struct Values {
Setting<bool> perform_vulkan_check{linkage, true, "perform_vulkan_check", Category::Debugging}; Setting<bool> perform_vulkan_check{linkage, true, "perform_vulkan_check", Category::Debugging};
// Miscellaneous // Miscellaneous
Setting<std::string> log_filter{linkage, "*:Info", "log_filter", Category::Miscellaneous}; Setting<std::string> log_filter{linkage, "*:Info", "log_filter", Category::Debugging};
Setting<bool> use_dev_keys{linkage, false, "use_dev_keys", Category::Miscellaneous}; Setting<bool> use_dev_keys{linkage, false, "use_dev_keys", Category::Miscellaneous};
// Network // Network