diff --git a/app/build.gradle b/app/build.gradle index 743c59a7..2184ab6d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,4 +189,5 @@ dependencies { // android tv implementation 'androidx.leanback:leanback:1.1.0-rc02' implementation "androidx.leanback:leanback-tab:1.1.0-beta01" + implementation "androidx.leanback:leanback-preference:1.2.0-alpha02" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 21281e75..7a85ed8a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,6 +64,11 @@ android:screenOrientation="landscape" android:theme="@style/LeanbackAppTheme" /> + + - val uiMode = settingsRepository.prefValueToUiMode(newValue as String?) - AppCompatDelegate.setDefaultNightMode(uiMode) + private val channelSelectionClickListener = Preference.OnPreferenceClickListener { + val direction = + SettingsFragmentDirections.toChannelSelectionFragment() + findNavController().navigate(direction) true } - private val languageChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - val languageTag = newValue as String - val appLocale = LocaleListCompat.forLanguageTags(languageTag) - AppCompatDelegate.setApplicationLocales(appLocale) - - true - } - - override fun onAttach(context: Context) { - super.onAttach(context) - - settingsRepository = SettingsRepository(context) - } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences) - shortcutPreference = preferenceScreen.findPreference(PREF_SHORTCUTS)!! - dynamicColorsPreference = preferenceScreen.findPreference(PREF_DYNAMIC_COLORS)!! - uiModePreference = preferenceScreen.findPreference(PREF_UI_MODE)!! - languagePreference = preferenceScreen.findPreference(PREF_LANGUAGE)!! - channelSelectionPreference = preferenceScreen.findPreference(PREF_CHANNEL_SELECTION)!! - - val languages = LanguageHelper.getAvailableLanguages(requireContext()) - languagePreference.value = LanguageHelper.getCurrentLanguageTag() - languagePreference.entries = languages.values.toTypedArray() - languagePreference.entryValues = languages.keys.toTypedArray() - - // only show the preference for dynamic colors when available (Android 12 and up) - dynamicColorsPreference.isVisible = DynamicColors.isDynamicColorAvailable() - dynamicColorsPreference.setOnPreferenceChangeListener { _, useDynamicColors -> - // save explicitly to persist before app restart - settingsRepository.dynamicColors = useDynamicColors as Boolean - // app restart - ProcessPhoenix.triggerRebirth(context) - true - } - - channelSelectionPreference.setOnPreferenceClickListener { - val direction = - SettingsFragmentDirections.toChannelSelectionFragment() - findNavController().navigate(direction) - true - } + preferenceFragmentHelper.initPreferences(channelSelectionClickListener) } - override fun onResume() { - super.onResume() - - shortcutPreference.onPreferenceChangeListener = shortcutPreference - uiModePreference.onPreferenceChangeListener = uiModeChangeListener - languagePreference.onPreferenceChangeListener = languageChangeListener - } - - override fun onPause() { - super.onPause() - - shortcutPreference.onPreferenceChangeListener = null - uiModePreference.onPreferenceChangeListener = null - languagePreference.onPreferenceChangeListener = null + override fun onDestroy() { + super.onDestroy() + preferenceFragmentHelper.destroy() } } diff --git a/app/src/main/java/de/christinecoenen/code/zapp/tv/about/AboutFragment.kt b/app/src/main/java/de/christinecoenen/code/zapp/tv/about/AboutFragment.kt index 8d3dc6f9..31a6585e 100644 --- a/app/src/main/java/de/christinecoenen/code/zapp/tv/about/AboutFragment.kt +++ b/app/src/main/java/de/christinecoenen/code/zapp/tv/about/AboutFragment.kt @@ -19,7 +19,7 @@ class AboutFragment : Fragment(), AboutItemListener { val binding = TvFragmentAboutBinding.inflate(inflater, container, false) binding.grid.adapter = AboutListAdapter(this) - binding.grid.layoutManager = GridLayoutManager(requireContext(), 2) + binding.grid.layoutManager = GridLayoutManager(requireContext(), 3) return binding.root } diff --git a/app/src/main/java/de/christinecoenen/code/zapp/tv/about/AboutListAdapter.kt b/app/src/main/java/de/christinecoenen/code/zapp/tv/about/AboutListAdapter.kt index 4846aa33..2f3a8763 100644 --- a/app/src/main/java/de/christinecoenen/code/zapp/tv/about/AboutListAdapter.kt +++ b/app/src/main/java/de/christinecoenen/code/zapp/tv/about/AboutListAdapter.kt @@ -7,12 +7,18 @@ import de.christinecoenen.code.zapp.R import de.christinecoenen.code.zapp.databinding.TvAboutItemBinding import de.christinecoenen.code.zapp.tv.changelog.ChangelogActivity import de.christinecoenen.code.zapp.tv.faq.FaqActivity +import de.christinecoenen.code.zapp.tv.settings.SettingsActivity class AboutListAdapter( private val listener: AboutItemListener ) : RecyclerView.Adapter() { private val aboutItems = listOf( + AboutItem( + R.string.activity_settings_title, + R.drawable.ic_outline_settings_24, + SettingsActivity + ), AboutItem( R.string.changelog_title, R.drawable.ic_sharp_format_list_bulleted_24, diff --git a/app/src/main/java/de/christinecoenen/code/zapp/tv/main/MainFragment.kt b/app/src/main/java/de/christinecoenen/code/zapp/tv/main/MainFragment.kt index 0c17e205..c39f168c 100644 --- a/app/src/main/java/de/christinecoenen/code/zapp/tv/main/MainFragment.kt +++ b/app/src/main/java/de/christinecoenen/code/zapp/tv/main/MainFragment.kt @@ -5,7 +5,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import de.christinecoenen.code.zapp.R +import de.christinecoenen.code.zapp.app.settings.repository.SettingsRepository import de.christinecoenen.code.zapp.databinding.TvFragmentMainBinding +import org.koin.android.ext.android.inject class MainFragment : Fragment() { @@ -13,6 +16,8 @@ class MainFragment : Fragment() { private var _binding: TvFragmentMainBinding? = null private val binding: TvFragmentMainBinding get() = _binding!! + private val settingsRepository: SettingsRepository by inject() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -23,7 +28,13 @@ class MainFragment : Fragment() { binding.viewpager.adapter = MainNavPagerAdapter(requireContext(), parentFragmentManager) binding.tabs.setupWithViewPager(binding.viewpager) - binding.tabs.getTabAt(0)?.view?.requestFocus() + val selectedTabIndex = when (settingsRepository.startFragment) { + R.id.mediathekListFragment -> 1 + else -> 0 + } + val selectedTab = binding.tabs.getTabAt(selectedTabIndex) + binding.tabs.selectTab(selectedTab) + selectedTab?.view?.requestFocus() return binding.root } diff --git a/app/src/main/java/de/christinecoenen/code/zapp/tv/settings/PreferenceFragment.kt b/app/src/main/java/de/christinecoenen/code/zapp/tv/settings/PreferenceFragment.kt new file mode 100644 index 00000000..50671b96 --- /dev/null +++ b/app/src/main/java/de/christinecoenen/code/zapp/tv/settings/PreferenceFragment.kt @@ -0,0 +1,25 @@ +package de.christinecoenen.code.zapp.tv.settings + +import android.os.Bundle +import androidx.leanback.preference.LeanbackPreferenceFragmentCompat +import de.christinecoenen.code.zapp.R +import de.christinecoenen.code.zapp.app.settings.repository.SettingsRepository +import de.christinecoenen.code.zapp.utils.system.PreferenceFragmentHelper +import org.koin.android.ext.android.inject + +class PreferenceFragment : LeanbackPreferenceFragmentCompat() { + + private val settingsRepository: SettingsRepository by inject() + private val preferenceFragmentHelper = PreferenceFragmentHelper(this, settingsRepository) + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.tv_preferences, rootKey) + + preferenceFragmentHelper.initPreferences() + } + + override fun onDestroy() { + super.onDestroy() + preferenceFragmentHelper.destroy() + } +} diff --git a/app/src/main/java/de/christinecoenen/code/zapp/tv/settings/SettingsActivity.kt b/app/src/main/java/de/christinecoenen/code/zapp/tv/settings/SettingsActivity.kt new file mode 100644 index 00000000..988ad947 --- /dev/null +++ b/app/src/main/java/de/christinecoenen/code/zapp/tv/settings/SettingsActivity.kt @@ -0,0 +1,24 @@ +package de.christinecoenen.code.zapp.tv.settings + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.FragmentActivity +import de.christinecoenen.code.zapp.databinding.TvActivitySettingsBinding +import de.christinecoenen.code.zapp.utils.system.IStartableActivity + +class SettingsActivity : FragmentActivity() { + + companion object : IStartableActivity { + override fun getStartIntent(context: Context?): Intent = + Intent(context, SettingsActivity::class.java) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val binding = TvActivitySettingsBinding.inflate(layoutInflater) + + setContentView(binding.root) + } +} diff --git a/app/src/main/java/de/christinecoenen/code/zapp/tv/settings/SettingsFragment.kt b/app/src/main/java/de/christinecoenen/code/zapp/tv/settings/SettingsFragment.kt new file mode 100644 index 00000000..0cd9420d --- /dev/null +++ b/app/src/main/java/de/christinecoenen/code/zapp/tv/settings/SettingsFragment.kt @@ -0,0 +1,29 @@ +package de.christinecoenen.code.zapp.tv.settings + +import androidx.leanback.preference.LeanbackSettingsFragmentCompat +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceScreen + + +class SettingsFragment : LeanbackSettingsFragmentCompat() { + + override fun onPreferenceStartFragment( + caller: PreferenceFragmentCompat, + pref: Preference + ): Boolean { + return false + } + + override fun onPreferenceStartScreen( + caller: PreferenceFragmentCompat, + pref: PreferenceScreen + ): Boolean { + return false + } + + override fun onPreferenceStartInitialScreen() { + startPreferenceFragment(PreferenceFragment()) + } + +} diff --git a/app/src/main/java/de/christinecoenen/code/zapp/utils/system/PreferenceFragmentHelper.kt b/app/src/main/java/de/christinecoenen/code/zapp/utils/system/PreferenceFragmentHelper.kt new file mode 100644 index 00000000..70b630e9 --- /dev/null +++ b/app/src/main/java/de/christinecoenen/code/zapp/utils/system/PreferenceFragmentHelper.kt @@ -0,0 +1,110 @@ +package de.christinecoenen.code.zapp.utils.system + +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.Preference.OnPreferenceClickListener +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import com.google.android.material.color.DynamicColors +import com.jakewharton.processphoenix.ProcessPhoenix +import de.christinecoenen.code.zapp.app.settings.helper.ShortcutPreference +import de.christinecoenen.code.zapp.app.settings.repository.SettingsRepository + +class PreferenceFragmentHelper( + private val preferenceFragment: PreferenceFragmentCompat, + private val settingsRepository: SettingsRepository, +) : DefaultLifecycleObserver { + + companion object { + + private const val PREF_SHORTCUTS = "pref_shortcuts" + private const val PREF_DYNAMIC_COLORS = "dynamic_colors" + private const val PREF_UI_MODE = "pref_ui_mode" + private const val PREF_LANGUAGE = "pref_key_language" + private const val PREF_CHANNEL_SELECTION = "pref_key_channel_selection" + + } + + private var shortcutPreference: ShortcutPreference? = null + private var dynamicColorsPreference: SwitchPreferenceCompat? = null + private var uiModePreference: ListPreference? = null + private var languagePreference: ListPreference? = null + private var channelSelectionPreference: Preference? = null + + private var channelSelectionClickListener: OnPreferenceClickListener? = null + + private val dynamicColorChangeListener = Preference.OnPreferenceChangeListener { _, _ -> + ProcessPhoenix.triggerRebirth(preferenceFragment.requireContext()) + true + } + + private val uiModeChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val uiMode = settingsRepository.prefValueToUiMode(newValue as String?) + AppCompatDelegate.setDefaultNightMode(uiMode) + true + } + + private val languageChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val languageTag = newValue as String + val appLocale = LocaleListCompat.forLanguageTags(languageTag) + AppCompatDelegate.setApplicationLocales(appLocale) + true + } + + init { + preferenceFragment.lifecycle.addObserver(this) + } + + fun initPreferences(channelSelectionClickListener: OnPreferenceClickListener? = null) { + val preferenceScreen = preferenceFragment.preferenceScreen + + shortcutPreference = preferenceScreen.findPreference(PREF_SHORTCUTS) + dynamicColorsPreference = preferenceScreen.findPreference(PREF_DYNAMIC_COLORS) + uiModePreference = preferenceScreen.findPreference(PREF_UI_MODE) + languagePreference = preferenceScreen.findPreference(PREF_LANGUAGE) + channelSelectionPreference = preferenceScreen.findPreference(PREF_CHANNEL_SELECTION) + + languagePreference?.let { + val languages = + LanguageHelper.getAvailableLanguages(preferenceFragment.requireContext()) + it.value = LanguageHelper.getCurrentLanguageTag() + it.entries = languages.values.toTypedArray() + it.entryValues = languages.keys.toTypedArray() + } + + // only show the preference for dynamic colors when available (Android 12 and up) + dynamicColorsPreference?.let { + it.isVisible = DynamicColors.isDynamicColorAvailable() + } + + this.channelSelectionClickListener = channelSelectionClickListener + } + + fun destroy() { + preferenceFragment.lifecycle.removeObserver(this) + } + + override fun onStart(owner: LifecycleOwner) { + super.onStart(owner) + + shortcutPreference?.onPreferenceChangeListener = shortcutPreference + dynamicColorsPreference?.onPreferenceChangeListener = dynamicColorChangeListener + uiModePreference?.onPreferenceChangeListener = uiModeChangeListener + languagePreference?.onPreferenceChangeListener = languageChangeListener + channelSelectionPreference?.onPreferenceClickListener = channelSelectionClickListener + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + + shortcutPreference?.onPreferenceChangeListener = null + dynamicColorsPreference?.onPreferenceChangeListener = null + uiModePreference?.onPreferenceChangeListener = null + languagePreference?.onPreferenceChangeListener = null + channelSelectionPreference?.onPreferenceClickListener = null + } +} diff --git a/app/src/main/res/layout/tv_activity_settings.xml b/app/src/main/res/layout/tv_activity_settings.xml new file mode 100644 index 00000000..9031e63e --- /dev/null +++ b/app/src/main/res/layout/tv_activity_settings.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/src/main/res/layout/tv_fragment_about.xml b/app/src/main/res/layout/tv_fragment_about.xml index bf7015df..55d149ae 100644 --- a/app/src/main/res/layout/tv_fragment_about.xml +++ b/app/src/main/res/layout/tv_fragment_about.xml @@ -3,7 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:padding="64dp"> + app:layout_constraintTop_toTopOf="parent" /> personal + + @string/activity_main_tab_live + @string/activity_main_tab_mediathek + + + + live + mediathek + + @fraction/mediathek_filter_min_duration @fraction/mediathek_filter_max_duration diff --git a/app/src/main/res/values/styles_tv.xml b/app/src/main/res/values/styles_tv.xml index e3e4eec3..abc0d0ea 100644 --- a/app/src/main/res/values/styles_tv.xml +++ b/app/src/main/res/values/styles_tv.xml @@ -22,6 +22,10 @@ @color/colorPrimary + +