mirror of
https://github.com/Michatec/Radio.git
synced 2026-04-01 07:56:27 +02:00
Compare commits
76 Commits
14.1
...
3171c21db9
| Author | SHA1 | Date | |
|---|---|---|---|
| 3171c21db9 | |||
| f43775d3f2 | |||
| 868c39a6e1 | |||
| 985e7d4e1b | |||
| 363d197ba5 | |||
| d0e412ca83 | |||
| 93008f7247 | |||
| 7e95a0bfe1 | |||
| 8d509b6b23 | |||
| cb4aff47a7 | |||
| 293a823442 | |||
| a5b11ba99a | |||
| 2ccb4225d4 | |||
| 453e6e5b71 | |||
| 4108cf2c7c | |||
| f038e01981 | |||
| f66b5af810 | |||
| a6a9daf3a3 | |||
| d79d5e15fa | |||
| c3f37092de | |||
| 48d14fecdb | |||
| c8293b1b6d | |||
| 51c8d5b303 | |||
| 180d5c2218 | |||
| 391999c406 | |||
| fb779bd661 | |||
| 46ebf21c06 | |||
|
|
2d2d95875f | ||
|
|
46e7b905c8 | ||
|
|
d033ae6344 | ||
|
|
fc275d349b | ||
|
|
4e6f7c7c67 | ||
|
|
272d6fd908 | ||
|
|
4a30828c99 | ||
|
|
963f6e7618 | ||
|
|
29ead7e1d8 | ||
| 9140b54a23 | |||
| 57c4075f19 | |||
| 5334b88f1d | |||
| 5978aab0aa | |||
|
|
c3c2ccfdd9 | ||
|
|
38fb4d162b | ||
|
|
3c3c18104b | ||
|
|
4e1ee7d6a7 | ||
|
|
64ba020eae | ||
|
|
b35d7bd67c | ||
|
|
d3dd098639 | ||
|
|
a8f6c7f946 | ||
|
|
765ccc9250 | ||
|
|
c9d6cf27ec | ||
|
|
7b7579d416 | ||
|
|
d452d7f7ee | ||
|
|
2bb3d22cab | ||
|
|
b4ed3e107c | ||
|
|
ef843b601a | ||
|
|
219d54f4e4 | ||
|
|
c8a39bf2d7 | ||
|
|
81ff920c2c | ||
|
|
f4a5209e14 | ||
|
|
c7b7bdcbed | ||
|
|
eae9176f21 | ||
|
|
b3a833fa44 | ||
|
|
35a8ed46ff | ||
|
|
fd18943878 | ||
|
|
094fb508e2 | ||
|
|
874cf1eb39 | ||
|
|
211cb387ad | ||
|
|
5481a06343 | ||
| fbafcb9773 | |||
| 27c405fd58 | |||
|
|
c2cba26c66 | ||
|
|
9224e45b18 | ||
|
|
90164d7e6a | ||
|
|
45e208e7a9 | ||
|
|
1d040e3edc | ||
|
|
efe17bc82b |
8
.github/workflows/gradle-publish.yml
vendored
8
.github/workflows/gradle-publish.yml
vendored
@@ -3,8 +3,10 @@ name: Build and publish APK
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'b*'
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
|
||||
env:
|
||||
ANDROID_HOME: /usr/local/lib/android/sdk/
|
||||
@@ -38,7 +40,7 @@ jobs:
|
||||
## It is not necessary to check for cache hit as it
|
||||
## will not download Android SDK again
|
||||
#if: steps.cache-android-sdk.outputs.cache-hit != 'true'
|
||||
uses: android-actions/setup-android@v3
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
packages: ''
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,3 +5,5 @@
|
||||
/.idea
|
||||
/build
|
||||
/captures
|
||||
/gradle/gradle-daemon-jvm.properties
|
||||
/.kotlin
|
||||
@@ -1,7 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
=====================
|
||||
|
||||
Copyright (c) 2025 - Michatec
|
||||
Copyright (c) 2026 - Michatec
|
||||
--------------------------------
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
||||
30
README.md
30
README.md
@@ -3,6 +3,7 @@
|
||||
### ℹ️ About Radio
|
||||
**Radio is an application with a minimalist approach to listening to radio over the Internet.** <br>
|
||||
**Radio only offers a very basic search option, and it imports audio streaming links when you tap them in a web browser.** <br>
|
||||
**Radio now also supports Android TV (Beta).** <br>
|
||||
**Pull request are welcome at any time.**<br>
|
||||
|
||||
**Radio is free software. It is released under the [MIT open source license](https://opensource.org/licenses/MIT).**
|
||||
@@ -10,6 +11,19 @@
|
||||
|
||||
----------------------------------------
|
||||
|
||||
<details>
|
||||
<summary>❗Warning</summary>
|
||||
<br>
|
||||
<p>
|
||||
Google has announced that, starting in 2026/2027, all apps on certified Android devices will require the developer to submit personal identity details directly to Google.
|
||||
|
||||
Since the developers of this app do not agree to this requirement, this app will no longer work on certified Android devices after that time.
|
||||
</p>
|
||||
<a href="https://github.com/woheller69/FreeDroidWarn">ℹ️ More Information</a>
|
||||
</details>
|
||||
|
||||
----------------------------------------
|
||||
|
||||
<details>
|
||||
<summary>⚙️ Install Radio</summary>
|
||||
<br>
|
||||
@@ -57,6 +71,22 @@ You can help out the radio-browser.info community by [adding the missing station
|
||||
|
||||
----------------------------------------
|
||||
|
||||
<details>
|
||||
<summary>📺 Android TV Controls</summary>
|
||||
<br>
|
||||
|
||||
When **Edit Stations** is enabled:
|
||||
- Press **← (Left)** on the remote to open the detailed station editing area
|
||||
- Press **2** or **Back** to close the editing area
|
||||
|
||||
**General TV** Controls:
|
||||
- Press **0** or **DEL** to remove the selected radio station
|
||||
- Press **1** or **SPACE** to make the selected radio station favourite
|
||||
|
||||
</details>
|
||||
|
||||
----------------------------------------
|
||||
|
||||
<details>
|
||||
<summary>📜️ Credit</summary>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
alias libs.plugins.android.application
|
||||
id 'kotlin-parcelize'
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ android {
|
||||
applicationId 'com.michatec.radio'
|
||||
minSdk 28
|
||||
targetSdk 36
|
||||
versionCode 141
|
||||
versionName '14.1'
|
||||
versionCode 144
|
||||
versionName '14.4'
|
||||
resourceConfigurations += ['en', 'de', 'el', 'nl', 'pl', 'ru','uk', 'ja', 'da', 'fr']
|
||||
}
|
||||
|
||||
@@ -55,24 +55,27 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
// Google Stuff //
|
||||
implementation 'com.google.android.material:material:1.13.0'
|
||||
implementation 'com.google.code.gson:gson:2.13.2'
|
||||
implementation libs.material
|
||||
implementation libs.gson
|
||||
|
||||
// AndroidX Stuff //
|
||||
implementation 'androidx.core:core-ktx:1.17.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.12.4'
|
||||
implementation 'androidx.palette:palette-ktx:1.0.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.media:media:1.7.1'
|
||||
implementation 'androidx.media3:media3-exoplayer:1.9.2'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.9.2'
|
||||
implementation 'androidx.media3:media3-session:1.9.2'
|
||||
implementation 'androidx.media3:media3-datasource-okhttp:1.9.2'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.7'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.9.7'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.11.1'
|
||||
implementation libs.core.ktx
|
||||
implementation libs.activity.ktx
|
||||
implementation libs.palette.ktx
|
||||
implementation libs.preference.ktx
|
||||
implementation libs.media
|
||||
implementation libs.media3.exoplayer
|
||||
implementation libs.media3.exoplayer.hls
|
||||
implementation libs.media3.session
|
||||
implementation libs.media3.datasource.okhttp
|
||||
implementation libs.navigation.fragment.ktx
|
||||
implementation libs.navigation.ui.ktx
|
||||
implementation libs.work.runtime.ktx
|
||||
implementation libs.leanback
|
||||
|
||||
implementation libs.freedroidwarn
|
||||
|
||||
// Volley HTTP request //
|
||||
implementation 'com.android.volley:volley:1.2.1'
|
||||
implementation 'androidx.compose.material3:material3:1.4.0'
|
||||
implementation libs.volley
|
||||
implementation libs.material3
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -21,7 +29,7 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="tiramisu">
|
||||
tools:targetApi="33">
|
||||
|
||||
<!-- ANDROID AUTO SUPPORT -->
|
||||
<!-- https://developer.android.com/training/auto/audio/ -->
|
||||
@@ -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 -->
|
||||
@@ -58,6 +68,7 @@
|
||||
</intent-filter>
|
||||
|
||||
<!-- react to playlist-links based on file extension -->
|
||||
<!-- This is intended as an App Link for specific extensions -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -66,14 +77,16 @@
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="*" />
|
||||
<data android:host="*"
|
||||
tools:ignore="AppLinkUrlError" />
|
||||
<data android:pathPattern=".*\\.m3u" />
|
||||
<data android:pathPattern=".*\\.m3u8" />
|
||||
<data android:pathPattern=".*\\.pls" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- react to playlist-links based on mimetype -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<!-- Note: MIME types prevent strict App Link verification, but are kept as requested -->
|
||||
<intent-filter android:autoVerify="false">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
@@ -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.*
|
||||
@@ -78,7 +64,6 @@ object Keys {
|
||||
// default const values
|
||||
const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25
|
||||
const val DEFAULT_MAX_LENGTH_OF_METADATA_ENTRY: Int = 127
|
||||
const val DEFAULT_DOWNLOAD_OVER_MOBILE: Boolean = false
|
||||
const val ACTIVE_DOWNLOADS_EMPTY: String = "zero"
|
||||
const val DEFAULT_MAX_RECONNECTION_COUNT: Int = 30
|
||||
const val LARGE_BUFFER_SIZE_MULTIPLIER: Int = 8
|
||||
@@ -99,6 +84,7 @@ object Keys {
|
||||
const val DIALOG_REMOVE_STATION: Int = 2
|
||||
const val DIALOG_UPDATE_STATION_IMAGES: Int = 4
|
||||
const val DIALOG_RESTORE_COLLECTION: Int = 5
|
||||
const val DIALOG_THEME_SELECTION: Int = 6
|
||||
|
||||
// dialog results
|
||||
const val DIALOG_EMPTY_PAYLOAD_STRING: String = ""
|
||||
@@ -149,9 +135,6 @@ object Keys {
|
||||
const val RADIO_BROWSER_API_BASE: String = "all.api.radio-browser.info"
|
||||
const val RADIO_BROWSER_API_DEFAULT: String = "de1.api.radio-browser.info"
|
||||
|
||||
// locations
|
||||
const val LOCATION_DEFAULT_STATION_IMAGE: String = "android.resource://com.michatec.radio/drawable/ic_default_station_image_24dp"
|
||||
|
||||
// sizes (in dp)
|
||||
const val SIZE_STATION_IMAGE_CARD: Int = 72
|
||||
const val SIZE_STATION_IMAGE_MAXIMUM: Int = 640
|
||||
|
||||
@@ -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
|
||||
@@ -25,7 +29,7 @@ import androidx.navigation.ui.navigateUp
|
||||
import com.michatec.radio.helpers.AppThemeHelper
|
||||
import com.michatec.radio.helpers.FileHelper
|
||||
import com.michatec.radio.helpers.PreferencesHelper
|
||||
|
||||
import org.woheller69.freeDroidWarn.FreeDroidWarn
|
||||
|
||||
/*
|
||||
* MainActivity class
|
||||
@@ -38,8 +42,12 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
/* Overrides onCreate from AppCompatActivity */
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(R.style.AppTheme)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Free Android
|
||||
FreeDroidWarn.showWarningOnUpgrade(this, BuildConfig.VERSION_CODE)
|
||||
|
||||
// set up views
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
@@ -55,11 +63,30 @@ 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)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onResume from AppCompatActivity */
|
||||
override fun onResume() {
|
||||
try {
|
||||
super.onResume()
|
||||
} catch (_: ClassCastException) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onSupportNavigateUp from AppCompatActivity */
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
// Taken from: https://developer.android.com/guide/navigation/navigation-ui#action_bar
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -400,19 +397,19 @@ class PlayerService : MediaLibraryService() {
|
||||
customLayout: ImmutableList<CommandButton>,
|
||||
showPauseButton: Boolean
|
||||
): ImmutableList<CommandButton> {
|
||||
val seekToPreviousCommandButton = CommandButton.Builder()
|
||||
val seekToPreviousCommandButton = CommandButton.Builder(CommandButton.ICON_UNDEFINED)
|
||||
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
|
||||
.setIconResId(R.drawable.ic_notification_skip_to_previous_36dp)
|
||||
.setCustomIconResId(R.drawable.ic_notification_skip_to_previous_36dp)
|
||||
.setEnabled(true)
|
||||
.build()
|
||||
val playCommandButton = CommandButton.Builder()
|
||||
val playCommandButton = CommandButton.Builder(CommandButton.ICON_UNDEFINED)
|
||||
.setPlayerCommand(Player.COMMAND_PLAY_PAUSE)
|
||||
.setIconResId(if (player.isPlaying) R.drawable.ic_notification_stop_36dp else R.drawable.ic_notification_play_36dp)
|
||||
.setCustomIconResId(if (player.isPlaying) R.drawable.ic_notification_stop_36dp else R.drawable.ic_notification_play_36dp)
|
||||
.setEnabled(true)
|
||||
.build()
|
||||
val seekToNextCommandButton = CommandButton.Builder()
|
||||
val seekToNextCommandButton = CommandButton.Builder(CommandButton.ICON_UNDEFINED)
|
||||
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
|
||||
.setIconResId(R.drawable.ic_notification_skip_to_next_36dp)
|
||||
.setCustomIconResId(R.drawable.ic_notification_skip_to_next_36dp)
|
||||
.setEnabled(true)
|
||||
.build()
|
||||
val commandButtons: MutableList<CommandButton> = mutableListOf(
|
||||
|
||||
@@ -34,9 +34,4 @@ class Radio : Application() {
|
||||
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
|
||||
}
|
||||
|
||||
/* Implements onTerminate */
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -28,11 +14,11 @@ import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.*
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.michatec.radio.dialogs.ErrorDialog
|
||||
import com.michatec.radio.dialogs.ThemeSelectionDialog
|
||||
import com.michatec.radio.dialogs.YesNoDialog
|
||||
import com.michatec.radio.helpers.*
|
||||
import com.michatec.radio.helpers.AppThemeHelper.getColor
|
||||
@@ -46,7 +32,7 @@ import java.util.*
|
||||
/*
|
||||
* SettingsFragment class
|
||||
*/
|
||||
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener {
|
||||
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener {
|
||||
|
||||
|
||||
/* Define log tag */
|
||||
@@ -70,33 +56,17 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
val screen = preferenceManager.createPreferenceScreen(context)
|
||||
|
||||
// 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.setIcon(R.drawable.ic_brush_24dp)
|
||||
preferenceThemeSelection.key = Keys.PREF_THEME_SELECTION
|
||||
preferenceThemeSelection.summary = "${getString(R.string.pref_theme_selection_summary)} ${
|
||||
AppThemeHelper.getCurrentTheme(activity as Context)
|
||||
}"
|
||||
preferenceThemeSelection.entries = 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)
|
||||
)
|
||||
preferenceThemeSelection.entryValues = arrayOf(
|
||||
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]}"
|
||||
return@setOnPreferenceChangeListener true
|
||||
} else {
|
||||
return@setOnPreferenceChangeListener false
|
||||
}
|
||||
preferenceThemeSelection.setOnPreferenceClickListener {
|
||||
// show theme selection dialog
|
||||
ThemeSelectionDialog(this).show(activity as Context)
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
// set up "Update Station Images" preference
|
||||
@@ -115,7 +85,6 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
|
||||
// set up "Update Stations" preference
|
||||
val preferenceUpdateCollection = Preference(activity as Context)
|
||||
preferenceUpdateCollection.title = getString(R.string.pref_update_collection_title)
|
||||
@@ -194,7 +163,8 @@ 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))
|
||||
preferenceEnableEditingStreamUri.isEnabled = PreferencesHelper.loadEditStreamUrisEnabled(context)
|
||||
|
||||
|
||||
// set up "Edit Stations" preference
|
||||
@@ -204,7 +174,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 -> {
|
||||
@@ -218,7 +188,6 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
return@setOnPreferenceChangeListener true
|
||||
}
|
||||
|
||||
|
||||
// set up "App Version" preference
|
||||
val preferenceAppVersion = Preference(context)
|
||||
preferenceAppVersion.title = getString(R.string.pref_app_version_title)
|
||||
@@ -319,6 +288,26 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
preferenceScreen = screen
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
override fun onYesNoDialog(
|
||||
@@ -427,9 +416,9 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
val sourceUri: Uri? = result.data?.data
|
||||
if (sourceUri != null) {
|
||||
// open and import OPML in player fragment
|
||||
val bundle: Bundle = bundleOf(
|
||||
Keys.ARG_RESTORE_COLLECTION to "$sourceUri"
|
||||
)
|
||||
val bundle = Bundle().apply {
|
||||
putString(Keys.ARG_RESTORE_COLLECTION, "$sourceUri")
|
||||
}
|
||||
this.findNavController().navigate(R.id.player_destination, bundle)
|
||||
}
|
||||
}
|
||||
@@ -445,7 +434,9 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
// update collection in player screen
|
||||
val bundle: Bundle = bundleOf(Keys.ARG_UPDATE_COLLECTION to true)
|
||||
val bundle = Bundle().apply {
|
||||
putBoolean(Keys.ARG_UPDATE_COLLECTION, true)
|
||||
}
|
||||
this.findNavController().navigate(R.id.player_destination, bundle)
|
||||
} else {
|
||||
ErrorDialog().show(
|
||||
@@ -466,9 +457,9 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
// update collection in player screen
|
||||
val bundle: Bundle = bundleOf(
|
||||
Keys.ARG_UPDATE_IMAGES to true
|
||||
)
|
||||
val bundle = Bundle().apply {
|
||||
putBoolean(Keys.ARG_UPDATE_IMAGES, true)
|
||||
}
|
||||
this.findNavController().navigate(R.id.player_destination, bundle)
|
||||
} else {
|
||||
ErrorDialog().show(
|
||||
|
||||
@@ -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,38 @@ 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_NUMPAD_2, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_BACK -> {
|
||||
if (expandedStationPosition == stationViewHolder.bindingAdapterPosition) {
|
||||
val position: Int = stationViewHolder.bindingAdapterPosition
|
||||
toggleEditViews(position, station.uuid)
|
||||
return@setOnKeyListener true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_DEL -> {
|
||||
removeStation(context, stationViewHolder.bindingAdapterPosition)
|
||||
return@setOnKeyListener true
|
||||
}
|
||||
KeyEvent.KEYCODE_NUMPAD_1, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_SPACE -> {
|
||||
toggleStarredStation(context, stationViewHolder.bindingAdapterPosition)
|
||||
return@setOnKeyListener true
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
stationViewHolder.playButtonView.setOnLongClickListener {
|
||||
if (editStationsEnabled) {
|
||||
val position: Int = stationViewHolder.bindingAdapterPosition
|
||||
@@ -649,9 +687,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)
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
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
|
||||
import com.michatec.radio.helpers.PreferencesHelper
|
||||
|
||||
|
||||
/*
|
||||
* 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
|
||||
|
||||
|
||||
/* Update radio buttons to reflect current theme */
|
||||
private fun updateRadioButtons(
|
||||
context: Context,
|
||||
radioFollowSystem: RadioButton,
|
||||
radioLight: RadioButton,
|
||||
radioDark: RadioButton
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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
|
||||
}
|
||||
// save theme selection to preferences
|
||||
PreferencesHelper.saveThemeSelection(selectedTheme)
|
||||
// apply theme immediately
|
||||
AppThemeHelper.setTheme(selectedTheme)
|
||||
// notify listener
|
||||
themeSelectionDialogListener.onThemeSelectionDialog(true, selectedTheme)
|
||||
// dismiss dialog
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
// set custom view
|
||||
builder.setView(view)
|
||||
|
||||
// handle outside-click as cancel
|
||||
builder.setOnCancelListener {
|
||||
themeSelectionDialogListener.onThemeSelectionDialog(false, Keys.STATE_THEME_FOLLOW_SYSTEM)
|
||||
}
|
||||
|
||||
// display dialog
|
||||
dialog = builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ package com.michatec.radio.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionResult
|
||||
@@ -71,8 +70,11 @@ fun MediaController.play(context: Context, station: Station) {
|
||||
|
||||
/* Starts playback with of a stream url */
|
||||
fun MediaController.playStreamDirectly(streamUri: String) {
|
||||
val bundle = Bundle().apply {
|
||||
putString(Keys.KEY_STREAM_URI, streamUri)
|
||||
}
|
||||
sendCustomCommand(
|
||||
SessionCommand(Keys.CMD_PLAY_STREAM, Bundle.EMPTY),
|
||||
bundleOf(Pair(Keys.KEY_STREAM_URI, streamUri))
|
||||
bundle
|
||||
)
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import kotlinx.coroutines.Dispatchers.IO
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
/*
|
||||
@@ -292,7 +291,7 @@ object FileHelper {
|
||||
collection: Collection,
|
||||
lastUpdate: Date
|
||||
) {
|
||||
return suspendCoroutine { cont ->
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
cont.resume(saveCollection(context, collection, lastUpdate))
|
||||
}
|
||||
}
|
||||
@@ -311,7 +310,7 @@ object FileHelper {
|
||||
originalFileUri: Uri,
|
||||
targetFileUri: Uri
|
||||
): Boolean {
|
||||
return suspendCoroutine { cont ->
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
cont.resume(copyFile(context, originalFileUri, targetFileUri))
|
||||
}
|
||||
}
|
||||
@@ -319,7 +318,7 @@ object FileHelper {
|
||||
|
||||
/* Suspend function: Exports collection of stations as M3U file - local backup copy */
|
||||
suspend fun backupCollectionAsM3uSuspended(context: Context, collection: Collection) {
|
||||
return suspendCoroutine { cont ->
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
Log.v(TAG, "Backing up collection as M3U - Thread: ${Thread.currentThread().name}")
|
||||
// create M3U string
|
||||
val m3uString: String = CollectionHelper.createM3uString(collection)
|
||||
@@ -338,7 +337,7 @@ object FileHelper {
|
||||
|
||||
/* Suspend function: Exports collection of stations as PLS file - local backup copy */
|
||||
suspend fun backupCollectionAsPlsSuspended(context: Context, collection: Collection) {
|
||||
return suspendCoroutine { cont ->
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
Log.v(TAG, "Backing up collection as PLS - Thread: ${Thread.currentThread().name}")
|
||||
// create PLS string
|
||||
val plsString: String = CollectionHelper.createPlsString(collection)
|
||||
|
||||
@@ -19,13 +19,13 @@ import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.util.Log
|
||||
import com.michatec.radio.Keys
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetAddress
|
||||
import java.net.URL
|
||||
import java.net.UnknownHostException
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
/*
|
||||
@@ -105,7 +105,7 @@ object NetworkHelper {
|
||||
|
||||
/* Suspend function: Detects content type (mime type) from given URL string - async using coroutine */
|
||||
suspend fun detectContentTypeSuspended(urlString: String): ContentType {
|
||||
return suspendCoroutine { cont ->
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
cont.resume(detectContentType(urlString))
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ object NetworkHelper {
|
||||
|
||||
/* Suspend function: Gets a random radio-browser.info api address - async using coroutine */
|
||||
suspend fun getRadioBrowserServerSuspended(): String {
|
||||
return suspendCoroutine { cont ->
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
val serverAddress: String = try {
|
||||
// get all available radio browser servers
|
||||
val serverAddressList: Array<InetAddress> =
|
||||
|
||||
@@ -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
|
||||
@@ -222,14 +223,24 @@ object PreferencesHelper {
|
||||
}
|
||||
|
||||
|
||||
/* Save currently selected app theme */
|
||||
fun saveThemeSelection(theme: String) {
|
||||
sharedPreferences.edit {
|
||||
putString(Keys.PREF_THEME_SELECTION, theme)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -19,6 +5,7 @@ import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
@@ -54,30 +41,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 +86,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 +126,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 +177,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 +253,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 +267,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 +288,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 +307,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 +329,27 @@ 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)
|
||||
}
|
||||
|
||||
if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||
// TV: Toggle play button immediately for snappier feel
|
||||
togglePlayButton(isPlaying)
|
||||
} else {
|
||||
// Handy/Tablet: Rotate the play button
|
||||
val rotateAnimation = AnimationUtils.loadAnimation(context, if (isPlaying) R.anim.rotate_clockwise_slow else R.anim.rotate_counterclockwise_fast)
|
||||
rotateAnimation.setAnimationListener(createAnimationListener(isPlaying))
|
||||
playButtonView.startAnimation(rotateAnimation)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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 +358,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
|
||||
@@ -388,7 +379,7 @@ data class LayoutHolder(var rootView: View) {
|
||||
return object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {}
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
// set up button symbol and playback indicator afterwards
|
||||
// set up button symbol and playback indicator afterward
|
||||
togglePlayButton(isPlaying)
|
||||
}
|
||||
|
||||
@@ -399,38 +390,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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
app/src/main/res/color/selector_card_station_stroke.xml
Normal file
5
app/src/main/res/color/selector_card_station_stroke.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
11
app/src/main/res/drawable/selector_generic_button_focus.xml
Normal file
11
app/src/main/res/drawable/selector_generic_button_focus.xml
Normal 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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
12
app/src/main/res/drawable/splash_screen.xml
Normal file
12
app/src/main/res/drawable/splash_screen.xml
Normal 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>
|
||||
@@ -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>
|
||||
54
app/src/main/res/layout-television/dialog_add_station.xml
Normal file
54
app/src/main/res/layout-television/dialog_add_station.xml
Normal 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="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
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>
|
||||
88
app/src/main/res/layout-television/dialog_find_station.xml
Normal file
88
app/src/main/res/layout-television/dialog_find_station.xml
Normal 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="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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: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: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:focusable="true"
|
||||
android:clickable="true" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
61
app/src/main/res/layout-television/element_search_result.xml
Normal file
61
app/src/main/res/layout-television/element_search_result.xml
Normal 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>
|
||||
206
app/src/main/res/layout-television/fragment_player.xml
Normal file
206
app/src/main/res/layout-television/fragment_player.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
49
app/src/main/res/layout/dialog_theme_selection.xml
Normal file
49
app/src/main/res/layout/dialog_theme_selection.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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="16dp">
|
||||
|
||||
<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="18sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="12dp" />
|
||||
|
||||
<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="16sp"
|
||||
android:padding="8dp"/>
|
||||
|
||||
<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="16sp"
|
||||
android:padding="8dp"/>
|
||||
|
||||
<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="16sp"
|
||||
android:padding="8dp"/>
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -11,6 +11,9 @@
|
||||
<item name="colorOnSurfaceVariant">@android:color/system_accent1_100</item>
|
||||
<item name="android:colorBackground">@android:color/system_neutral1_900</item>
|
||||
<item name="android:textColorHighlight">@android:color/system_accent1_500</item>
|
||||
<item name="colorControlHighlight">#33FFFFFF</item>
|
||||
<item name="android:colorControlHighlight">#33FFFFFF</item>
|
||||
<item name="android:colorFocusedHighlight">#80FFFFFF</item>
|
||||
|
||||
<!-- Do not use primary colored elevation overlays to present a visual hierarchy - TOO COLORFUL -->
|
||||
<item name="elevationOverlayEnabled">false</item>
|
||||
@@ -39,6 +42,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 +53,12 @@
|
||||
<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>
|
||||
<item name="colorControlHighlight">#33FFFFFF</item>
|
||||
<item name="android:colorFocusedHighlight">#80FFFFFF</item>
|
||||
</style>
|
||||
|
||||
<style name="AlertDialog.TextColor" parent="@style/TextAppearance.MaterialComponents.Body1">
|
||||
<item name="android:textColor">?attr/colorControlNormal</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
<!-- NIGHT THEME COLORS -->
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<item name="colorAccent">#FFDAE2FF</item>
|
||||
<item name="colorOnPrimary">#FF182E60</item>
|
||||
<item name="android:textColorHighlight">#FF495D92</item>
|
||||
<item name="colorControlHighlight">#33FFFFFF</item>
|
||||
<item name="android:colorControlHighlight">#33FFFFFF</item>
|
||||
<item name="android:colorFocusedHighlight">#80FFFFFF</item>
|
||||
|
||||
<!-- Do not use primary colored elevation overlays to present a visual hierarchy - TOO COLORFUL -->
|
||||
<item name="elevationOverlayEnabled">false</item>
|
||||
|
||||
8
app/src/main/res/values-television/styles.xml
Normal file
8
app/src/main/res/values-television/styles.xml
Normal 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>
|
||||
@@ -6,6 +6,9 @@
|
||||
<item name="colorPrimary">#FF495D92</item>
|
||||
<item name="colorAccent">#FF495D92</item>
|
||||
<item name="android:textColorHighlight">#FF495D92</item>
|
||||
<item name="colorControlHighlight">#33000000</item>
|
||||
<item name="android:colorControlHighlight">#33000000</item>
|
||||
<item name="android:colorFocusedHighlight">#80000000</item>
|
||||
|
||||
<!-- Do not use primary colored elevation overlays to present a visual hierarchy - TOO COLORFUL -->
|
||||
<item name="elevationOverlayEnabled">false</item>
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<item name="colorOnSurfaceVariant">@android:color/system_accent1_600</item>
|
||||
<item name="android:colorBackground">@android:color/system_neutral2_10</item>
|
||||
<item name="android:textColorHighlight">@android:color/system_accent1_200</item>
|
||||
<item name="colorControlHighlight">#22000000</item>
|
||||
<item name="android:colorControlHighlight">#22000000</item>
|
||||
<item name="android:colorFocusedHighlight">#66000000</item>
|
||||
|
||||
<!-- Do not use primary colored elevation overlays to present a visual hierarchy - TOO COLORFUL -->
|
||||
<item name="elevationOverlayEnabled">false</item>
|
||||
@@ -38,6 +41,8 @@
|
||||
<item name="colorSurface">@color/list_card_background</item>
|
||||
<item name="materialAlertDialogBodyTextStyle">@style/TextAppearance.MaterialComponents.Body1</item>
|
||||
<item name="android:backgroundDimAmount">0.64</item>
|
||||
<item name="colorControlHighlight">#22000000</item>
|
||||
<item name="android:colorFocusedHighlight">#66000000</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.App.TimePicker" parent="ThemeOverlay.MaterialComponents.TimePicker">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,29 +2,24 @@
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Set AppCompat’s 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>
|
||||
<item name="colorControlHighlight">#80000000</item>
|
||||
<item name="android:colorControlHighlight">#80000000</item>
|
||||
<item name="android:colorFocusedHighlight">#FF333333</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 +33,13 @@
|
||||
<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>
|
||||
<item name="colorControlHighlight">#80000000</item>
|
||||
<item name="android:colorFocusedHighlight">#FF333333</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.App.TimePicker" parent="ThemeOverlay.MaterialComponents.TimePicker">
|
||||
@@ -46,13 +47,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">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
plugins {
|
||||
id 'com.android.application' version '9.0.1' apply false
|
||||
id 'com.android.library' version '9.0.1' apply false
|
||||
id 'org.jetbrains.kotlin.android' version "2.3.10" apply false
|
||||
alias libs.plugins.android.application apply false
|
||||
alias libs.plugins.android.library apply false
|
||||
alias libs.plugins.jetbrains.kotlin.android apply false
|
||||
}
|
||||
|
||||
tasks.register('clean', Delete) {
|
||||
|
||||
42
gradle/libs.versions.toml
Normal file
42
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
[versions]
|
||||
activityKtx = "1.13.0"
|
||||
agp = "9.1.0"
|
||||
coreKtx = "1.18.0"
|
||||
freedroidwarn = "V1.10"
|
||||
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.10.0"
|
||||
navigation = "2.9.7"
|
||||
paletteKtx = "1.0.0"
|
||||
preferenceKtx = "1.2.1"
|
||||
volley = "1.2.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" }
|
||||
media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3" }
|
||||
media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
|
||||
media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" }
|
||||
media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
|
||||
navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" }
|
||||
navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" }
|
||||
palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "paletteKtx" }
|
||||
preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" }
|
||||
volley = { group = "com.android.volley", name = "volley", version.ref = "volley" }
|
||||
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
2
gradlew
vendored
2
gradlew
vendored
@@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
@@ -3,13 +3,19 @@ pluginManagement {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user