From ef208bb215cd60152809a32382361409392085f8 Mon Sep 17 00:00:00 2001 From: brahmkshatriya <69040506+brahmkshatriya@users.noreply.github.com> Date: Sat, 14 Dec 2024 03:41:54 +0530 Subject: [PATCH] Clean up the player and made playing videos better --- app/build.gradle.kts | 5 +- .../dev/brahmkshatriya/echo/PlayerService.kt | 6 +- .../dev/brahmkshatriya/echo/di/AppModule.kt | 5 - .../echo/offline/TestExtension.kt | 2 +- .../echo/playback/render/FFTAudioProcessor.kt | 265 ------------------ .../echo/playback/render/RenderersFactory.kt | 6 +- .../echo/ui/item/ItemViewModel.kt | 55 ++-- .../echo/ui/player/PlayerFragment.kt | 29 ++ .../echo/ui/player/PlayerTrackAdapter.kt | 127 ++++----- .../echo/ui/player/PlayerUiListener.kt | 1 + .../echo/ui/player/TrackDetailsFragment.kt | 39 +-- .../echo/ui/settings/LookFragment.kt | 11 + .../echo/utils/ExoVisualizer.kt | 222 --------------- .../echo/viewmodels/PlayerViewModel.kt | 2 - .../echo/viewmodels/UiViewModel.kt | 2 +- .../res/layout-land/item_player_track.xml | 82 ------ .../main/res/layout/item_player_collapsed.xml | 4 +- .../main/res/layout/item_player_controls.xml | 16 +- app/src/main/res/layout/item_player_track.xml | 69 ----- .../res/layout/item_quick_search_media.xml | 2 +- .../res/layout/item_quick_search_query.xml | 2 +- app/src/main/res/values/strings.xml | 2 + .../echo/common/helpers/PagedData.kt | 5 +- 23 files changed, 162 insertions(+), 797 deletions(-) delete mode 100644 app/src/main/java/dev/brahmkshatriya/echo/playback/render/FFTAudioProcessor.kt delete mode 100644 app/src/main/java/dev/brahmkshatriya/echo/utils/ExoVisualizer.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a4ce7b5..ad1f2351 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,8 +54,8 @@ dependencies { implementation("androidx.fragment:fragment-ktx:1.6.2") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") - implementation("androidx.paging:paging-common-ktx:3.3.4") - implementation("androidx.paging:paging-runtime-ktx:3.3.4") + implementation("androidx.paging:paging-common-ktx:3.3.5") + implementation("androidx.paging:paging-runtime-ktx:3.3.5") implementation("androidx.preference:preference-ktx:1.2.1") implementation("androidx.room:room-runtime:2.6.1") @@ -84,7 +84,6 @@ dependencies { implementation("com.github.bosphere.android-fadingedgelayout:fadingedgelayout:1.0.0") implementation("me.zhanghai.android.fastscroll:library:1.3.0") implementation("com.flaviofaria:kenburnsview:1.0.7") - implementation("com.github.paramsen:noise:2.0.0") testImplementation("org.jetbrains.kotlin:kotlin-reflect:1.9.24") testImplementation("junit:junit:4.13.2") diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt index b0f0c7f1..e91fb13d 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt @@ -27,7 +27,6 @@ import dev.brahmkshatriya.echo.playback.listeners.PlayerEventListener import dev.brahmkshatriya.echo.playback.listeners.Radio import dev.brahmkshatriya.echo.playback.listeners.TrackingListener import dev.brahmkshatriya.echo.playback.loading.StreamableMediaSource -import dev.brahmkshatriya.echo.playback.render.FFTAudioProcessor import dev.brahmkshatriya.echo.playback.render.PlayerBitmapLoader import dev.brahmkshatriya.echo.playback.render.RenderersFactory import dev.brahmkshatriya.echo.ui.settings.AudioFragment.AudioPreference.Companion.CLOSE_PLAYER @@ -69,9 +68,6 @@ class PlayerService : MediaLibraryService() { @Inject lateinit var currentServers: MutableStateFlow> - @Inject - lateinit var fftAudioProcessor: FFTAudioProcessor - private val scope = CoroutineScope(Dispatchers.Main) @OptIn(UnstableApi::class) @@ -93,7 +89,7 @@ class PlayerService : MediaLibraryService() { ) ExoPlayer.Builder(this, factory) - .setRenderersFactory(RenderersFactory(this, fftAudioProcessor)) + .setRenderersFactory(RenderersFactory(this)) .setHandleAudioBecomingNoisy(true) .setWakeMode(C.WAKE_MODE_NETWORK) .setSkipSilenceEnabled(settings.getBoolean(SKIP_SILENCE, true)) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/di/AppModule.kt b/app/src/main/java/dev/brahmkshatriya/echo/di/AppModule.kt index 4a9c1f6b..fd983978 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/di/AppModule.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/di/AppModule.kt @@ -17,7 +17,6 @@ import dev.brahmkshatriya.echo.common.models.Streamable import dev.brahmkshatriya.echo.db.models.UserEntity import dev.brahmkshatriya.echo.playback.Current import dev.brahmkshatriya.echo.playback.listeners.Radio -import dev.brahmkshatriya.echo.playback.render.FFTAudioProcessor import dev.brahmkshatriya.echo.ui.settings.AudioFragment.AudioPreference.Companion.CACHE_SIZE import dev.brahmkshatriya.echo.viewmodels.SnackBar import kotlinx.coroutines.flow.MutableSharedFlow @@ -77,8 +76,4 @@ class AppModule { @Provides @Singleton fun provideExtensionListFlow() = MutableStateFlow(Radio.State.Empty) - - @Provides - @Singleton - fun providesAudioProcessor() = FFTAudioProcessor() } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt b/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt index f8255fac..a820d5b2 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt @@ -86,7 +86,7 @@ class TestExtension : ExtensionClient, LoginClient.UsernamePassword, TrackClient Streamable.MediaType.Server -> { val srcs = Srcs.valueOf(streamable.id) when (srcs) { - Srcs.Single -> throw Exception("Single source not supported") + Srcs.Single -> FUN.toServerMedia() Srcs.Merged -> Streamable.Media.Server( listOf( BUNNY.toSource(), diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/render/FFTAudioProcessor.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/render/FFTAudioProcessor.kt deleted file mode 100644 index 7a02a07c..00000000 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/render/FFTAudioProcessor.kt +++ /dev/null @@ -1,265 +0,0 @@ -package dev.brahmkshatriya.echo.playback.render - -import android.media.AudioTrack -import android.media.AudioTrack.ERROR_BAD_VALUE -import androidx.annotation.OptIn -import androidx.media3.common.C -import androidx.media3.common.Format -import androidx.media3.common.audio.AudioProcessor -import androidx.media3.common.util.Assertions -import androidx.media3.common.util.UnstableApi -import androidx.media3.common.util.Util -import com.paramsen.noise.Noise -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import java.nio.ByteBuffer -import java.nio.ByteOrder -import kotlin.math.max - -/** - * An audio processor which forwards the input to the output, - * but also takes the input and executes a Fast-Fourier Transformation (FFT) on it. - * The results of this transformation is a 'list' of frequencies with their amplitudes, - * which will be forwarded to the listener - * - * https://github.com/dzolnai/ExoVisualizer - */ - -@OptIn(UnstableApi::class) -class FFTAudioProcessor : AudioProcessor { - - companion object { - const val SAMPLE_SIZE = 8192 - - // From DefaultAudioSink.java:160 'MIN_BUFFER_DURATION_US' - private const val EXO_MIN_BUFFER_DURATION_US: Long = 250000 - - // From DefaultAudioSink.java:164 'MAX_BUFFER_DURATION_US' - private const val EXO_MAX_BUFFER_DURATION_US: Long = 750000 - - // From DefaultAudioSink.java:173 'BUFFER_MULTIPLICATION_FACTOR' - private const val EXO_BUFFER_MULTIPLICATION_FACTOR = 4 - - // Extra size next in addition to the AudioTrack buffer size - private const val BUFFER_EXTRA_SIZE = SAMPLE_SIZE * 8 - } - - private var noise: Noise? = null - - private var isActive: Boolean = false - - private var processBuffer: ByteBuffer - private var fftBuffer: ByteBuffer - private var outputBuffer: ByteBuffer - - private val listeners = mutableListOf() - private var inputEnded: Boolean = false - - private lateinit var srcBuffer: ByteBuffer - private var srcBufferPosition = 0 - private val tempByteArray = ByteArray(SAMPLE_SIZE * 2) - - private var audioTrackBufferSize = 0 - - private val src = FloatArray(SAMPLE_SIZE) - private val dst = FloatArray(SAMPLE_SIZE + 2) - - interface FFTListener { - fun onFFTReady(sampleRateHz: Int, channelCount: Int, fft: FloatArray) - } - - fun addListener(listener: FFTListener) { - listeners.add(listener) - } - - fun removeListener(listener: FFTListener) { - listeners.remove(listener) - } - - init { - processBuffer = AudioProcessor.EMPTY_BUFFER - fftBuffer = AudioProcessor.EMPTY_BUFFER - outputBuffer = AudioProcessor.EMPTY_BUFFER - } - - /** - * The following method matches the implementation of getDefaultBufferSize in DefaultAudioSink - * of ExoPlayer. - * Because there is an AudioTrack buffer between the processor and the sound output, the processor receives everything early. - * By putting the audio data to process in a buffer which has the same size as the audiotrack buffer, - * we will delay ourselves to match the audio output. - */ - private fun getDefaultBufferSizeInBytes(audioFormat: AudioProcessor.AudioFormat): Int { - val outputPcmFrameSize = - Util.getPcmFrameSize(audioFormat.encoding, audioFormat.channelCount) - val minBufferSize = - AudioTrack.getMinBufferSize( - audioFormat.sampleRate, - Util.getAudioTrackChannelConfig(audioFormat.channelCount), - audioFormat.encoding - ) - Assertions.checkState(minBufferSize != ERROR_BAD_VALUE) - val multipliedBufferSize = minBufferSize * EXO_BUFFER_MULTIPLICATION_FACTOR - val minAppBufferSize = - durationUsToFrames(EXO_MIN_BUFFER_DURATION_US).toInt() * outputPcmFrameSize - val maxAppBufferSize = max( - minBufferSize.toLong(), - durationUsToFrames(EXO_MAX_BUFFER_DURATION_US) * outputPcmFrameSize - ).toInt() - val bufferSizeInFrames = Util.constrainValue( - multipliedBufferSize, - minAppBufferSize, - maxAppBufferSize - ) / outputPcmFrameSize - return bufferSizeInFrames * outputPcmFrameSize - } - - private fun durationUsToFrames(durationUs: Long): Long { - return durationUs * inputAudioFormat.sampleRate / C.MICROS_PER_SECOND - } - - override fun isActive(): Boolean { - return isActive - } - - private lateinit var inputAudioFormat: AudioProcessor.AudioFormat - - override fun configure(inputAudioFormat: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat { - if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { - throw AudioProcessor.UnhandledAudioFormatException( - inputAudioFormat - ) - } - this.inputAudioFormat = inputAudioFormat - isActive = true - - noise = Noise.real(SAMPLE_SIZE) - - audioTrackBufferSize = getDefaultBufferSizeInBytes(inputAudioFormat) - - srcBuffer = ByteBuffer.allocate(audioTrackBufferSize + BUFFER_EXTRA_SIZE) - - return inputAudioFormat - } - - override fun queueInput(inputBuffer: ByteBuffer) { - var position = inputBuffer.position() - val limit = inputBuffer.limit() - val frameCount = (limit - position) / (2 * inputAudioFormat.channelCount) - val singleChannelOutputSize = frameCount * 2 - val outputSize = frameCount * inputAudioFormat.channelCount * 2 - - - if (processBuffer.capacity() < outputSize) { - processBuffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()) - } else { - processBuffer.clear() - } - - if (fftBuffer.capacity() < singleChannelOutputSize) { - fftBuffer = - ByteBuffer.allocateDirect(singleChannelOutputSize).order(ByteOrder.nativeOrder()) - } else { - fftBuffer.clear() - } - - while (position < limit) { - var summedUp = 0 - for (channelIndex in 0 until inputAudioFormat.channelCount) { - val current = inputBuffer.getShort(position + 2 * channelIndex) - processBuffer.putShort(current) - summedUp += current - } - // For the FFT, we use an currentAverage of all the channels - fftBuffer.putShort((summedUp / inputAudioFormat.channelCount).toShort()) - position += inputAudioFormat.channelCount * 2 - } - - inputBuffer.position(limit) - - processFFT(this.fftBuffer) - - processBuffer.flip() - outputBuffer = this.processBuffer - } - - private fun processFFT(buffer: ByteBuffer) { - scope.launch { - //Need to additional delay because exoplayer sends data before silence skipping - //if this processor is added after silence skipping, - //it gives weird artifacts in the audio - delay(5000) - if (listeners.isEmpty()) return@launch - - srcBuffer.put(buffer.array()) - srcBufferPosition += buffer.array().size - // Since this is PCM 16 bit, each sample will be 2 bytes. - // So to get the sample size in the end, we need to take twice as many bytes off the buffer - val bytesToProcess = SAMPLE_SIZE * 2 - var currentByte: Byte? = null - while (srcBufferPosition > audioTrackBufferSize) { - srcBuffer.position(0) - srcBuffer.get(tempByteArray, 0, bytesToProcess) - - tempByteArray.forEachIndexed { index, byte -> - if (currentByte == null) { - currentByte = byte - } else { - src[index / 2] = - (currentByte!!.toFloat() * Byte.MAX_VALUE + byte) / (Byte.MAX_VALUE * Byte.MAX_VALUE) - dst[index / 2] = 0f - currentByte = null - } - - } - srcBuffer.position(bytesToProcess) - srcBuffer.compact() - srcBufferPosition -= bytesToProcess - runCatching { - srcBuffer.position(srcBufferPosition) - val fft = noise?.fft(src, dst)!! - - listeners.forEach { - it.onFFTReady( - inputAudioFormat.sampleRate, - inputAudioFormat.channelCount, - fft - ) - } - } - } - } - } - - private val scope = CoroutineScope(Dispatchers.Main) - - override fun queueEndOfStream() { - inputEnded = true - processBuffer = AudioProcessor.EMPTY_BUFFER - } - - override fun getOutput(): ByteBuffer { - val outputBuffer = this.outputBuffer - this.outputBuffer = AudioProcessor.EMPTY_BUFFER - return outputBuffer - } - - override fun isEnded(): Boolean { - return inputEnded && processBuffer === AudioProcessor.EMPTY_BUFFER - } - - override fun flush() { - outputBuffer = AudioProcessor.EMPTY_BUFFER - inputEnded = false - // A new stream is incoming. - } - - override fun reset() { - flush() - processBuffer = AudioProcessor.EMPTY_BUFFER - inputAudioFormat = - AudioProcessor.AudioFormat(Format.NO_VALUE, Format.NO_VALUE, Format.NO_VALUE) - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/render/RenderersFactory.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/render/RenderersFactory.kt index d61e875b..9227bb02 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/render/RenderersFactory.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/render/RenderersFactory.kt @@ -2,7 +2,6 @@ package dev.brahmkshatriya.echo.playback.render import android.content.Context import androidx.annotation.OptIn -import androidx.media3.common.audio.AudioProcessor import androidx.media3.common.audio.SonicAudioProcessor import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.DefaultRenderersFactory @@ -11,8 +10,7 @@ import androidx.media3.exoplayer.audio.SilenceSkippingAudioProcessor @OptIn(UnstableApi::class) class RenderersFactory( - context: Context, - private val audioProcessor: AudioProcessor + context: Context ) : DefaultRenderersFactory(context) { override fun buildAudioSink( context: Context, @@ -32,7 +30,7 @@ class RenderersFactory( .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) .setAudioProcessorChain( DefaultAudioSink.DefaultAudioProcessorChain( - arrayOf(audioProcessor), silenceSkippingAudioProcessor, SonicAudioProcessor() + emptyArray(), silenceSkippingAudioProcessor, SonicAudioProcessor() ) ) .build() diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemViewModel.kt index 99c40d81..a051932f 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemViewModel.kt @@ -1,6 +1,7 @@ package dev.brahmkshatriya.echo.ui.item import android.app.Application +import android.content.Context import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import androidx.paging.PagingData @@ -85,31 +86,7 @@ class ItemViewModel @Inject constructor( ) is TrackItem -> loadItem( - item, { loadTrack(it.track).toMediaItem() }, { trackItem -> - val client = this - val track = trackItem.track - val album = trackItem.track.album - val artists = trackItem.track.artists - PagedData.Concat( - if (client is AlbumClient && album != null) PagedData.Single { - listOf( - client.loadAlbum(album).toMediaItem().toShelf() - ) - } else PagedData.empty(), - if (artists.isNotEmpty()) PagedData.Single { - listOf( - Shelf.Lists.Items( - app.getString(R.string.artists), - if (client is ArtistClient) artists.map { - val artist = client.loadArtist(it) - artist.toMediaItem() - } else artists.map { it.toMediaItem() } - ) - ) - } else PagedData.empty(), - client.getShelves(track) - ) - } + item, { loadTrack(it.track).toMediaItem() }, { getTrackShelves(it.track, app) } ) is RadioItem -> loadItem(item, { it }, { null }) @@ -231,4 +208,32 @@ class ItemViewModel @Inject constructor( } } + companion object { + + fun Any.getTrackShelves( + track: Track, context: Context + ): PagedData.Concat { + val album = track.album + val artists = track.artists + return PagedData.Concat( + if (album != null) PagedData.Single { + val a = if (this !is AlbumClient) album + else loadAlbum(album) + listOf(a.toMediaItem().toShelf()) + } else PagedData.empty(), + if (artists.isNotEmpty()) PagedData.Single { + listOf( + Shelf.Lists.Items( + context.getString(R.string.artists), + if (this is ArtistClient) artists.map { + val artist = loadArtist(it) + artist.toMediaItem() + } else artists.map { it.toMediaItem() } + ) + ) + } else PagedData.empty(), + if (this is TrackClient) getShelves(track) else PagedData.empty() + ) + } + } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerFragment.kt index 6dc11355..f61d73c3 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerFragment.kt @@ -1,9 +1,14 @@ package dev.brahmkshatriya.echo.ui.player +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.WindowManager +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope @@ -19,6 +24,7 @@ import dev.brahmkshatriya.echo.databinding.FragmentPlayerBinding import dev.brahmkshatriya.echo.ui.common.openFragment import dev.brahmkshatriya.echo.ui.item.ItemBottomSheet import dev.brahmkshatriya.echo.ui.item.ItemFragment +import dev.brahmkshatriya.echo.utils.animateVisibility import dev.brahmkshatriya.echo.utils.autoCleared import dev.brahmkshatriya.echo.utils.emit import dev.brahmkshatriya.echo.utils.observe @@ -116,12 +122,35 @@ class PlayerFragment : Fragment() { viewModel.browser.value?.volume = 1 + min(0f, it) val offset = max(0f, it) binding.playerOutline.alpha = 1 - offset + if (offset < 1) + requireActivity().hideSystemUi(false) + else if (uiViewModel.playerBgVisibleState.value) + requireActivity().hideSystemUi(true) } observe(uiViewModel.infoSheetState) { binding.viewPager.isUserInputEnabled = requireContext().isLandscape() || it == STATE_COLLAPSED } + + observe(uiViewModel.playerBgVisibleState) { + binding.playerInfoContainer.animateVisibility(!it) + requireActivity().hideSystemUi(it) + } + } + + private fun Activity.hideSystemUi(hide: Boolean) { + val controller = WindowCompat.getInsetsController(window, window.decorView) + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + + if (hide) { + controller.hide(WindowInsetsCompat.Type.systemBars()) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + controller.show(WindowInsetsCompat.Type.systemBars()) + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } } private fun ViewPager2.registerOnUserPageChangeCallback( diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt index 5358ac9f..44a7e824 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt @@ -10,13 +10,11 @@ import android.text.Spanned import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.ViewGroup -import android.view.ViewGroup.MarginLayoutParams import android.widget.TextView import androidx.annotation.OptIn import androidx.appcompat.content.res.AppCompatResources import androidx.core.net.toUri import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams import androidx.core.view.updatePaddingRelative import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -64,7 +62,6 @@ import dev.brahmkshatriya.echo.ui.player.PlayerColors.Companion.defaultPlayerCol import dev.brahmkshatriya.echo.ui.player.PlayerColors.Companion.getColorsFrom import dev.brahmkshatriya.echo.ui.settings.LookFragment import dev.brahmkshatriya.echo.utils.animateVisibility -import dev.brahmkshatriya.echo.utils.dpToPx import dev.brahmkshatriya.echo.utils.emit import dev.brahmkshatriya.echo.utils.load import dev.brahmkshatriya.echo.utils.loadBitmap @@ -75,8 +72,8 @@ import dev.brahmkshatriya.echo.utils.toTimeString import dev.brahmkshatriya.echo.viewmodels.PlayerViewModel import dev.brahmkshatriya.echo.viewmodels.UiViewModel import dev.brahmkshatriya.echo.viewmodels.UiViewModel.Companion.applyInsets -import dev.brahmkshatriya.echo.viewmodels.UiViewModel.Companion.isLandscape import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch import kotlin.math.max import kotlin.math.min @@ -105,18 +102,59 @@ class PlayerTrackAdapter( return ViewHolder(ItemPlayerTrackBinding.inflate(inflater, parent, false)) } + @OptIn(UnstableApi::class) inner class ViewHolder(val binding: ItemPlayerTrackBinding) : Holder(binding.root) { + override fun bind(item: MediaItem) { + this.item = item onBind(bindingAdapterPosition) + + applyVideoVisibility(false) + binding.bgVideo.run { + player = null + val background = item.background + if (background != null && showBackground()) { + resizeMode = RESIZE_MODE_ZOOM + player = getPlayer(context, viewModel.cache, background) + applyVideoVisibility(true) + } + } + observe(merge(viewModel.currentFlow, viewModel.browser)) { applyVideo() } + } + + private fun applyVideoVisibility(visible: Boolean) { + binding.bgVideo.isVisible = visible + binding.bgImage.isVisible = !visible + } + + private fun isCurrent() = viewModel.currentFlow.value?.index == bindingAdapterPosition + var item: MediaItem? = null + fun applyVideo() { + if (item?.background != null) return + if (isCurrent()) { + val player = viewModel.browser.value + binding.bgVideo.resizeMode = RESIZE_MODE_FIT + binding.bgVideo.player = player + applyVideoVisibility(player.hasVideo()) + } else { + binding.bgVideo.player = null + applyVideoVisibility(false) + } + } + + init { + viewModel.browser.value!!.addListener(object : Player.Listener { + override fun onTracksChanged(tracks: Tracks) { + applyVideo() + } + }) } } private val viewModel by fragment.activityViewModels() private val uiViewModel by fragment.activityViewModels() - @OptIn(UnstableApi::class) fun ViewHolder.onBind(position: Int) { - binding.bgVisualizer.processor = viewModel.fftAudioProcessor val item = getItem(position) ?: return val clientId = item.clientId @@ -126,11 +164,7 @@ class PlayerTrackAdapter( lifecycleScope.launch { val bitmap = item.track.cover?.loadBitmap(binding.root.context) val colors = binding.root.context.getPlayerColors(bitmap) - binding.root.setBackgroundColor(colors.background) - binding.bgInfoSpace.setBackgroundColor(colors.background) - binding.bgVisualizer.setColors(colors.accent) binding.bgGradient.imageTintList = ColorStateList.valueOf(colors.background) - binding.bgInfoGradient.imageTintList = ColorStateList.valueOf(colors.background) binding.expandedToolbar.run { setTitleTextColor(colors.text) setSubtitleTextColor(colors.text) @@ -138,9 +172,8 @@ class PlayerTrackAdapter( binding.collapsedContainer.applyColors(colors) binding.playerControls.applyColors(colors) - binding.bgInfoTitle.setTextColor(colors.text) - binding.bgInfoArtist.setTextColor(colors.text) - binding.bgImage.loadBlurred(bitmap, 12f) + if (showBackground()) binding.bgImage.loadBlurred(bitmap, 12f) + else binding.bgImage.setImageDrawable(null) } binding.collapsedContainer.root.setOnClickListener { @@ -165,7 +198,6 @@ class PlayerTrackAdapter( observe(uiViewModel.playerBgVisibleState) { val animate = viewModel.currentFlow.value?.index == bindingAdapterPosition - binding.bgInfoContainer.animateVisibility(it, animate) if (uiViewModel.playerSheetState.value == STATE_EXPANDED) { binding.bgGradient.animateVisibility(!it, animate) binding.expandedTrackCoverContainer.animateVisibility(!it, animate) @@ -179,28 +211,22 @@ class PlayerTrackAdapter( } } - binding.bgImage.setOnClickListener { + binding.bgContainer.setOnClickListener { emit(uiViewModel.playerBgVisibleState) { false } if (uiViewModel.infoSheetState.value == STATE_EXPANDED) emit(uiViewModel.changeInfoState) { STATE_COLLAPSED } } -// binding.bgVideo.setOnClickListener { -// emit(uiViewModel.playerBgVisibleState) { false } -// if (uiViewModel.infoSheetState.value == STATE_EXPANDED) -// emit(uiViewModel.changeInfoState) { STATE_COLLAPSED } -// } - binding.expandedTrackCover.setOnClickListener { - emit(uiViewModel.playerBgVisibleState) { true } + val bgImage = binding.bgImage.drawable != null + if (bgImage || viewModel.browser.value.hasVideo() || item.background != null) + emit(uiViewModel.playerBgVisibleState) { true } } observe(uiViewModel.infoSheetOffset) { if (uiViewModel.playerBgVisibleState.value) { binding.bgGradient.isVisible = it != 0f binding.bgGradient.alpha = it - if (!binding.root.context.isLandscape()) - binding.bgInfoContainer.alpha = 1 - it } else { binding.expandedContainer.alpha = 1 - it binding.expandedContainer.isVisible = it != 1f @@ -209,49 +235,14 @@ class PlayerTrackAdapter( observe(uiViewModel.systemInsets) { binding.expandedTrackCoverContainer.applyInsets(it, 24) - binding.bgInfoContainer.applyInsets(it) - binding.bgInfoSpace.updateLayoutParams { - val context = binding.bgInfoSpace.context - height = (if (context.isLandscape()) 0 else 64).dpToPx(context) + it.bottom - bottomMargin = -it.bottom - } binding.collapsedContainer.root.updatePaddingRelative(start = it.start, end = it.end) } binding.playerControls.bind(this, item, clientId) - - //VIDEO STUFF - binding.bgVideo.apply { - isVisible = false - val background = item.background - if (background != null) { - isVisible = true - val player = getPlayer(context, viewModel.cache, background) - setPlayer(player) - resizeMode = RESIZE_MODE_ZOOM - } else { - observe(viewModel.currentFlow) { - val isCurrent = it?.index == bindingAdapterPosition - if (!isCurrent) return@observe - post { - setPlayer(null) - val player = viewModel.browser.value - isVisible = player != null && player.hasVideo() - setPlayer(player) - player?.addListener(object : Player.Listener { - override fun onTracksChanged(tracks: Tracks) { - isVisible = player.hasVideo() - } - }) - } - } - resizeMode = RESIZE_MODE_FIT - } - } } - private fun Player.hasVideo() = - currentTracks.groups.any { it.type == C.TRACK_TYPE_VIDEO } + private fun Player?.hasVideo() = + this?.currentTracks?.groups.orEmpty().any { it.type == C.TRACK_TYPE_VIDEO } private fun ItemPlayerControlsBinding.bind( viewHolder: ViewHolder, @@ -439,7 +430,6 @@ class PlayerTrackAdapter( playerControls.run { applyTitles(track, trackTitle, trackArtist, client, listener) } - applyTitles(track, bgInfoTitle, bgInfoArtist, client, listener) expandedToolbar.run { val itemContext = item.context @@ -483,19 +473,20 @@ class PlayerTrackAdapter( trackArtist.movementMethod = LinkMovementMethod.getInstance() } + private fun showBackground() = + viewModel.settings.getBoolean(LookFragment.SHOW_BACKGROUND, true) + + private fun isDynamic() = + viewModel.settings.getBoolean(LookFragment.DYNAMIC_PLAYER, true) + private fun Context.getPlayerColors(bitmap: Bitmap?): PlayerColors { val defaultColors = defaultPlayerColors() bitmap ?: return defaultColors - val preferences = - applicationContext.getSharedPreferences(packageName, Context.MODE_PRIVATE) - val dynamicPlayer = preferences.getBoolean(LookFragment.DYNAMIC_PLAYER, true) - val imageColors = - if (dynamicPlayer) getColorsFrom(bitmap) else defaultColors + val imageColors = if (isDynamic()) getColorsFrom(bitmap) else defaultColors return imageColors ?: defaultColors } private fun ItemPlayerCollapsedBinding.applyColors(colors: PlayerColors) { -// root.setBackgroundColor(colors.background) collapsedProgressBar.setIndicatorColor(colors.accent) collapsedSeekBar.setIndicatorColor(colors.accent) collapsedBuffer.setIndicatorColor(colors.accent) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerUiListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerUiListener.kt index eb74cf62..e111f3ab 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerUiListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerUiListener.kt @@ -110,6 +110,7 @@ class PlayerUiListener( override fun onPlayerError(error: PlaybackException) { viewModel.isPlaying.value = false + viewModel.buffering.value = false val cause = error.cause?.cause ?: error.cause ?: error viewModel.run { if (cause !is StreamableLoadingException) createException(cause.toExceptionDetails(app)) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/TrackDetailsFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/TrackDetailsFragment.kt index b6da9c5f..828cc007 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/TrackDetailsFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/TrackDetailsFragment.kt @@ -27,10 +27,6 @@ import com.google.android.material.chip.ChipGroup import dagger.hilt.android.lifecycle.HiltViewModel import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.common.MusicExtension -import dev.brahmkshatriya.echo.common.clients.AlbumClient -import dev.brahmkshatriya.echo.common.clients.ArtistClient -import dev.brahmkshatriya.echo.common.clients.TrackClient -import dev.brahmkshatriya.echo.common.helpers.PagedData import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem import dev.brahmkshatriya.echo.common.models.Shelf import dev.brahmkshatriya.echo.common.models.Streamable @@ -49,6 +45,8 @@ import dev.brahmkshatriya.echo.playback.MediaItemUtils.subtitleIndex import dev.brahmkshatriya.echo.playback.MediaItemUtils.track import dev.brahmkshatriya.echo.ui.adapter.ShelfAdapter import dev.brahmkshatriya.echo.ui.adapter.ShelfClickListener +import dev.brahmkshatriya.echo.ui.item.ExplicitAdapter +import dev.brahmkshatriya.echo.ui.item.ItemViewModel.Companion.getTrackShelves import dev.brahmkshatriya.echo.ui.paging.toFlow import dev.brahmkshatriya.echo.utils.autoCleared import dev.brahmkshatriya.echo.utils.observe @@ -93,7 +91,9 @@ class TrackDetailsFragment : Fragment() { val adapter = ShelfAdapter(this, "track_details", extension, shelfClickListener) mediaAdapter = adapter - binding.root.adapter = ConcatAdapter(infoAdapter, adapter.withLoaders()) + binding.root.adapter = ConcatAdapter( + ExplicitAdapter(track.toMediaItem()), infoAdapter, adapter.withLoaders() + ) viewModel.load(item.clientId, track) } @@ -401,33 +401,10 @@ class TrackDetailsFragment : Fragment() { itemsFlow.value = null val extension = extensionListFlow.getExtension(clientId) ?: return - val client = extension.instance.value.getOrNull() - val album = track.album - val artists = track.artists - viewModelScope.launch { - val pagedData = PagedData.Concat( - if (client is AlbumClient && album != null) PagedData.Single { - listOf( - client.loadAlbum(album).toMediaItem().toShelf() - ) - } else PagedData.empty(), - if (artists.isNotEmpty()) PagedData.Single { - listOf( - Shelf.Lists.Items( - app.getString(R.string.artists), - if (client is ArtistClient) artists.map { - val artist = client.loadArtist(it) - artist.toMediaItem() - } else artists.map { it.toMediaItem() } - ) - ) - } else PagedData.empty(), - if (client is TrackClient) extension.run(throwableFlow) { - client.getShelves(track) - } ?: PagedData.empty() - else PagedData.empty() - ) + val pagedData = extension.run(throwableFlow) { + getTrackShelves(track, app) + } ?: return@launch pagedData.toFlow().collectTo(itemsFlow) } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/settings/LookFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/settings/LookFragment.kt index c917ec46..de5b4aea 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/settings/LookFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/settings/LookFragment.kt @@ -119,6 +119,16 @@ class LookFragment : BaseSettingsFragment() { setDefaultValue(true) addPreference(this) } + + SwitchPreferenceCompat(context).apply { + key = SHOW_BACKGROUND + title = getString(R.string.show_background) + summary = getString(R.string.show_background_summary) + layoutResource = R.layout.preference_switch + isIconSpaceReserved = false + setDefaultValue(true) + addPreference(this) + } } PreferenceCategory(context).apply { @@ -161,6 +171,7 @@ class LookFragment : BaseSettingsFragment() { const val AMOLED_KEY = "amoled" const val NAVBAR_GRADIENT = "navbar_gradient" const val DYNAMIC_PLAYER = "dynamic_player" + const val SHOW_BACKGROUND = "show_background" const val ANIMATIONS_KEY = "animations" const val SHARED_ELEMENT_KEY = "shared_element_transitions" } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/ExoVisualizer.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/ExoVisualizer.kt deleted file mode 100644 index 23be5b4d..00000000 --- a/app/src/main/java/dev/brahmkshatriya/echo/utils/ExoVisualizer.kt +++ /dev/null @@ -1,222 +0,0 @@ -package dev.brahmkshatriya.echo.utils - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.Path -import android.util.AttributeSet -import android.view.View -import android.widget.FrameLayout -import dev.brahmkshatriya.echo.playback.render.FFTAudioProcessor -import java.lang.System.arraycopy -import kotlin.math.cos -import kotlin.math.floor -import kotlin.math.pow -import kotlin.math.roundToInt - -/** - * The visualizer is a view which listens to the FFT changes and forwards it to the band view. - * https://github.com/dzolnai/ExoVisualizer - */ -class ExoVisualizer @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr), FFTAudioProcessor.FFTListener { - - var processor: FFTAudioProcessor? = null - private var currentWaveform: FloatArray? = null - - private val bandView = FFTBandView(context, attrs) - - init { - addView(bandView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) - } - - private fun updateProcessorListenerState(enable: Boolean) { - if (enable) { - processor?.addListener(this) - } else { - processor?.removeListener(this) - currentWaveform = null - } - } - - fun setColors(color: Int) { - bandView.setColor(color) - } - - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - updateProcessorListenerState(true) - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - updateProcessorListenerState(false) - } - - override fun onFFTReady(sampleRateHz: Int, channelCount: Int, fft: FloatArray) { - currentWaveform = fft - bandView.onFFT(fft) - } - - class FFTBandView @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 - ) : View(context, attrs, defStyleAttr) { - - companion object { - // Taken from: https://en.wikipedia.org/wiki/Preferred_number#Audio_frequencies -// private val FREQUENCY_BAND_LIMITS = arrayOf( -// 20, 25, 32, 40, 50, 63, 80, 100, -// 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, -// 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, -// 12500, 16000, 20000 -// ) - private val FREQUENCY_BAND_LIMITS = (0..31).map { - (20 * 1.25.pow(it)).roundToInt().coerceAtMost(20000) - } - } - - private val bands = FREQUENCY_BAND_LIMITS.size - private val size = FFTAudioProcessor.SAMPLE_SIZE / 2 - private val maxConst = 100000 // Reference max value for accum magnitude - - private val fft: FloatArray = FloatArray(size) - private val paintPaths = listOf( -// Paint() -// Paint(), Paint(), Paint() - ) - private val paintBandsFill = Paint() - - // We average out the values over 3 occurences (plus the current one), so big jumps are smoothed out - private val smoothingFactor = 8 - private val previousValues = FloatArray(bands * smoothingFactor) - - private val fftPath = paintPaths.map { Path() } - - private var startedAt: Long = 0 - - init { - paintBandsFill.style = Paint.Style.FILL - paintPaths.forEach { paint -> - paint.strokeWidth = 8f - paint.isAntiAlias = true - paint.style = Paint.Style.STROKE - } - } - - fun setColor(color: Int) { - paintBandsFill.color = color - paintPaths.forEachIndexed { index, paint -> - paint.color = color - paint.alpha = 250 * (index + 1) / paintPaths.size - } - } - - private fun drawAudio(canvas: Canvas) { - // Clear the previous drawing on the screen - canvas.drawColor(Color.TRANSPARENT) - - // Set up counters and widgets - var currentFftPosition = 0 - var currentFrequencyBandLimitIndex = 0 - fftPath.forEach { it.reset() } - fftPath.forEach { it.moveTo(0f, height.toFloat()) } - - // Iterate over the entire FFT result array - while (currentFftPosition < size) { - var accum = 0f - - // We divide the bands by frequency. - // Check until which index we need to stop for the current band - val nextLimitAtPosition = - floor(FREQUENCY_BAND_LIMITS[currentFrequencyBandLimitIndex] / 20_000.toFloat() * size).toInt() - - synchronized(fft) { - // Here we iterate within this single band - for (j in 0 until nextLimitAtPosition - currentFftPosition step 2) { - // Convert real and imaginary part to get energy - val raw = (fft[currentFftPosition + j].toDouble().pow(2.0) + - fft.getOrElse(currentFftPosition + j + 1) { - fft.last() - }.toDouble().pow(2.0)).toFloat() - - // Hamming window (by frequency band instead of frequency, otherwise it would prefer 10kHz, which is too high) - // The window mutes down the very high and the very low frequencies, usually not hearable by the human ear - val m = bands / 2 - val windowed = - raw * (0.54f - 0.46f * cos(2 * Math.PI * currentFrequencyBandLimitIndex / (m + 1))).toFloat() - accum += windowed - } - } - // A window might be empty which would result in a 0 division - if (nextLimitAtPosition - currentFftPosition != 0) - accum /= nextLimitAtPosition - currentFftPosition - else accum = 0.0f - currentFftPosition = nextLimitAtPosition - - // Here we do the smoothing - // If you increase the smoothing factor, the high shoots will be toned down, but the - // 'movement' in general will decrease too - var smoothedAccum = accum - for (i in 0 until smoothingFactor) { - smoothedAccum += previousValues[i * bands + currentFrequencyBandLimitIndex] - if (i != smoothingFactor - 1) { - previousValues[i * bands + currentFrequencyBandLimitIndex] = - previousValues[(i + 1) * bands + currentFrequencyBandLimitIndex] - } else { - previousValues[i * bands + currentFrequencyBandLimitIndex] = accum - } - } - smoothedAccum /= smoothingFactor + 1 // +1 because it also includes the current value - - val leftX = width * (currentFrequencyBandLimitIndex / bands.toFloat()) - val rightX = leftX + width / bands.toFloat() - - val fillBarHeight = height * (smoothedAccum / maxConst).coerceAtMost(1f) - val fillTop = height - fillBarHeight - canvas.drawRect( - leftX, - fillTop, - rightX, - height.toFloat(), - paintBandsFill - ) - fftPath.forEachIndexed { index, path -> - val barHeight = height * (smoothedAccum / maxConst).coerceAtMost(1f) - val top = height - (barHeight * (index + 1) / paintPaths.size.toFloat()) - if (top > 1) path.lineTo((leftX + rightX) / 2, top) - } - - currentFrequencyBandLimitIndex++ - } - - paintPaths.forEachIndexed { index, paintPath -> - canvas.drawPath(fftPath[index], paintPath) - } - } - - fun onFFT(fft: FloatArray) { - synchronized(this.fft) { - if (startedAt == 0L) { - startedAt = System.currentTimeMillis() - } - // The resulting graph is mirrored, because we are using real numbers instead of imaginary - // Explanations: https://www.mathworks.com/matlabcentral/answers/338408-why-are-fft-diagrams-mirrored - // https://dsp.stackexchange.com/questions/4825/why-is-the-fft-mirrored/4827#4827 - // So what we do here, we only check the left part of the graph. - arraycopy(fft, 2, this.fft, 0, size) - // By calling invalidate, we request a redraw. - invalidate() - } - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - drawAudio(canvas) - // By calling invalidate, we request a redraw. See https://github.com/dzolnai/ExoVisualizer/issues/2 - invalidate() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/PlayerViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/PlayerViewModel.kt index f1b66ee4..a0dd0b57 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/PlayerViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/PlayerViewModel.kt @@ -30,7 +30,6 @@ import dev.brahmkshatriya.echo.playback.MediaItemUtils import dev.brahmkshatriya.echo.playback.PlayerCommands.radioCommand import dev.brahmkshatriya.echo.playback.ResumptionUtils import dev.brahmkshatriya.echo.playback.listeners.Radio -import dev.brahmkshatriya.echo.playback.render.FFTAudioProcessor import dev.brahmkshatriya.echo.ui.editplaylist.EditPlaylistViewModel.Companion.deletePlaylist import dev.brahmkshatriya.echo.ui.exception.ExceptionFragment import dev.brahmkshatriya.echo.ui.player.CheckBoxListener @@ -57,7 +56,6 @@ class PlayerViewModel @Inject constructor( val radioStateFlow: MutableStateFlow, val currentServers: MutableStateFlow>, val cache: SimpleCache, - val fftAudioProcessor: FFTAudioProcessor, private val mutableMessageFlow: MutableSharedFlow, throwableFlow: MutableSharedFlow, ) : CatchingViewModel(throwableFlow) { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/UiViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/UiViewModel.kt index 8306e598..5b92fdba 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/UiViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/UiViewModel.kt @@ -96,7 +96,7 @@ class UiViewModel @Inject constructor( } fun setSystemInsets(context: Context, insets: WindowInsetsCompat) { - val system = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val system = insets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars()) val inset = system.run { if (context.isRTL()) Insets(top, bottom, right, left) else Insets(top, bottom, left, right) diff --git a/app/src/main/res/layout-land/item_player_track.xml b/app/src/main/res/layout-land/item_player_track.xml index c8767bb1..7493f5ba 100644 --- a/app/src/main/res/layout-land/item_player_track.xml +++ b/app/src/main/res/layout-land/item_player_track.xml @@ -37,88 +37,6 @@ app:tintMode="src_in" /> - - - - - - - - - - - - - - - - - - - - - - - - + app:trackThickness="1dp" /> + app:trackThickness="1dp" /> + app:trackThickness="2dp" /> + app:trackHeight="2dp" /> - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_quick_search_query.xml b/app/src/main/res/layout/item_quick_search_query.xml index 9e9fe169..7c5000ca 100644 --- a/app/src/main/res/layout/item_quick_search_query.xml +++ b/app/src/main/res/layout/item_quick_search_query.xml @@ -37,7 +37,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/more" - android:padding="8dp" + android:padding="12dp" app:icon="@drawable/ic_arrow_insert" app:iconSize="24dp" app:iconTint="?colorOnSurface" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92995579..baf348b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -270,6 +270,8 @@ Blacklist Folder Keywords Keywords to blacklist folders, use comma for separator Explicit + Show Background + Background video or blurred artwork of track in player Highest Medium diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/helpers/PagedData.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/helpers/PagedData.kt index 8eb35212..22eeb9cb 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/helpers/PagedData.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/helpers/PagedData.kt @@ -180,8 +180,9 @@ sealed class PagedData { override suspend fun loadAll(): List = sources.flatMap { it.loadAll() } private fun splitContinuation(continuation: String?): Pair { - val index = continuation?.substringBefore("_")?.toIntOrNull() ?: -1 - val token = continuation?.substringAfter("_") + if (continuation == null) return 0 to null + val index = continuation.substringBefore("_").toIntOrNull() ?: -1 + val token = continuation.substringAfter("_") return index to token }