feat(ui): implement theme selection dialog with TV support

This commit is contained in:
2026-03-28 21:55:38 +01:00
parent 2ccb4225d4
commit a5b11ba99a
4 changed files with 231 additions and 22 deletions

View File

@@ -84,6 +84,7 @@ object Keys {
const val DIALOG_REMOVE_STATION: Int = 2 const val DIALOG_REMOVE_STATION: Int = 2
const val DIALOG_UPDATE_STATION_IMAGES: Int = 4 const val DIALOG_UPDATE_STATION_IMAGES: Int = 4
const val DIALOG_RESTORE_COLLECTION: Int = 5 const val DIALOG_RESTORE_COLLECTION: Int = 5
const val DIALOG_THEME_SELECTION: Int = 6
// dialog results // dialog results
const val DIALOG_EMPTY_PAYLOAD_STRING: String = "" const val DIALOG_EMPTY_PAYLOAD_STRING: String = ""

View File

@@ -5,6 +5,7 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -16,8 +17,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.* import androidx.preference.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.michatec.radio.dialogs.ErrorDialog import com.michatec.radio.dialogs.ErrorDialog
import com.michatec.radio.dialogs.ThemeSelectionDialog
import com.michatec.radio.dialogs.YesNoDialog import com.michatec.radio.dialogs.YesNoDialog
import com.michatec.radio.helpers.* import com.michatec.radio.helpers.*
import com.michatec.radio.helpers.AppThemeHelper.getColor import com.michatec.radio.helpers.AppThemeHelper.getColor
@@ -31,7 +34,7 @@ import java.util.*
/* /*
* SettingsFragment class * SettingsFragment class
*/ */
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener { class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener {
/* Define log tag */ /* Define log tag */
@@ -55,35 +58,24 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
val screen = preferenceManager.createPreferenceScreen(context) val screen = preferenceManager.createPreferenceScreen(context)
// set up "App Theme" preference // set up "App Theme" preference
val preferenceThemeSelection = ListPreference(activity as Context) val preferenceThemeSelection = Preference(activity as Context)
preferenceThemeSelection.title = getString(R.string.pref_theme_selection_title) preferenceThemeSelection.title = getString(R.string.pref_theme_selection_title)
preferenceThemeSelection.setIcon(R.drawable.ic_brush_24dp) preferenceThemeSelection.setIcon(R.drawable.ic_brush_24dp)
preferenceThemeSelection.key = Keys.PREF_THEME_SELECTION preferenceThemeSelection.key = Keys.PREF_THEME_SELECTION
preferenceThemeSelection.summary = "${getString(R.string.pref_theme_selection_summary)} ${ preferenceThemeSelection.summary = "${getString(R.string.pref_theme_selection_summary)} ${
AppThemeHelper.getCurrentTheme(activity as Context) AppThemeHelper.getCurrentTheme(activity as Context)
}" }"
preferenceThemeSelection.entries = arrayOf( preferenceThemeSelection.setOnPreferenceClickListener {
getString(R.string.pref_theme_selection_mode_device_default), // check if device is a TV
getString(R.string.pref_theme_selection_mode_light), val isTv = requireContext().packageManager.hasSystemFeature(android.content.pm.PackageManager.FEATURE_LEANBACK)
getString(R.string.pref_theme_selection_mode_dark) if (isTv) {
) // show TV-specific theme selection dialog
preferenceThemeSelection.entryValues = arrayOf( ThemeSelectionDialog(this).show(activity as Context)
Keys.STATE_THEME_FOLLOW_SYSTEM,
Keys.STATE_THEME_LIGHT_MODE,
Keys.STATE_THEME_DARK_MODE
)
preferenceThemeSelection.setDefaultValue(Keys.STATE_THEME_FOLLOW_SYSTEM)
preferenceThemeSelection.setOnPreferenceChangeListener { preference, newValue ->
if (preference is ListPreference) {
val index: Int = preference.entryValues.indexOf(newValue)
preferenceThemeSelection.summary =
"${getString(R.string.pref_theme_selection_summary)} ${preference.entries[index]}"
AppThemeHelper.setTheme(newValue as String)
return@setOnPreferenceChangeListener true
} else { } else {
return@setOnPreferenceChangeListener false // show standard theme selection dialog for non-TV devices
showThemeSelectionDialog()
} }
return@setOnPreferenceClickListener true
} }
// set up "Update Station Images" preference // set up "Update Station Images" preference
@@ -306,6 +298,57 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
} }
/* Shows theme selection dialog for non-TV devices */
private fun showThemeSelectionDialog() {
val themes = arrayOf(
getString(R.string.pref_theme_selection_mode_device_default),
getString(R.string.pref_theme_selection_mode_light),
getString(R.string.pref_theme_selection_mode_dark)
)
val themeValues = arrayOf(
Keys.STATE_THEME_FOLLOW_SYSTEM,
Keys.STATE_THEME_LIGHT_MODE,
Keys.STATE_THEME_DARK_MODE
)
val currentTheme = AppThemeHelper.getCurrentTheme(activity as Context)
val currentIndex = themes.indexOf(currentTheme)
val builder = MaterialAlertDialogBuilder(activity as Context)
builder.setTitle(getString(R.string.pref_theme_selection_title))
builder.setSingleChoiceItems(themes, currentIndex) { dialog, which ->
val selectedTheme = themeValues[which]
AppThemeHelper.setTheme(selectedTheme)
// update summary
val preferenceThemeSelection = findPreference<Preference>(Keys.PREF_THEME_SELECTION)
preferenceThemeSelection?.summary = "${getString(R.string.pref_theme_selection_summary)} ${themes[which]}"
dialog.dismiss()
}
builder.setNegativeButton(R.string.dialog_generic_button_cancel, null)
builder.show()
}
/* Overrides onThemeSelectionDialog from ThemeSelectionDialogListener */
override fun onThemeSelectionDialog(dialogResult: Boolean, selectedTheme: String) {
if (dialogResult) {
// update summary
val themes = arrayOf(
getString(R.string.pref_theme_selection_mode_device_default),
getString(R.string.pref_theme_selection_mode_light),
getString(R.string.pref_theme_selection_mode_dark)
)
val themeValues = arrayOf(
Keys.STATE_THEME_FOLLOW_SYSTEM,
Keys.STATE_THEME_LIGHT_MODE,
Keys.STATE_THEME_DARK_MODE
)
val index = themeValues.indexOf(selectedTheme)
val preferenceThemeSelection = findPreference<Preference>(Keys.PREF_THEME_SELECTION)
preferenceThemeSelection?.summary = "${getString(R.string.pref_theme_selection_summary)} ${themes[index]}"
}
}
/* Overrides onYesNoDialog from YesNoDialogListener */ /* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog( override fun onYesNoDialog(
type: Int, type: Int,

View File

@@ -0,0 +1,101 @@
package com.michatec.radio.dialogs
import android.content.Context
import android.view.LayoutInflater
import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.michatec.radio.Keys
import com.michatec.radio.R
import com.michatec.radio.helpers.AppThemeHelper
/*
* ThemeSelectionDialog class
*/
class ThemeSelectionDialog(private var themeSelectionDialogListener: ThemeSelectionDialogListener) {
/* Interface used to communicate back to activity */
interface ThemeSelectionDialogListener {
fun onThemeSelectionDialog(dialogResult: Boolean, selectedTheme: String) {
}
}
/* Main class variables */
private lateinit var dialog: AlertDialog
/* Construct and show dialog */
fun show(context: Context) {
// prepare dialog builder
val builder = MaterialAlertDialogBuilder(context)
// inflate custom layout
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.dialog_theme_selection, null)
// find radio buttons
val radioGroup = view.findViewById<android.widget.RadioGroup>(R.id.theme_radio_group)
val radioFollowSystem = view.findViewById<RadioButton>(R.id.radio_theme_follow_system)
val radioLight = view.findViewById<RadioButton>(R.id.radio_theme_light)
val radioDark = view.findViewById<RadioButton>(R.id.radio_theme_dark)
// set current selection
val currentTheme = AppThemeHelper.getCurrentTheme(context)
when (currentTheme) {
context.getString(R.string.pref_theme_selection_mode_device_default) -> {
radioFollowSystem.isChecked = true
}
context.getString(R.string.pref_theme_selection_mode_light) -> {
radioLight.isChecked = true
}
context.getString(R.string.pref_theme_selection_mode_dark) -> {
radioDark.isChecked = true
}
}
// set up radio group listener
radioGroup.setOnCheckedChangeListener { _, checkedId ->
val selectedTheme = when (checkedId) {
R.id.radio_theme_follow_system -> Keys.STATE_THEME_FOLLOW_SYSTEM
R.id.radio_theme_light -> Keys.STATE_THEME_LIGHT_MODE
R.id.radio_theme_dark -> Keys.STATE_THEME_DARK_MODE
else -> Keys.STATE_THEME_FOLLOW_SYSTEM
}
// apply theme immediately
AppThemeHelper.setTheme(selectedTheme)
}
// set custom view
builder.setView(view)
// add OK button
builder.setPositiveButton(R.string.dialog_generic_button_ok) { _, _ ->
// get selected theme
val selectedTheme = when (radioGroup.checkedRadioButtonId) {
R.id.radio_theme_follow_system -> Keys.STATE_THEME_FOLLOW_SYSTEM
R.id.radio_theme_light -> Keys.STATE_THEME_LIGHT_MODE
R.id.radio_theme_dark -> Keys.STATE_THEME_DARK_MODE
else -> Keys.STATE_THEME_FOLLOW_SYSTEM
}
// notify listener
themeSelectionDialogListener.onThemeSelectionDialog(true, selectedTheme)
}
// add cancel button
builder.setNegativeButton(R.string.dialog_generic_button_cancel) { _, _ ->
// notify listener
themeSelectionDialogListener.onThemeSelectionDialog(false, Keys.STATE_THEME_FOLLOW_SYSTEM)
}
// handle outside-click as cancel
builder.setOnCancelListener {
themeSelectionDialogListener.onThemeSelectionDialog(false, Keys.STATE_THEME_FOLLOW_SYSTEM)
}
// display dialog
dialog = builder.create()
dialog.show()
}
}

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_theme_selection_title"
android:textSize="24sp"
android:textStyle="bold"
android:paddingBottom="16dp" />
<RadioGroup
android:id="@+id/theme_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/radio_theme_follow_system"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_theme_selection_mode_device_default"
android:textSize="20sp"
android:padding="12dp"
android:button="@null"
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="16dp"
android:focusable="true"
android:clickable="true" />
<RadioButton
android:id="@+id/radio_theme_light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_theme_selection_mode_light"
android:textSize="20sp"
android:padding="12dp"
android:button="@null"
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="16dp"
android:focusable="true"
android:clickable="true" />
<RadioButton
android:id="@+id/radio_theme_dark"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_theme_selection_mode_dark"
android:textSize="20sp"
android:padding="12dp"
android:button="@null"
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="16dp"
android:focusable="true"
android:clickable="true" />
</RadioGroup>
</LinearLayout>