Add support for Android TV and update dependencies

*   Implement initial Android TV support, including `LEANBACK_LAUNCHER` intent filter, hardware feature declarations, and television-specific layouts for the player, search results, and dialogs.
*   Add a splash/loading screen for the TV interface and a dedicated `SplashTheme`.
*   Improve DPAD navigation by adding `OnKeyListener` for station cards and allowing focus on internal elements.
*   Update `LayoutHolder` and `PlayerFragment` to handle TV layouts and add previous/next station navigation buttons.
*   Adjust `PreferencesHelper` to disable station editing by default on TV devices.
*   Update `androidx.media3` to v1.10.0, `work-runtime-ktx` to v2.11.2, and add `androidx.leanback` dependency.
*   Bump `versionCode` to 144 and `versionName` to 14.4.
*   Refactor `PlayerService` to simplify sleep timer cancellation logic.
*   Remove stale copyright headers and license comments from several Kotlin files.
This commit is contained in:
2026-03-28 18:36:50 +01:00
parent 9140b54a23
commit 46ebf21c06
34 changed files with 990 additions and 196 deletions

View File

@@ -19,8 +19,8 @@ android {
applicationId 'com.michatec.radio'
minSdk 28
targetSdk 36
versionCode 143
versionName '14.3'
versionCode 144
versionName '14.4'
resourceConfigurations += ['en', 'de', 'el', 'nl', 'pl', 'ru','uk', 'ja', 'da', 'fr']
}
@@ -71,6 +71,7 @@ dependencies {
implementation libs.navigation.fragment.ktx
implementation libs.navigation.ui.ktx
implementation libs.work.runtime.ktx
implementation libs.leanback
implementation libs.freedroidwarn

View File

@@ -1,6 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
@@ -10,6 +17,7 @@
<application
android:name=".Radio"
android:allowBackup="true"
android:banner="@mipmap/ic_launcher"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
@@ -32,17 +40,19 @@
android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@mipmap/ic_launcher" />
<!-- Main activity for radio station playback on phone -->
<!-- Main activity for radio station playback on phone and TV -->
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:windowSoftInputMode="adjustPan"
android:theme="@style/SplashTheme"
android:exported="true">
<!-- react to main intents -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<!-- react to be recognized as a music player -->

View File

@@ -1,17 +1,3 @@
/*
* Keys.kt
* Implements the keys used throughout the app
* This object hosts all keys used to control Radio state
*
* This file is part of
* TRANSISTOR - Radio App for Android
*
* Copyright (c) 2015-22 - Y20K.org
* Licensed under the MIT-License
* http://opensource.org/licenses/MIT
*/
package com.michatec.radio
import java.util.*

View File

@@ -15,7 +15,11 @@
package com.michatec.radio
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.NavHostFragment
@@ -38,6 +42,7 @@ class MainActivity : AppCompatActivity() {
/* Overrides onCreate from AppCompatActivity */
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme)
super.onCreate(savedInstanceState)
// Free Android
@@ -58,6 +63,15 @@ class MainActivity : AppCompatActivity() {
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
supportActionBar?.hide()
// TV-specific loading logic
if (packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
Handler(Looper.getMainLooper()).postDelayed({
findViewById<View>(R.id.loading_layout)?.visibility = View.GONE
}, 1500)
} else {
findViewById<View>(R.id.loading_layout)?.visibility = View.GONE
}
// register listener for changes in shared preferences
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
}

View File

@@ -462,7 +462,7 @@ class PlayerFragment : Fragment(),
swipeToMarkStarredItemTouchHelper.attachToRecyclerView(layout.recyclerView)
// set up sleep timer start button
layout.sheetSleepTimerStartButtonView.setOnClickListener {
layout.sheetSleepTimerStartButtonView?.setOnClickListener {
when (controller?.isPlaying) {
true -> {
val timePicker = MaterialTimePicker.Builder()
@@ -492,12 +492,28 @@ class PlayerFragment : Fragment(),
}
// set up sleep timer cancel button
layout.sheetSleepTimerCancelButtonView.setOnClickListener {
layout.sheetSleepTimerCancelButtonView?.setOnClickListener {
playerState.sleepTimerRunning = false
controller?.cancelSleepTimer()
togglePeriodicSleepTimerUpdateRequest()
}
// set up TV station navigation
layout.playerPrevButtonView?.setOnClickListener {
val currentPosition = CollectionHelper.getStationPosition(collection, playerState.stationUuid)
if (currentPosition > 0) {
val prevStation = collection.stations[currentPosition - 1]
onPlayButtonTapped(prevStation.uuid)
}
}
layout.playerNextButtonView?.setOnClickListener {
val currentPosition = CollectionHelper.getStationPosition(collection, playerState.stationUuid)
if (currentPosition < collection.stations.size - 1) {
val nextStation = collection.stations[currentPosition + 1]
onPlayButtonTapped(nextStation.uuid)
}
}
}
/* Sets up the player */
@@ -740,6 +756,7 @@ class PlayerFragment : Fragment(),
layout.showBufferingIndicator(buffering = false)
} else {
// playback is paused or stopped
layout.updateSleepTimer(activity as Context, 0L)
// check if buffering (playback is not active but playWhenReady is true)
if (controller?.playWhenReady == true) {
// playback is buffering, show the buffering indicator

View File

@@ -210,12 +210,9 @@ class PlayerService : MediaLibraryService() {
/* Cancels sleep timer */
private fun cancelSleepTimer() {
if (this::sleepTimer.isInitialized) {
if (manuallyCancelledSleepTimer) {
sleepTimerTimeRemaining = 0L
sleepTimer.cancel()
}
manuallyCancelledSleepTimer = false
sleepTimer.cancel()
}
sleepTimerTimeRemaining = 0L
// store timer state
PreferencesHelper.saveSleepTimerRunning(isRunning = false)
}

View File

@@ -1,17 +1,3 @@
/*
* SettingsFragment.kt
* Implements the SettingsFragment fragment
* A SettingsFragment displays the user accessible settings of the app
*
* This file is part of
* TRANSISTOR - Radio App for Android
*
* Copyright (c) 2015-22 - Y20K.org
* Licensed under the MIT-License
* http://opensource.org/licenses/MIT
*/
package com.michatec.radio
import android.app.Activity
@@ -92,6 +78,8 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
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 {
return@setOnPreferenceChangeListener false
@@ -193,7 +181,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceEnableEditingStreamUri.key = Keys.PREF_EDIT_STREAMS_URIS
preferenceEnableEditingStreamUri.summaryOn = getString(R.string.pref_edit_station_stream_summary_enabled)
preferenceEnableEditingStreamUri.summaryOff = getString(R.string.pref_edit_station_stream_summary_disabled)
preferenceEnableEditingStreamUri.setDefaultValue(PreferencesHelper.loadEditStreamUrisEnabled())
preferenceEnableEditingStreamUri.setDefaultValue(PreferencesHelper.loadEditStreamUrisEnabled(context))
// set up "Edit Stations" preference
@@ -203,7 +191,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceEnableEditingGeneral.key = Keys.PREF_EDIT_STATIONS
preferenceEnableEditingGeneral.summaryOn = getString(R.string.pref_edit_station_summary_enabled)
preferenceEnableEditingGeneral.summaryOff = getString(R.string.pref_edit_station_summary_disabled)
preferenceEnableEditingGeneral.setDefaultValue(PreferencesHelper.loadEditStationsEnabled())
preferenceEnableEditingGeneral.setDefaultValue(PreferencesHelper.loadEditStationsEnabled(context))
preferenceEnableEditingGeneral.setOnPreferenceChangeListener { _, newValue ->
when (newValue) {
true -> {

View File

@@ -19,6 +19,7 @@ import android.content.Context
import android.content.SharedPreferences
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -62,8 +63,8 @@ class CollectionAdapter(
/* Main class variables */
private lateinit var collectionViewModel: CollectionViewModel
private var collection: Collection = Collection()
private var editStationsEnabled: Boolean = PreferencesHelper.loadEditStationsEnabled()
private var editStationStreamsEnabled: Boolean = PreferencesHelper.loadEditStreamUrisEnabled()
private var editStationsEnabled: Boolean = PreferencesHelper.loadEditStationsEnabled(context)
private var editStationStreamsEnabled: Boolean = PreferencesHelper.loadEditStreamUrisEnabled(context)
private var expandedStationUuid: String = PreferencesHelper.loadStationListStreamUuid()
private var expandedStationPosition: Int = -1
var isExpandedForEdit: Boolean = false
@@ -214,6 +215,8 @@ class CollectionAdapter(
stationViewHolder.stationNameEditView.imeOptions =
EditorInfo.IME_ACTION_DONE
}
// Allow internal focus
stationViewHolder.stationCardView.descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS
}
// hide edit views
else -> {
@@ -222,6 +225,8 @@ class CollectionAdapter(
stationViewHolder.stationStarredView.isVisible = station.starred
stationViewHolder.editViews.isGone = true
stationViewHolder.stationUriEditView.isGone = true
// Block internal focus
stationViewHolder.stationCardView.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
}
}
}
@@ -387,6 +392,7 @@ class CollectionAdapter(
false -> stationViewHolder.playButtonView.visibility = View.INVISIBLE
}
stationViewHolder.stationCardView.setOnClickListener {
if (expandedStationPosition == stationViewHolder.bindingAdapterPosition) return@setOnClickListener
collectionAdapterListener.onPlayButtonTapped(station.uuid)
}
stationViewHolder.playButtonView.setOnClickListener {
@@ -401,6 +407,29 @@ class CollectionAdapter(
stationViewHolder.stationImageView.setOnClickListener {
collectionAdapterListener.onPlayButtonTapped(station.uuid)
}
// TV improvement: Allow opening edit view with DPAD_LEFT
stationViewHolder.stationCardView.setOnKeyListener { _, keyCode, event ->
if (event.action == KeyEvent.ACTION_DOWN) {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> {
if (editStationsEnabled && expandedStationPosition != stationViewHolder.bindingAdapterPosition) {
val position: Int = stationViewHolder.bindingAdapterPosition
toggleEditViews(position, station.uuid)
return@setOnKeyListener true
}
}
KeyEvent.KEYCODE_BACK -> {
if (expandedStationPosition == stationViewHolder.bindingAdapterPosition) {
toggleEditViews(stationViewHolder.bindingAdapterPosition, station.uuid)
return@setOnKeyListener true
}
}
}
}
false
}
stationViewHolder.playButtonView.setOnLongClickListener {
if (editStationsEnabled) {
val position: Int = stationViewHolder.bindingAdapterPosition
@@ -649,9 +678,9 @@ class CollectionAdapter(
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
when (key) {
Keys.PREF_EDIT_STATIONS -> editStationsEnabled =
PreferencesHelper.loadEditStationsEnabled()
PreferencesHelper.loadEditStationsEnabled(context)
Keys.PREF_EDIT_STREAMS_URIS -> editStationStreamsEnabled =
PreferencesHelper.loadEditStreamUrisEnabled()
PreferencesHelper.loadEditStreamUrisEnabled(context)
}
}
/*

View File

@@ -16,7 +16,9 @@ package com.michatec.radio.dialogs
import android.content.Context
import android.view.LayoutInflater
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -45,6 +47,8 @@ class AddStationDialog (
/* Main class variables */
private lateinit var dialog: AlertDialog
private lateinit var stationSearchResultList: RecyclerView
private var customPositiveButton: Button? = null
private var customNegativeButton: Button? = null
private lateinit var searchResultAdapter: SearchResultAdapter
private var station: Station = Station()
@@ -73,6 +77,10 @@ class AddStationDialog (
// set up list of search results
setupRecyclerView(context)
// find custom buttons (for TV layout)
customPositiveButton = view.findViewById(R.id.dialog_positive_button)
customNegativeButton = view.findViewById(R.id.dialog_negative_button)
// add okay ("Add") button
builder.setPositiveButton(R.string.dialog_find_station_button_add) { _, _ ->
// listen for click on add button
@@ -88,6 +96,17 @@ class AddStationDialog (
searchResultAdapter.stopPrePlayback()
}
// set up custom buttons if they exist (TV layout)
customPositiveButton?.setOnClickListener {
listener.onAddStationDialog(station)
searchResultAdapter.stopPrePlayback()
dialog.dismiss()
}
customNegativeButton?.setOnClickListener {
searchResultAdapter.stopPrePlayback()
dialog.dismiss()
}
// set dialog view
builder.setView(view)
@@ -95,8 +114,16 @@ class AddStationDialog (
dialog = builder.create()
dialog.show()
// initially disable "Add" button
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
// handle button visibility and state
if (customPositiveButton != null) {
// hide default buttons if custom ones are used
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isGone = true
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isGone = true
customPositiveButton?.isEnabled = false
} else {
// initially disable default "Add" button
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
}
}
@@ -117,12 +144,14 @@ class AddStationDialog (
/* Implement activateAddButton to enable the "Add" button */
override fun activateAddButton() {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
customPositiveButton?.isEnabled = true
}
/* Implement deactivateAddButton to disable the "Add" button */
override fun deactivateAddButton() {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
customPositiveButton?.isEnabled = false
}

View File

@@ -21,6 +21,7 @@ import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.ProgressBar
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
@@ -63,6 +64,8 @@ class FindStationDialog (
private lateinit var searchRequestProgressIndicator: ProgressBar
private lateinit var noSearchResultsTextView: MaterialTextView
private lateinit var stationSearchResultList: RecyclerView
private var customPositiveButton: Button? = null
private var customNegativeButton: Button? = null
private lateinit var searchResultAdapter: SearchResultAdapter
private lateinit var radioBrowserSearch: RadioBrowserSearch
private lateinit var directInputCheck: DirectInputCheck
@@ -134,6 +137,10 @@ class FindStationDialog (
// set up list of search results
setupRecyclerView(context)
// find custom buttons (for TV layout)
customPositiveButton = view.findViewById(R.id.dialog_positive_button)
customNegativeButton = view.findViewById(R.id.dialog_negative_button)
// add okay ("Add") button
builder.setPositiveButton(R.string.dialog_find_station_button_add) { _, _ ->
// listen for click on add button
@@ -152,6 +159,18 @@ class FindStationDialog (
searchResultAdapter.stopPrePlayback()
}
// set up custom buttons if they exist (TV layout)
customPositiveButton?.setOnClickListener {
listener.onFindStationDialog(station)
searchResultAdapter.stopPrePlayback()
dialog.dismiss()
}
customNegativeButton?.setOnClickListener {
radioBrowserSearch.stopSearchRequest()
searchResultAdapter.stopPrePlayback()
dialog.dismiss()
}
// listen for input
stationSearchBoxView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextChange(query: String): Boolean {
@@ -174,10 +193,18 @@ class FindStationDialog (
dialog = builder.create()
dialog.show()
// initially disable "Add" button
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isAllCaps = true
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isAllCaps = true
// handle button visibility and state
if (customPositiveButton != null) {
// hide default buttons if custom ones are used
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isGone = true
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isGone = true
customPositiveButton?.isEnabled = false
} else {
// initially disable default "Add" button
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isAllCaps = true
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isAllCaps = true
}
}
@@ -242,12 +269,14 @@ class FindStationDialog (
/* Makes the "Add" button clickable */
override fun activateAddButton() {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
customPositiveButton?.isEnabled = true
searchRequestProgressIndicator.isGone = true
noSearchResultsTextView.isGone = true
}
override fun deactivateAddButton() {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
customPositiveButton?.isEnabled = false
searchRequestProgressIndicator.isGone = true
noSearchResultsTextView.isGone = true
}
@@ -256,6 +285,7 @@ class FindStationDialog (
/* Resets the dialog layout to default state */
private fun resetLayout(clearAdapter: Boolean = false) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
customPositiveButton?.isEnabled = false
searchRequestProgressIndicator.isGone = true
noSearchResultsTextView.isGone = true
searchResultAdapter.resetSelection(clearAdapter)
@@ -265,6 +295,7 @@ class FindStationDialog (
/* Display the "No Results" error - hide other unneeded views */
private fun showNoResultsError() {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
customPositiveButton?.isEnabled = false
searchRequestProgressIndicator.isGone = true
noSearchResultsTextView.isVisible = true
}
@@ -273,6 +304,7 @@ class FindStationDialog (
/* Display the "No Results" error - hide other unneeded views */
private fun showProgressIndicator() {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
customPositiveButton?.isEnabled = false
searchRequestProgressIndicator.isVisible = true
noSearchResultsTextView.isGone = true
}

View File

@@ -16,6 +16,7 @@ package com.michatec.radio.helpers
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.util.Log
import androidx.core.content.edit
import androidx.preference.PreferenceManager
@@ -223,13 +224,15 @@ object PreferencesHelper {
/* Loads value of the option: Edit Stations */
fun loadEditStationsEnabled(): Boolean {
return sharedPreferences.getBoolean(Keys.PREF_EDIT_STATIONS, true)
fun loadEditStationsEnabled(context: Context): Boolean {
val defaultValue = !context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
return sharedPreferences.getBoolean(Keys.PREF_EDIT_STATIONS, defaultValue)
}
/* Loads value of the option: Edit Station Streams */
fun loadEditStreamUrisEnabled(): Boolean {
return sharedPreferences.getBoolean(Keys.PREF_EDIT_STREAMS_URIS, true)
fun loadEditStreamUrisEnabled(context: Context): Boolean {
val defaultValue = !context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
return sharedPreferences.getBoolean(Keys.PREF_EDIT_STREAMS_URIS, defaultValue)
}

View File

@@ -1,17 +1,3 @@
/*
* LayoutHolder.kt
* Implements the LayoutHolder class
* A LayoutHolder hold references to the main views
*
* This file is part of
* TRANSISTOR - Radio App for Android
*
* Copyright (c) 2015-22 - Y20K.org
* Licensed under the MIT-License
* http://opensource.org/licenses/MIT
*/
package com.michatec.radio.ui
import android.annotation.SuppressLint
@@ -54,30 +40,32 @@ data class LayoutHolder(var rootView: View) {
/* Main class variables */
var recyclerView: RecyclerView = rootView.findViewById(R.id.station_list)
val layoutManager: LinearLayoutManager
private var bottomSheet: ConstraintLayout = rootView.findViewById(R.id.bottom_sheet)
private var bottomSheet: ConstraintLayout? = rootView.findViewById(R.id.bottom_sheet)
//private var sheetMetadataViews: Group
private var sleepTimerRunningViews: Group = rootView.findViewById(R.id.sleep_timer_running_views)
private var downloadProgressIndicator: ProgressBar = rootView.findViewById(R.id.download_progress_indicator)
private var stationImageView: ImageView = rootView.findViewById(R.id.station_icon)
private var stationNameView: TextView = rootView.findViewById(R.id.player_station_name)
private var metadataView: TextView = rootView.findViewById(R.id.player_station_metadata)
private var sleepTimerRunningViews: Group? = rootView.findViewById(R.id.sleep_timer_running_views)
private var downloadProgressIndicator: ProgressBar? = rootView.findViewById(R.id.download_progress_indicator)
private var stationImageView: ImageView? = rootView.findViewById(R.id.station_icon)
private var stationNameView: TextView? = rootView.findViewById(R.id.player_station_name)
private var metadataView: TextView? = rootView.findViewById(R.id.player_station_metadata)
var playButtonView: ImageButton = rootView.findViewById(R.id.player_play_button)
var playerPrevButtonView: ImageButton? = rootView.findViewById(R.id.player_prev_button)
var playerNextButtonView: ImageButton? = rootView.findViewById(R.id.player_next_button)
private var bufferingIndicator: ProgressBar = rootView.findViewById(R.id.player_buffering_indicator)
private var sheetStreamingLinkHeadline: TextView = rootView.findViewById(R.id.sheet_streaming_link_headline)
private var sheetStreamingLinkView: TextView = rootView.findViewById(R.id.sheet_streaming_link)
private var sheetMetadataHistoryHeadline: TextView = rootView.findViewById(R.id.sheet_metadata_headline)
private var sheetMetadataHistoryView: TextView = rootView.findViewById(R.id.sheet_metadata_history)
private var sheetNextMetadataView: ImageButton = rootView.findViewById(R.id.sheet_next_metadata_button)
private var sheetPreviousMetadataView: ImageButton = rootView.findViewById(R.id.sheet_previous_metadata_button)
private var sheetCopyMetadataButtonView: ImageButton = rootView.findViewById(R.id.copy_station_metadata_button)
private var sheetShareLinkButtonView: ImageView = rootView.findViewById(R.id.sheet_share_link_button)
private var sheetBitrateView: TextView = rootView.findViewById(R.id.sheet_bitrate_view)
var sheetSleepTimerStartButtonView: ImageButton = rootView.findViewById(R.id.sleep_timer_start_button)
var sheetSleepTimerCancelButtonView: ImageButton = rootView.findViewById(R.id.sleep_timer_cancel_button)
private var sheetStreamingLinkHeadline: TextView? = rootView.findViewById(R.id.sheet_streaming_link_headline)
private var sheetStreamingLinkView: TextView? = rootView.findViewById(R.id.sheet_streaming_link)
private var sheetMetadataHistoryHeadline: TextView? = rootView.findViewById(R.id.sheet_metadata_headline)
private var sheetMetadataHistoryView: TextView? = rootView.findViewById(R.id.sheet_metadata_history)
private var sheetNextMetadataView: ImageButton? = rootView.findViewById(R.id.sheet_next_metadata_button)
private var sheetPreviousMetadataView: ImageButton? = rootView.findViewById(R.id.sheet_previous_metadata_button)
private var sheetCopyMetadataButtonView: ImageButton? = rootView.findViewById(R.id.copy_station_metadata_button)
private var sheetShareLinkButtonView: ImageView? = rootView.findViewById(R.id.sheet_share_link_button)
private var sheetBitrateView: TextView? = rootView.findViewById(R.id.sheet_bitrate_view)
var sheetSleepTimerStartButtonView: ImageButton? = rootView.findViewById(R.id.sleep_timer_start_button)
var sheetSleepTimerCancelButtonView: ImageButton? = rootView.findViewById(R.id.sleep_timer_cancel_button)
private var sheetSleepTimerRemainingTimeView: TextView = rootView.findViewById(R.id.sleep_timer_remaining_time)
private var onboardingLayout: ConstraintLayout = rootView.findViewById(R.id.onboarding_layout)
private var bottomSheetBehavior: BottomSheetBehavior<ConstraintLayout> = BottomSheetBehavior.from(bottomSheet)
private var bottomSheetBehavior: BottomSheetBehavior<ConstraintLayout>? = bottomSheet?.let { BottomSheetBehavior.from(it) }
private var metadataHistory: MutableList<String>
private var metadataHistoryPosition: Int
private var isBuffering: Boolean
@@ -97,31 +85,31 @@ data class LayoutHolder(var rootView: View) {
recyclerView.itemAnimator = DefaultItemAnimator()
// set up metadata history next and previous buttons
sheetPreviousMetadataView.setOnClickListener {
sheetPreviousMetadataView?.setOnClickListener {
if (metadataHistory.isNotEmpty()) {
if (metadataHistoryPosition > 0) {
metadataHistoryPosition -= 1
} else {
metadataHistoryPosition = metadataHistory.size - 1
}
sheetMetadataHistoryView.text = metadataHistory[metadataHistoryPosition]
sheetMetadataHistoryView?.text = metadataHistory[metadataHistoryPosition]
}
}
sheetNextMetadataView.setOnClickListener {
sheetNextMetadataView?.setOnClickListener {
if (metadataHistory.isNotEmpty()) {
if (metadataHistoryPosition < metadataHistory.size - 1) {
metadataHistoryPosition += 1
} else {
metadataHistoryPosition = 0
}
sheetMetadataHistoryView.text = metadataHistory[metadataHistoryPosition]
sheetMetadataHistoryView?.text = metadataHistory[metadataHistoryPosition]
}
}
sheetMetadataHistoryView.setOnLongClickListener {
sheetMetadataHistoryView?.setOnLongClickListener {
copyMetadataHistoryToClipboard()
return@setOnLongClickListener true
}
sheetMetadataHistoryHeadline.setOnLongClickListener {
sheetMetadataHistoryHeadline?.setOnLongClickListener {
copyMetadataHistoryToClipboard()
return@setOnLongClickListener true
}
@@ -137,29 +125,29 @@ data class LayoutHolder(var rootView: View) {
// set default metadata views, when playback has stopped
if (!isPlaying) {
metadataView.text = station.name
sheetMetadataHistoryView.text = station.name
metadataView?.text = station.name
sheetMetadataHistoryView?.text = station.name
// sheetMetadataHistoryView.isSelected = true
}
// update name
stationNameView.text = station.name
stationNameView?.text = station.name
// toggle text scrolling (marquee) if necessary
stationNameView.isSelected = isPlaying
stationNameView?.isSelected = isPlaying
// reduce the shadow left and right because of scrolling (Marquee)
stationNameView.setFadingEdgeLength(8)
stationNameView?.setFadingEdgeLength(8)
// update cover
if (station.imageColor != -1) {
stationImageView.setBackgroundColor(station.imageColor)
stationImageView?.setBackgroundColor(station.imageColor)
}
stationImageView.setImageBitmap(ImageHelper.getStationImage(context, station.smallImage))
stationImageView.contentDescription = "${context.getString(R.string.descr_player_station_image)}: ${station.name}"
stationImageView?.setImageBitmap(ImageHelper.getStationImage(context, station.smallImage))
stationImageView?.contentDescription = "${context.getString(R.string.descr_player_station_image)}: ${station.name}"
// update streaming link
sheetStreamingLinkView.text = station.getStreamUri()
sheetStreamingLinkView?.text = station.getStreamUri()
val bitrateText: CharSequence = if (station.codec.isNotEmpty()) {
if (station.bitrate == 0) {
@@ -188,50 +176,50 @@ data class LayoutHolder(var rootView: View) {
}
// update bitrate
sheetBitrateView.text = bitrateText
sheetBitrateView?.text = bitrateText
// update click listeners
sheetStreamingLinkHeadline.setOnClickListener {
sheetStreamingLinkHeadline?.setOnClickListener {
copyToClipboard(
context,
sheetStreamingLinkView.text
sheetStreamingLinkView?.text ?: ""
)
}
sheetStreamingLinkView.setOnClickListener {
sheetStreamingLinkView?.setOnClickListener {
copyToClipboard(
context,
sheetStreamingLinkView.text
sheetStreamingLinkView?.text ?: ""
)
}
sheetMetadataHistoryHeadline.setOnClickListener {
sheetMetadataHistoryHeadline?.setOnClickListener {
copyToClipboard(
context,
sheetMetadataHistoryView.text
sheetMetadataHistoryView?.text ?: ""
)
}
sheetMetadataHistoryView.setOnClickListener {
sheetMetadataHistoryView?.setOnClickListener {
copyToClipboard(
context,
sheetMetadataHistoryView.text
sheetMetadataHistoryView?.text ?: ""
)
}
sheetCopyMetadataButtonView.setOnClickListener {
sheetCopyMetadataButtonView?.setOnClickListener {
copyToClipboard(
context,
sheetMetadataHistoryView.text
sheetMetadataHistoryView?.text ?: ""
)
}
sheetBitrateView.setOnClickListener {
sheetBitrateView?.setOnClickListener {
copyToClipboard(
context,
sheetBitrateView.text
sheetBitrateView?.text ?: ""
)
}
sheetShareLinkButtonView.setOnClickListener {
sheetShareLinkButtonView?.setOnClickListener {
val share = Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TITLE, stationNameView.text)
putExtra(Intent.EXTRA_TEXT, sheetStreamingLinkView.text)
putExtra(Intent.EXTRA_TITLE, stationNameView?.text)
putExtra(Intent.EXTRA_TEXT, sheetStreamingLinkView?.text ?: "")
type = "text/plain"
}, null)
context.startActivity(share)
@@ -264,11 +252,11 @@ data class LayoutHolder(var rootView: View) {
fun updateMetadata(metadataHistoryList: MutableList<String>?) {
if (!metadataHistoryList.isNullOrEmpty()) {
metadataHistory = metadataHistoryList
if (metadataHistory.last() != metadataView.text) {
if (metadataHistory.last() != metadataView?.text) {
metadataHistoryPosition = metadataHistory.size - 1
val metadataString = metadataHistory[metadataHistoryPosition]
metadataView.text = metadataString
sheetMetadataHistoryView.text = metadataString
metadataView?.text = metadataString
sheetMetadataHistoryView?.text = metadataString
}
}
}
@@ -278,14 +266,16 @@ data class LayoutHolder(var rootView: View) {
fun updateSleepTimer(context: Context, timeRemaining: Long = 0L) {
when (timeRemaining) {
0L -> {
sleepTimerRunningViews.isGone = true
sleepTimerRunningViews?.isGone = true
sheetSleepTimerRemainingTimeView.isVisible = false
}
else -> {
sleepTimerRunningViews.isVisible = true
sleepTimerRunningViews?.isVisible = true
sheetSleepTimerRemainingTimeView.isVisible = true
val sleepTimerTimeRemaining = DateTimeHelper.convertToHoursMinutesSeconds(timeRemaining)
sheetSleepTimerRemainingTimeView.text = sleepTimerTimeRemaining
sheetSleepTimerRemainingTimeView.contentDescription = "${context.getString(R.string.descr_expanded_player_sleep_timer_remaining_time)}: $sleepTimerTimeRemaining"
stationNameView.isSelected = false
stationNameView?.isSelected = false
}
}
}
@@ -297,11 +287,11 @@ data class LayoutHolder(var rootView: View) {
playButtonView.setImageResource(R.drawable.ic_audio_waves_animated)
val animatedVectorDrawable = playButtonView.drawable as? AnimatedVectorDrawable
animatedVectorDrawable?.start()
sheetSleepTimerStartButtonView.isVisible = true
sheetSleepTimerStartButtonView?.isVisible = true
// bufferingIndicator.isVisible = false
} else {
playButtonView.setImageResource(R.drawable.ic_player_play_symbol_42dp)
sheetSleepTimerStartButtonView.isVisible = false
sheetSleepTimerStartButtonView?.isVisible = false
// bufferingIndicator.isVisible = isBuffering
}
}
@@ -316,8 +306,8 @@ data class LayoutHolder(var rootView: View) {
/* Toggles visibility of the download progress indicator */
fun toggleDownloadProgressIndicator() {
when (PreferencesHelper.loadActiveDownloads()) {
Keys.ACTIVE_DOWNLOADS_EMPTY -> downloadProgressIndicator.isGone = true
else -> downloadProgressIndicator.isVisible = true
Keys.ACTIVE_DOWNLOADS_EMPTY -> downloadProgressIndicator?.isGone = true
else -> downloadProgressIndicator?.isVisible = true
}
}
@@ -338,27 +328,20 @@ data class LayoutHolder(var rootView: View) {
/* Initiates the rotation animation of the play button */
fun animatePlaybackButtonStateTransition(context: Context, isPlaying: Boolean) {
when (isPlaying) {
true -> {
val rotateClockwise = AnimationUtils.loadAnimation(context, R.anim.rotate_clockwise_slow)
rotateClockwise.setAnimationListener(createAnimationListener(true))
playButtonView.startAnimation(rotateClockwise)
}
false -> {
val rotateCounterClockwise = AnimationUtils.loadAnimation(context, R.anim.rotate_counterclockwise_fast)
rotateCounterClockwise.setAnimationListener(createAnimationListener(false))
playButtonView.startAnimation(rotateCounterClockwise)
}
}
// Toggle play button immediately for snappier feel
togglePlayButton(isPlaying)
}
/* Shows player */
fun showPlayer(context: Context): Boolean {
UiHelper.setViewMargins(context, recyclerView, 0, 0, 0, Keys.BOTTOM_SHEET_PEEK_HEIGHT)
if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_HIDDEN && onboardingLayout.isGone) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
if (bottomSheetBehavior != null) {
UiHelper.setViewMargins(context, recyclerView, 0, 0, 0, Keys.BOTTOM_SHEET_PEEK_HEIGHT)
if (bottomSheetBehavior?.state == BottomSheetBehavior.STATE_HIDDEN && onboardingLayout.isGone) {
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
}
} else {
UiHelper.setViewMargins(context, recyclerView, 0, 0, 0, 0)
}
return true
}
@@ -367,15 +350,15 @@ data class LayoutHolder(var rootView: View) {
/* Hides player */
private fun hidePlayer(context: Context): Boolean {
UiHelper.setViewMargins(context, recyclerView, 0, 0, 0, 0)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN
return true
}
/* Minimizes player sheet if expanded */
fun minimizePlayerIfExpanded(): Boolean {
return if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
return if (bottomSheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
true
} else {
false
@@ -399,38 +382,40 @@ data class LayoutHolder(var rootView: View) {
/* Sets up the player (BottomSheet) */
private fun setupBottomSheet() {
// show / hide the small player
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
bottomSheetBehavior.addBottomSheetCallback(object :
BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(view: View, slideOffset: Float) {
}
override fun onStateChanged(view: View, state: Int) {
when (state) {
BottomSheetBehavior.STATE_COLLAPSED -> Unit // do nothing
BottomSheetBehavior.STATE_DRAGGING -> Unit // do nothing
BottomSheetBehavior.STATE_EXPANDED -> Unit // do nothing
BottomSheetBehavior.STATE_HALF_EXPANDED -> Unit // do nothing
BottomSheetBehavior.STATE_SETTLING -> Unit // do nothing
BottomSheetBehavior.STATE_HIDDEN -> showPlayer(rootView.context)
if (bottomSheetBehavior != null) {
// show / hide the small player
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
bottomSheetBehavior?.addBottomSheetCallback(object :
BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(view: View, slideOffset: Float) {
}
}
})
// toggle collapsed state on tap
bottomSheet.setOnClickListener { toggleBottomSheetState() }
stationImageView.setOnClickListener { toggleBottomSheetState() }
stationNameView.setOnClickListener { toggleBottomSheetState() }
metadataView.setOnClickListener { toggleBottomSheetState() }
override fun onStateChanged(view: View, state: Int) {
when (state) {
BottomSheetBehavior.STATE_COLLAPSED -> Unit // do nothing
BottomSheetBehavior.STATE_DRAGGING -> Unit // do nothing
BottomSheetBehavior.STATE_EXPANDED -> Unit // do nothing
BottomSheetBehavior.STATE_HALF_EXPANDED -> Unit // do nothing
BottomSheetBehavior.STATE_SETTLING -> Unit // do nothing
BottomSheetBehavior.STATE_HIDDEN -> showPlayer(rootView.context)
}
}
})
// toggle collapsed state on tap
bottomSheet?.setOnClickListener { toggleBottomSheetState() }
stationImageView?.setOnClickListener { toggleBottomSheetState() }
stationNameView?.setOnClickListener { toggleBottomSheetState() }
metadataView?.setOnClickListener { toggleBottomSheetState() }
}
}
/* Toggle expanded/collapsed state of bottom sheet */
private fun toggleBottomSheetState() {
when (bottomSheetBehavior.state) {
BottomSheetBehavior.STATE_COLLAPSED -> bottomSheetBehavior.state =
when (bottomSheetBehavior?.state) {
BottomSheetBehavior.STATE_COLLAPSED -> bottomSheetBehavior?.state =
BottomSheetBehavior.STATE_EXPANDED
else -> bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
else -> bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/list_card_stroke_focused" android:state_focused="true" />
<item android:color="@color/list_card_stroke_background" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/search_result_background_selected" android:state_focused="true" />
<item android:drawable="@android:color/transparent" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/list_card_stroke_focused" android:state_focused="true" />
<item android:color="@color/list_card_stroke_background" />
</selector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="#80FFFFFF" />
<corners android:radius="8dp" />
<stroke android:width="3dp" android:color="@color/default_neutral_white" />
</shape>
</item>
<item android:drawable="@android:color/transparent" />
</selector>

View File

@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- View is "selected" -->
<!-- View is "selected" or "focused" (for TV) -->
<item android:drawable="@drawable/shape_search_result_item_selected" android:state_selected="true" />
<item android:drawable="@drawable/shape_search_result_item_selected" android:state_focused="true" />
<!-- Default state. -->
<item android:drawable="@drawable/shape_search_result_item" />

View File

@@ -1,8 +1,8 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="2dp"
android:color="@color/default_neutral_lighter" />
android:width="4dp"
android:color="@color/default_neutral_white" />
<size
android:width="56dp"
android:height="56dp" />

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashBackgroundColor" />
<item
android:width="160dp"
android:height="160dp"
android:gravity="center">
<bitmap
android:gravity="center"
android:src="@mipmap/ic_launcher" />
</item>
</layer-list>

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_player_sheet_background"
android:padding="24dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/station_icon"
android:layout_width="120dp"
android:layout_height="120dp"
android:background="@drawable/shape_cover_small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/RoundedCorners"
app:srcCompat="@drawable/ic_default_station_image_72dp" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/player_station_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textColor="@color/player_sheet_text_main"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/player_play_button"
app:layout_constraintStart_toEndOf="@+id/station_icon"
app:layout_constraintTop_toTopOf="@+id/station_icon"
tools:text="Station Name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/player_station_metadata"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="@color/player_sheet_text_main"
app:layout_constraintEnd_toEndOf="@+id/player_station_name"
app:layout_constraintStart_toStartOf="@+id/player_station_name"
app:layout_constraintTop_toBottomOf="@+id/player_station_name"
tools:text="Metadata Info" />
<ImageButton
android:id="@+id/player_play_button"
android:layout_width="80dp"
android:layout_height="80dp"
android:background="@drawable/selector_play_button"
android:focusable="true"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="@+id/station_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/station_icon"
app:srcCompat="@drawable/ic_player_play_symbol_42dp" />
<LinearLayout
android:id="@+id/detailed_controls_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/station_icon">
<ImageButton
android:id="@+id/sheet_previous_metadata_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/selector_generic_button_focus"
android:focusable="true"
app:srcCompat="@drawable/ic_chevron_left_24dp" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/sheet_metadata_history"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="16dp"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/player_sheet_text_main"
tools:text="Metadata History" />
<ImageButton
android:id="@+id/sheet_next_metadata_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/selector_generic_button_focus"
android:focusable="true"
app:srcCompat="@drawable/ic_chevron_right_24dp" />
<ImageButton
android:id="@+id/copy_station_metadata_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:background="@drawable/selector_generic_button_focus"
android:focusable="true"
app:srcCompat="@drawable/ic_copy_content_24dp" />
<ImageButton
android:id="@+id/sleep_timer_start_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:background="@drawable/selector_generic_button_focus"
android:focusable="true"
app:srcCompat="@drawable/ic_sleep_timer_24dp" />
<ImageButton
android:id="@+id/sleep_timer_cancel_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:background="@drawable/selector_generic_button_focus"
android:focusable="true"
android:visibility="gone"
app:srcCompat="@drawable/ic_clear_24dp" />
</LinearLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/sleep_timer_remaining_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/player_sheet_text_main"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detailed_controls_row"
tools:text="15:00" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="1000dp"
android:layout_height="500dp"
android:padding="16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/station_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.7" />
<LinearLayout
android:id="@+id/dialog_button_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/dialog_positive_button"
style="@style/Widget.Material3.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_find_station_button_add" />
<Button
android:id="@+id/dialog_negative_button"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dialog_generic_button_cancel" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="1000dp"
android:layout_height="500dp"
android:padding="16dp">
<androidx.appcompat.widget.SearchView
android:id="@+id/station_search_box_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:iconifiedByDefault="false"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:queryHint="@string/dialog_find_station_hint" />
<ProgressBar
android:id="@+id/search_request_progress_indicator"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/station_search_box_view"
app:layout_constraintStart_toStartOf="@+id/station_search_box_view"
app:layout_constraintTop_toBottomOf="@+id/station_search_box_view" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/no_results_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialog_find_station_no_results"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_request_progress_indicator" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/station_search_result_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:clipToPadding="false"
android:paddingBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/no_results_text_view" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.7" />
<!-- Right side: Actions -->
<LinearLayout
android:id="@+id/dialog_button_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/dialog_positive_button"
style="@style/Widget.Material3.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_find_station_button_add" />
<Button
android:id="@+id/dialog_negative_button"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dialog_generic_button_cancel" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="800dp"
android:layout_height="wrap_content"
android:padding="24dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/dialog_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_error_message_default"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
android:textColor="@color/text_default"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/dialog_error_message_default" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/dialog_details_link"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:focusable="true"
android:clickable="true"
android:background="?attr/selectableItemBackground"
android:padding="8dp"
android:text="@string/dialog_generic_details_button"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/text_default"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_message"
tools:text="@string/dialog_generic_details_button" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/dialog_details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:focusable="true"
android:clickable="true"
android:scrollbars="vertical"
android:text="@string/dialog_opml_import_details_default"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="@color/text_default"
android:textIsSelectable="true"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_details_link"
tools:text="@string/dialog_opml_import_details_default" />
</androidx.constraintlayout.widget.ConstraintLayout>

View 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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:focusable="true"
android:clickable="true"
android:nextFocusRight="@+id/dialog_positive_button"
android:background="@drawable/selector_search_result_item">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/station_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textColor="@color/text_default"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Station Name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/station_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="marquee"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="@color/text_lightweight"
app:layout_constraintEnd_toEndOf="@+id/station_name"
app:layout_constraintStart_toStartOf="@+id/station_name"
app:layout_constraintTop_toBottomOf="@+id/station_name"
tools:text="http://stream.url" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/station_bitrate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/text_lightweight"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/station_url"
app:layout_constraintStart_toStartOf="@+id/station_url"
app:layout_constraintTop_toBottomOf="@+id/station_url"
tools:text="128 kbps" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@@ -0,0 +1,206 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:baselineAligned="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/station_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
layout="@layout/element_onboarding"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ScrollView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.2"
android:background="@color/player_sheet_background"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_ui"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/station_icon"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_marginTop="16dp"
android:background="@drawable/shape_cover_small"
android:contentDescription="@string/descr_player_station_image"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/RoundedCorners"
app:srcCompat="@drawable/ic_default_station_image_72dp" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/player_station_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textColor="@color/player_sheet_text_main"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/station_icon"
tools:text="Station Name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/player_station_metadata"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="@color/player_sheet_text_main"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/player_station_name"
tools:text="Artist - Title" />
<LinearLayout
android:id="@+id/controls_row"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/player_station_metadata">
<ImageButton
android:id="@+id/player_prev_button"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginEnd="16dp"
android:background="@drawable/selector_generic_button_focus"
android:contentDescription="@string/descr_expanded_player_metadata_previous_button"
android:focusable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_chevron_left_24dp" />
<ImageButton
android:id="@+id/player_play_button"
android:layout_width="80dp"
android:layout_height="80dp"
android:background="@drawable/selector_play_button"
android:contentDescription="@string/descr_player_playback_button"
android:focusable="true"
android:scaleType="center"
app:srcCompat="@drawable/ic_player_play_symbol_42dp" />
<ImageButton
android:id="@+id/player_next_button"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="16dp"
android:background="@drawable/selector_generic_button_focus"
android:contentDescription="@string/descr_expanded_player_metadata_next_button"
android:focusable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_chevron_right_24dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/secondary_controls_row"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/controls_row">
<ImageButton
android:id="@+id/copy_station_metadata_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="12dp"
android:background="@drawable/selector_generic_button_focus"
android:focusable="true"
android:padding="8dp"
app:srcCompat="@drawable/ic_copy_content_24dp"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@+id/sleep_timer_start_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="12dp"
android:background="@drawable/selector_generic_button_focus"
android:focusable="true"
android:padding="8dp"
app:srcCompat="@drawable/ic_sleep_timer_24dp"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@+id/sleep_timer_cancel_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/selector_generic_button_focus"
android:focusable="true"
android:padding="8dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_clear_24dp"
tools:ignore="ContentDescription" />
</LinearLayout>
<ProgressBar
android:id="@+id/player_buffering_indicator"
style="?android:attr/progressBarStyleLarge"
android:layout_width="96dp"
android:layout_height="96dp"
android:indeterminateTint="@color/player_button_buffering"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/controls_row"
app:layout_constraintEnd_toEndOf="@+id/controls_row"
app:layout_constraintStart_toStartOf="@+id/controls_row"
app:layout_constraintTop_toTopOf="@+id/controls_row" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/sleep_timer_remaining_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/player_sheet_text_main"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/secondary_controls_row"
tools:text="15:00" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</LinearLayout>

View File

@@ -29,4 +29,49 @@
app:layout_constraintTop_toBottomOf="@+id/main_toolbar"
app:navGraph="@navigation/nav_graph_main" />
<!-- SPLASH / LOADING SCREEN -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loading_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/splashBackgroundColor"
android:elevation="10dp"
android:visibility="visible">
<ImageView
android:id="@+id/loading_logo"
android:layout_width="160dp"
android:layout_height="160dp"
android:contentDescription="@string/icon_launcher"
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.4" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/loading"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
android:textColor="@color/default_neutral_white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loading_logo" />
<ProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:indeterminate="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loading_logo"
android:visibility="invisible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -12,6 +12,8 @@
android:layout_marginStart="8dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="24dp"
android:focusable="true"
android:clickable="true"
android:stateListAnimator="@null"
app:backgroundTint="@color/list_card_background"
app:icon="@drawable/ic_add_24dp"
@@ -28,6 +30,8 @@
android:layout_marginTop="10dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="24dp"
android:focusable="true"
android:clickable="true"
android:stateListAnimator="@null"
app:backgroundTint="@color/list_card_background"
app:icon="@drawable/ic_settings_24dp"

View File

@@ -39,6 +39,7 @@
<item name="colorSurface">@color/player_sheet_background</item>
<item name="materialAlertDialogBodyTextStyle">@style/TextAppearance.MaterialComponents.Body1</item>
<item name="android:backgroundDimAmount">0.64</item>
<item name="colorControlActivated">#FFDAE2FF</item>
</style>
<style name="ThemeOverlay.App.TimePicker" parent="ThemeOverlay.MaterialComponents.TimePicker">
@@ -49,5 +50,10 @@
<item name="android:background">@color/player_sheet_background</item>
<item name="dialogCornerRadius">28dp</item>
<item name="checkedTextViewStyle">@style/AlertDialog.TextColor</item>
<item name="colorControlActivated">#FFDAE2FF</item>
</style>
</resources>
<style name="AlertDialog.TextColor" parent="@style/TextAppearance.MaterialComponents.Body1">
<item name="android:textColor">?attr/colorControlNormal</item>
</style>
</resources>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- NIGHT THEME COLORS -->

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="SplashTheme" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:statusBarColor">@color/splashBackgroundColor</item>
<item name="android:navigationBarColor">@color/splashBackgroundColor</item>
</style>
</resources>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- DAY THEME COLORS -->
@@ -16,6 +15,7 @@
<!-- list -->
<color name="list_card_background">#FFFEFBFF</color>
<color name="list_card_stroke_background">#FFC0C6DD</color>
<color name="list_card_stroke_focused">#FF495D92</color>
<color name="list_card_cover_background">#FFE7E0EC</color>
<color name="list_card_delete_background">#FFB3261E</color>
<color name="list_card_delete_icon">#FFFFFFFF</color>

View File

@@ -140,4 +140,6 @@
<string name="snackbar_url_app_home_page" translatable="false">https://github.com/michatec/Radio/releases/latest</string>
<string name="snackbar_github_update_check_url" translatable="false">https://api.github.com/repos/michatec/Radio/releases/latest</string>
<string name="app_name" translatable="false">Radio</string>
<string name="icon_launcher" translatable="false">Icon launcher.</string>
<string name="loading">Loading...</string>
</resources>

View File

@@ -2,29 +2,21 @@
<resources>
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Set AppCompats colors -->
<item name="colorPrimary">#FF495D92</item>
<item name="colorAccent">#FF495D92</item>
<item name="android:textColorHighlight">#FF495D92</item>
<!-- Do not use primary colored elevation overlays to present a visual hierarchy - TOO COLORFUL -->
<item name="elevationOverlayEnabled">false</item>
<!-- Switch Theming -->
<item name="colorControlActivated">#FFDAE2FF</item>
<item name="switchPreferenceCompatStyle">@style/Preference.SwitchPreferenceCompat.Material3</item>
<!-- Material Alert Dialog Theming -->
<item name="materialAlertDialogTheme">@style/ThemeOverlay.App.MaterialAlertDialog</item>
<item name="alertDialogTheme">@style/ThemeOverlay.App.AlertDialogTheme</item>
<!-- Material Time Picker Theming -->
<item name="materialTimePickerTheme">@style/ThemeOverlay.App.TimePicker</item>
<!-- Use "light" Status Bar -->
<item name="android:windowLightStatusBar">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
<style name="SplashTheme" parent="AppTheme" />
<style name="Preference.SwitchPreferenceCompat.Material3" parent="@style/Preference.SwitchPreferenceCompat.Material">
<item name="widgetLayout">@layout/preference_switch</item>
</style>
@@ -38,7 +30,11 @@
<style name="ThemeOverlay.App.AlertDialogTheme" parent="@style/ThemeOverlay.Material3.MaterialAlertDialog">
<item name="android:background">@color/list_card_background</item>
<item name="dialogCornerRadius">28dp</item>
<item name="checkedTextViewStyle">@style/AlertDialog.TextColor</item>
<!-- TV Fix: Explicitly set accent color for radio buttons/checkboxes -->
<item name="colorAccent">@color/icon_default</item>
<item name="colorControlActivated">@color/icon_default</item>
<item name="android:textColorPrimary">@color/text_default</item>
<item name="android:textColorSecondary">@color/text_lightweight</item>
</style>
<style name="ThemeOverlay.App.TimePicker" parent="ThemeOverlay.MaterialComponents.TimePicker">
@@ -46,13 +42,16 @@
</style>
<style name="AlertDialog.TextColor" parent="@style/TextAppearance.MaterialComponents.Body1">
<item name="android:textColor">?attr/colorControlNormal</item>
<item name="android:textColor">@color/text_default</item>
</style>
<style name="App.Widget.Material3.CardView.Outlined" parent="@style/Widget.Material3.CardView.Outlined">
<item name="strokeColor">@color/list_card_stroke_background</item>
<item name="strokeWidth">3dp</item>
<item name="strokeColor">@color/selector_card_station_stroke</item>
<item name="strokeWidth">4dp</item>
<item name="cardCornerRadius">24dp</item>
<item name="android:focusable">true</item>
<item name="android:clickable">true</item>
<item name="android:descendantFocusability">afterDescendants</item>
</style>
<style name="App.Widget.MaterialComponents.TextView" parent="@style/Widget.MaterialComponents.TextView">

View File

@@ -3,24 +3,25 @@ activityKtx = "1.13.0"
agp = "9.1.0"
coreKtx = "1.18.0"
freedroidwarn = "V1.10"
gradleToolchainsFoojayResolverConvention = "1.0.0"
gson = "2.13.2"
kotlin = "2.3.20"
leanback = "1.2.0"
material = "1.13.0"
material3 = "1.4.0"
media = "1.7.1"
media3 = "1.9.3"
media3 = "1.10.0"
navigation = "2.9.7"
paletteKtx = "1.0.0"
preferenceKtx = "1.2.1"
volley = "1.2.1"
workRuntimeKtx = "2.11.1"
workRuntimeKtx = "2.11.2"
[libraries]
activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
freedroidwarn = { group = "com.github.woheller69", name = "FreeDroidWarn", version.ref = "freedroidwarn" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
leanback = { group = "androidx.leanback", name = "leanback", version.ref = "leanback" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
media = { group = "androidx.media", name = "media", version.ref = "media" }
@@ -38,5 +39,4 @@ work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
foojay = { id = "org.gradle.toolchains.foojay-resolver-convention", version.ref = "gradleToolchainsFoojayResolverConvention" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }