From 905e6e05e78977b274f568337bff1398052018de Mon Sep 17 00:00:00 2001 From: Nguyen Duc Tuan Minh Date: Sun, 21 Jul 2024 16:34:31 +0700 Subject: [PATCH] Done migrate NowPlayingFragment to Compose. Fixed #448, Fixed #441 --- .../simpmusic/adapter/queue/QueueAdapter.kt | 9 + .../data/repository/MainRepository.kt | 5 +- .../simpmusic/service/SimpleMediaService.kt | 6 + .../service/SimpleMediaServiceHandler.kt | 146 ++++- .../com/maxrave/simpmusic/ui/MainActivity.kt | 218 ++----- .../simpmusic/ui/component/LyricsView.kt | 538 +++++++++++++++++- .../simpmusic/ui/component/MediaPlayerView.kt | 8 +- .../ui/component/ModalBottomSheet.kt | 524 +++++++++++------ .../ui/fragment/player/FullscreenFragment.kt | 178 +++--- .../ui/fragment/player/NowPlayingFragment.kt | 63 +- .../ui/fragment/player/QueueFragment.kt | 14 +- .../ui/screen/library/PlaylistScreen.kt | 4 +- .../ui/screen/player/NowPlayingScreen.kt | 176 +++++- .../simpmusic/viewModel/SharedViewModel.kt | 128 +---- app/src/main/res/anim/btt.xml | 7 + app/src/main/res/anim/ttb.xml | 7 + app/src/main/res/values/strings.xml | 1 + 17 files changed, 1319 insertions(+), 713 deletions(-) create mode 100644 app/src/main/res/anim/btt.xml create mode 100644 app/src/main/res/anim/ttb.xml diff --git a/app/src/main/java/com/maxrave/simpmusic/adapter/queue/QueueAdapter.kt b/app/src/main/java/com/maxrave/simpmusic/adapter/queue/QueueAdapter.kt index 6afa554c..b963496c 100644 --- a/app/src/main/java/com/maxrave/simpmusic/adapter/queue/QueueAdapter.kt +++ b/app/src/main/java/com/maxrave/simpmusic/adapter/queue/QueueAdapter.kt @@ -34,6 +34,15 @@ class QueueAdapter( mSwapListener = listener } + fun getIndexOf(videoId: String): Int? { + for (i in listTrack.indices) { + if (listTrack.getOrNull(i)?.videoId == videoId) { + return i + } + } + return null + } + interface OnOptionClickListener { fun onOptionClick(position: Int) } diff --git a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt index 14cf32a1..fa83b724 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt @@ -1608,7 +1608,6 @@ class MainRepository } else { searchResponse.tracks.items.firstOrNull() } - Log.d("Lyrics", "track: $track") if (track != null) { YouTube.getSpotifyCanvas( track.id, @@ -1642,7 +1641,6 @@ class MainRepository } else { searchResponse.tracks.items.firstOrNull() } - Log.d("Lyrics", "track: $track") if (track != null) { YouTube.getSpotifyCanvas( track.id, @@ -1987,9 +1985,10 @@ class MainRepository .onFailure { Log.e(TAG, "Fix musixmatch search" + it.message.toString()) YouTube.getLrclibLyrics(qtrack, qartist, durationInt).onSuccess { + Log.w(TAG, "Liblrc Item lyrics ${it?.lyrics?.syncType.toString()}") it?.let { emit(Pair(id, Resource.Success(it.toLyrics()))) } }.onFailure { - it.printStackTrace() + Log.e(TAG, "Liblrc Error: ${it.message}") emit(Pair(id, Resource.Error("Not found"))) } } diff --git a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt index 00b08eb1..4826b78f 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt @@ -54,6 +54,7 @@ import kotlinx.coroutines.flow.cancellable import kotlinx.coroutines.runBlocking import javax.inject.Inject + @AndroidEntryPoint @UnstableApi class SimpleMediaService : MediaLibraryService() { @@ -177,6 +178,11 @@ class SimpleMediaService : MediaLibraryService() { Log.d("SimpleMediaService", "onDestroy: ") } + override fun onTrimMemory(level: Int) { + super.onTrimMemory(level) + simpleMediaServiceHandler.mayBeSaveRecentSong() + } + @UnstableApi override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) diff --git a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt index 202ccd53..0f5a3568 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt @@ -8,6 +8,8 @@ import android.media.audiofx.LoudnessEnhancer import android.os.Bundle import android.util.Log import android.widget.Toast +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.toBitmap import androidx.core.net.toUri import androidx.media3.common.C import androidx.media3.common.MediaItem @@ -23,6 +25,8 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.CommandButton import androidx.media3.session.MediaSession import androidx.media3.session.SessionCommand +import coil.ImageLoader +import coil.request.ImageRequest import com.maxrave.simpmusic.R import com.maxrave.simpmusic.common.ASC import com.maxrave.simpmusic.common.DESC @@ -34,11 +38,13 @@ import com.maxrave.simpmusic.data.model.browse.album.Track import com.maxrave.simpmusic.data.model.searchResult.songs.Artist import com.maxrave.simpmusic.data.repository.MainRepository import com.maxrave.simpmusic.extension.connectArtists +import com.maxrave.simpmusic.extension.getScreenSize import com.maxrave.simpmusic.extension.isVideo import com.maxrave.simpmusic.extension.toArrayListTrack import com.maxrave.simpmusic.extension.toListName import com.maxrave.simpmusic.extension.toSongEntity import com.maxrave.simpmusic.service.test.source.MergingMediaSourceFactory +import com.maxrave.simpmusic.ui.widget.BasicWidget import com.maxrave.simpmusic.utils.Resource import com.maxrave.simpmusic.viewModel.FilterState import kotlinx.coroutines.CoroutineScope @@ -80,6 +86,10 @@ class SimpleMediaServiceHandler( private var sleepTimerJob: Job? = null + private var downloadImageForWidgetJob: Job? = null + + private val basicWidget = BasicWidget.instance + private val _simpleMediaState = MutableStateFlow(SimpleMediaState.Initial) val simpleMediaState = _simpleMediaState.asStateFlow() @@ -109,17 +119,14 @@ class SimpleMediaServiceHandler( private var _stateFlow = MutableStateFlow(StateSource.STATE_CREATED) val stateFlow = _stateFlow.asStateFlow() - private var _currentSongIndex = MutableStateFlow(0) + private var _currentSongIndex = MutableStateFlow(player.currentMediaItemIndex) val currentSongIndex = _currentSongIndex.asSharedFlow() private var _nowPlayingState = MutableStateFlow(NowPlayingTrackState.initial()) val nowPlayingState = _nowPlayingState.asStateFlow() - private val _sleepMinutes = MutableStateFlow(0) - val sleepMinutes = _sleepMinutes.asSharedFlow() - - private val _sleepDone = MutableStateFlow(false) - val sleepDone = _sleepDone.asSharedFlow() + private var _sleepTimerState = MutableStateFlow(SleepTimerState(false, 0)) + val sleepTimerState = _sleepTimerState.asStateFlow() private var skipSilent = false @@ -144,6 +151,7 @@ class SimpleMediaServiceHandler( toggleLikeJob = Job() loadJob = Job() songEntityJob = Job() + downloadImageForWidgetJob = Job() skipSilent = runBlocking { dataStoreManager.skipSilent.first() == DataStoreManager.TRUE } normalizeVolume = runBlocking { dataStoreManager.normalizeVolume.first() == DataStoreManager.TRUE } @@ -183,6 +191,7 @@ class SimpleMediaServiceHandler( track = track, ) } + updateWidget(mediaItem) getDataOfNowPlayingTrackStateJob?.cancel() getDataOfNowPlayingTrackStateJob = coroutineScope.launch { if (track != null) { @@ -202,6 +211,8 @@ class SimpleMediaServiceHandler( mainRepository.getSongById(videoId).cancellable().singleOrNull().let { songEntity -> if (songEntity != null) { _controlState.update { it.copy(isLiked = songEntity.liked) } + mainRepository.updateSongInLibrary(LocalDateTime.now(), songEntity.videoId) + mainRepository.updateListenCount(songEntity.videoId) } else { _controlState.update { it.copy(isLiked = false) } mainRepository.insertSong( @@ -260,27 +271,30 @@ class SimpleMediaServiceHandler( // Set sleep timer fun sleepStart(minutes: Int) { - _sleepDone.value = false sleepTimerJob?.cancel() sleepTimerJob = coroutineScope.launch(Dispatchers.Main) { - _sleepMinutes.value = minutes + _sleepTimerState.update { + it.copy(isDone = false, timeRemaining = minutes) + } var count = minutes while (count > 0) { delay(60 * 1000L) count-- - _sleepMinutes.value = count + _sleepTimerState.update { + it.copy(isDone = false, timeRemaining = count) + } } player.pause() - _sleepMinutes.value = 0 - _sleepDone.value = true + _sleepTimerState.update { + it.copy(isDone = true, timeRemaining = 0) + } } } fun sleepStop() { - _sleepDone.value = false sleepTimerJob?.cancel() - _sleepMinutes.value = 0 + _sleepTimerState.value = SleepTimerState(false, 0) } private fun updateNextPreviousTrackAvailability() { @@ -301,7 +315,7 @@ class SimpleMediaServiceHandler( _queueData.value = _queueData.value?.copy( listTracks = temp ?: arrayListOf(), ) - _currentSongIndex.value = currentIndex() + _currentSongIndex.value = player.currentMediaItemIndex } fun addMediaItem( @@ -357,7 +371,7 @@ class SimpleMediaServiceHandler( newIndex: Int, ) { player.moveMediaItem(fromIndex, newIndex) - _currentSongIndex.value = currentIndex() + _currentSongIndex.value = player.currentMediaItemIndex } suspend fun swap( @@ -522,7 +536,10 @@ class SimpleMediaServiceHandler( } } _queueData.value?.listTracks?.let { list -> - if (list.size > 3 && list.size - currentIndex() < 3 && list.size - currentIndex() >= 0 && _stateFlow.value == StateSource.STATE_INITIALIZED) { + if (list.size > 3 && list.size - player.currentMediaItemIndex < 3 + && list.size - player.currentMediaItemIndex >= 0 + && _stateFlow.value == StateSource.STATE_INITIALIZED + ) { Log.d("Check loadMore", "loadMore") loadMore() } @@ -569,10 +586,13 @@ class SimpleMediaServiceHandler( updateNotification() } - @OptIn(DelicateCoroutinesApi::class) override fun onIsPlayingChanged(isPlaying: Boolean) { _simpleMediaState.value = SimpleMediaState.Playing(isPlaying = isPlaying) _controlState.value = _controlState.value.copy(isPlaying = isPlaying) + basicWidget.updatePlayingState( + context, + isPlaying, + ) if (isPlaying) { coroutineScope.launch(Dispatchers.Main) { startProgressUpdate() @@ -696,10 +716,6 @@ class SimpleMediaServiceHandler( _queueData.value = queueData } - fun currentIndex(): Int { - return player.currentMediaItemIndex - } - fun mediaListSize(): Int { return player.mediaItemCount } @@ -779,6 +795,64 @@ class SimpleMediaServiceHandler( player.skipSilenceEnabled = skipSilent } + private fun updateWidget(nowPlaying: MediaItem) { + basicWidget.performUpdate( + context, + this, + null, + ) + downloadImageForWidgetJob?.cancel() + downloadImageForWidgetJob = + coroutineScope.launch { + val p = getScreenSize(context) + val widgetImageSize = p.x.coerceAtMost(p.y) + val imageRequest = + ImageRequest.Builder(context) + .data(nowPlaying.mediaMetadata.artworkUri) + .size(widgetImageSize) + .placeholder(R.drawable.holder_video) + .target( + onSuccess = { drawable -> + basicWidget.updateImage( + context, + drawable.toBitmap( + widgetImageSize, + widgetImageSize, + ), + ) + }, + onStart = { holder -> + if (holder != null) { + basicWidget.updateImage( + context, + holder.toBitmap( + widgetImageSize, + widgetImageSize, + ), + ) + } + }, + onError = { + AppCompatResources.getDrawable( + context, + R.drawable.holder_video, + ) + ?.let { it1 -> + basicWidget.updateImage( + context, + it1.toBitmap( + widgetImageSize, + widgetImageSize, + ), + ) + } + }, + ).build() + ImageLoader(context).execute(imageRequest) + } + } + + fun mayBeSaveRecentSong() { runBlocking { if (dataStoreManager.saveRecentSongAndQueue.first() == DataStoreManager.TRUE) { @@ -976,7 +1050,7 @@ class SimpleMediaServiceHandler( listTracks = list, ) } - _currentSongIndex.value = currentIndex() + _currentSongIndex.value = player.currentMediaItemIndex } @UnstableApi @@ -990,7 +1064,7 @@ class SimpleMediaServiceHandler( listTracks = list, ) } - _currentSongIndex.value = currentIndex() + _currentSongIndex.value = player.currentMediaItemIndex } @UnstableApi @@ -1330,7 +1404,7 @@ class SimpleMediaServiceHandler( } fun updateSubtitle(url: String) { - val index = currentIndex() + val index = player.currentMediaItemIndex val mediaItem = player.currentMediaItem Log.w("Subtitle", "updateSubtitle: $url") val subtitle = @@ -1363,12 +1437,12 @@ class SimpleMediaServiceHandler( val isSong = (track.thumbnails?.last()?.height != 0 && track.thumbnails?.last()?.height == track.thumbnails?.last()?.width && track.thumbnails?.last()?.height != null) && (track.thumbnails.lastOrNull()?.url?.contains("hq720") == false && track.thumbnails.lastOrNull()?.url?.contains("maxresdefault") == false) - if ((currentIndex() + 1 in 0..(queueData.first()?.listTracks?.size ?: 0))) { + if ((player.currentMediaItemIndex + 1 in 0..(queueData.first()?.listTracks?.size ?: 0))) { if (track.artists.isNullOrEmpty()) { mainRepository.getSongInfo(track.videoId).cancellable().first().let { songInfo -> if (songInfo != null) { catalogMetadata.add( - currentIndex() + 1, + player.currentMediaItemIndex + 1, track.copy( artists = listOf( @@ -1393,7 +1467,7 @@ class SimpleMediaServiceHandler( .build(), ) .build(), - currentIndex() + 1, + player.currentMediaItemIndex + 1, ) } else { val mediaItem = @@ -1411,9 +1485,9 @@ class SimpleMediaServiceHandler( .build(), ) .build() - addMediaItemNotSet(mediaItem, currentIndex() + 1) + addMediaItemNotSet(mediaItem, player.currentMediaItemIndex + 1) catalogMetadata.add( - currentIndex() + 1, + player.currentMediaItemIndex + 1, track.copy( artists = listOf(Artist("", "Various Artists")), ), @@ -1435,9 +1509,9 @@ class SimpleMediaServiceHandler( .build(), ) .build(), - currentIndex() + 1, + player.currentMediaItemIndex + 1, ) - catalogMetadata.add(currentIndex() + 1, track) + catalogMetadata.add(player.currentMediaItemIndex + 1, track) } Log.d( "MusicSource", @@ -1574,6 +1648,16 @@ data class QueueData( } } +/** + * @param isDone whether the timer is done to make a notification + * @param timeRemaining the time remaining in minutes + */ + +data class SleepTimerState( + val isDone : Boolean, + val timeRemaining: Int, +) + enum class PlaylistType { PLAYLIST, LOCAL_PLAYLIST, diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt b/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt index e233939a..4282d45f 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt @@ -16,6 +16,7 @@ import android.os.Bundle import android.os.IBinder import android.util.Log import android.view.View +import android.view.animation.AnimationUtils import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels @@ -104,7 +105,6 @@ class MainActivity : AppCompatActivity() { if (service.service.simpleMediaServiceHandler.queueData.value == null) { mayBeRestoreLastPlayedTrackAndQueue() } - runCollect() Log.w("TEST", viewModel.simpleMediaServiceHandler?.player.toString()) } } @@ -115,181 +115,6 @@ class MainActivity : AppCompatActivity() { } } - private fun runCollect() { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { -// val job5 = -// launch { -// viewModel.simpleMediaServiceHandler?.nowPlaying?.collect { -// if (it != null) { -// Log.w( -// "Test service", -// viewModel.simpleMediaServiceHandler?.getCurrentMediaItem()?.mediaMetadata?.title.toString(), -// ) -// binding.songTitle.setTextAnimation(it.mediaMetadata.title.toString()) -// binding.songTitle.isSelected = true -// binding.songArtist.setTextAnimation(it.mediaMetadata.artist.toString()) -// binding.songArtist.isSelected = true -// binding.ivArt.load(it.mediaMetadata.artworkUri) { -// crossfade(true) -// crossfade(300) -// placeholder(R.drawable.outline_album_24) -// transformations( -// object : Transformation { -// override val cacheKey: String -// get() = it.mediaMetadata.artworkUri.toString() -// -// override suspend fun transform( -// input: Bitmap, -// size: Size, -// ): Bitmap { -// val p = Palette.from(input).generate() -// val defaultColor = 0x000000 -// var startColor = p.getDarkVibrantColor(defaultColor) -// Log.d("Check Start Color", "transform: $startColor") -// if (startColor == defaultColor) { -// startColor = p.getDarkMutedColor(defaultColor) -// if (startColor == defaultColor) { -// startColor = p.getVibrantColor(defaultColor) -// if (startColor == defaultColor) { -// startColor = -// p.getMutedColor(defaultColor) -// if (startColor == defaultColor) { -// startColor = -// p.getLightVibrantColor( -// defaultColor, -// ) -// if (startColor == defaultColor) { -// startColor = -// p.getLightMutedColor( -// defaultColor, -// ) -// } -// } -// } -// } -// Log.d( -// "Check Start Color", -// "transform: $startColor", -// ) -// } -// val endColor = 0x1b1a1f -// val gd = -// GradientDrawable( -// GradientDrawable.Orientation.TOP_BOTTOM, -// intArrayOf(startColor, endColor), -// ) -// gd.cornerRadius = 0f -// gd.gradientType = GradientDrawable.LINEAR_GRADIENT -// gd.gradientRadius = 0.5f -// gd.alpha = 150 -// val bg = -// ColorUtils.setAlphaComponent(startColor, 255) -// binding.card.setCardBackgroundColor(bg) -// binding.cardBottom.setCardBackgroundColor(bg) -// return input -// } -// }, -// ) -// } -// // val request = ImageRequest.Builder(this@MainActivity) -// // .data(it.mediaMetadata.artworkUri) -// // .target( -// // onSuccess = { result -> -// // binding.ivArt.setImageDrawable(result) -// // }, -// // ) -// // .transformations(object : Transformation { -// // override val cacheKey: String -// // get() = it.mediaMetadata.artworkUri.toString() -// // -// // override suspend fun transform(input: Bitmap, size: Size): Bitmap { -// // val p = Palette.from(input).generate() -// // val defaultColor = 0x000000 -// // var startColor = p.getDarkVibrantColor(defaultColor) -// // Log.d("Check Start Color", "transform: $startColor") -// // if (startColor == defaultColor){ -// // startColor = p.getDarkMutedColor(defaultColor) -// // if (startColor == defaultColor){ -// // startColor = p.getVibrantColor(defaultColor) -// // if (startColor == defaultColor){ -// // startColor = p.getMutedColor(defaultColor) -// // if (startColor == defaultColor){ -// // startColor = p.getLightVibrantColor(defaultColor) -// // if (startColor == defaultColor){ -// // startColor = p.getLightMutedColor(defaultColor) -// // } -// // } -// // } -// // } -// // Log.d("Check Start Color", "transform: $startColor") -// // } -// // val endColor = 0x1b1a1f -// // val gd = GradientDrawable( -// // GradientDrawable.Orientation.TOP_BOTTOM, -// // intArrayOf(startColor, endColor) -// // ) -// // gd.cornerRadius = 0f -// // gd.gradientType = GradientDrawable.LINEAR_GRADIENT -// // gd.gradientRadius = 0.5f -// // gd.alpha = 150 -// // val bg = ColorUtils.setAlphaComponent(startColor, 255) -// // binding.card.setCardBackgroundColor(bg) -// // binding.cardBottom.setCardBackgroundColor(bg) -// // return input -// // } -// // -// // }) -// // .build() -// // ImageLoader(this@MainActivity).execute(request) -// } -// } -// } -// val job2 = -// launch { -// viewModel.progress.collect { -// binding.progressBar.progress = (it * 100).toInt() -// } -// } -// val job3 = -// launch { -// viewModel.isPlaying.collect { -// if (it) { -// binding.btPlayPause.setImageResource(R.drawable.baseline_pause_24) -// } else { -// binding.btPlayPause.setImageResource(R.drawable.baseline_play_arrow_24) -// } -// } -// } - val job4 = - launch { - viewModel.simpleMediaServiceHandler?.sleepDone?.collect { done -> - if (done) { - MaterialAlertDialogBuilder(this@MainActivity) - .setTitle(getString(R.string.sleep_timer_off)) - .setMessage(getString(R.string.good_night)) - .setPositiveButton(getString(R.string.yes)) { d, _ -> - d.dismiss() - } - .show() - } - } - } -// val likedJob = -// launch { -// viewModel.liked.collectLatest { -// Log.w("Check Like", "Collect from main activity $it") -// binding.cbFavorite.isChecked = it -// } -// } -// job2.join() -// job3.join() -// job5.join() - job4.join() -// likedJob.join() - } - } - } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) @@ -321,7 +146,6 @@ class MainActivity : AppCompatActivity() { // } if (viewModel.recreateActivity.value == true) { viewModel.simpleMediaServiceHandler?.coroutineScope = lifecycleScope - runCollect() viewModel.activityRecreateDone() } else { startMusicService() @@ -515,6 +339,7 @@ class MainActivity : AppCompatActivity() { } navController.addOnDestinationChangedListener { nav, destination, _ -> + Log.w("Destination", "onCreate: ${destination.id}") when (destination.id) { R.id.bottom_navigation_item_home, R.id.settingsFragment, R.id.recentlySongsFragment, R.id.moodFragment -> { binding.bottomNavigationView.menu.findItem( @@ -562,8 +387,26 @@ class MainActivity : AppCompatActivity() { } } } - - R.id.nowPlayingFragment, R.id. -> { + } + if ((destination.id == R.id.nowPlayingFragment || destination.id == R.id.fullscreenFragment || + destination.id == R.id.infoFragment || destination.id == R.id.queueFragment) && + binding.miniplayer.visibility != View.GONE && + binding.bottomNavigationView.visibility != View.GONE + ) { + binding.bottomNavigationView.animation = AnimationUtils.loadAnimation(this, R.anim.ttb) + binding.miniplayer.animation = AnimationUtils.loadAnimation(this, R.anim.ttb) + binding.bottomNavigationView.visibility = View.GONE + binding.miniplayer.visibility = View.GONE + } + else if (binding.bottomNavigationView.visibility != View.VISIBLE && + binding.miniplayer.visibility != View.VISIBLE + ) { + lifecycleScope.launch { + delay(500) + binding.bottomNavigationView.animation = AnimationUtils.loadAnimation(this@MainActivity, R.anim.btt) + binding.miniplayer.animation = AnimationUtils.loadAnimation(this@MainActivity, R.anim.btt) + binding.miniplayer.visibility = View.VISIBLE + binding.bottomNavigationView.visibility = View.VISIBLE } } } @@ -741,7 +584,24 @@ class MainActivity : AppCompatActivity() { } } } + val job2 = + launch { + viewModel.sleepTimerState.collect { state -> + if (state.isDone) { + Log.w("MainActivity", "Collect from main activity $state") + viewModel.stopSleepTimer() + MaterialAlertDialogBuilder(this@MainActivity) + .setTitle(getString(R.string.sleep_timer_off)) + .setMessage(getString(R.string.good_night)) + .setPositiveButton(getString(R.string.yes)) { d, _ -> + d.dismiss() + } + .show() + } + } + } job1.join() + job2.join() } lifecycleScope.launch { val miniplayerJob = launch { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/component/LyricsView.kt b/app/src/main/java/com/maxrave/simpmusic/ui/component/LyricsView.kt index 6047b076..325e3c2f 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/component/LyricsView.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/component/LyricsView.kt @@ -2,39 +2,96 @@ package com.maxrave.simpmusic.ui.component import android.util.Log import androidx.compose.animation.Crossfade +import androidx.compose.foundation.MarqueeAnimationMode +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.QueueMusic +import androidx.compose.material.icons.filled.PauseCircle +import androidx.compose.material.icons.filled.PlayCircle +import androidx.compose.material.icons.filled.Repeat +import androidx.compose.material.icons.filled.RepeatOne +import androidx.compose.material.icons.filled.Shuffle +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material.icons.filled.SkipPrevious +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.TextMeasurer -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.rememberTextMeasurer -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.media3.common.util.UnstableApi +import androidx.navigation.NavController +import com.maxrave.simpmusic.R import com.maxrave.simpmusic.extension.animateScrollAndCentralizeItem +import com.maxrave.simpmusic.extension.formatDuration +import com.maxrave.simpmusic.extension.navigateSafe +import com.maxrave.simpmusic.service.RepeatState +import com.maxrave.simpmusic.ui.theme.seed import com.maxrave.simpmusic.ui.theme.typo import com.maxrave.simpmusic.viewModel.NowPlayingScreenData +import com.maxrave.simpmusic.viewModel.SharedViewModel import com.maxrave.simpmusic.viewModel.TimeLine +import com.maxrave.simpmusic.viewModel.UIEvent import kotlinx.coroutines.flow.StateFlow -import kotlin.math.ceil +import kotlinx.coroutines.launch @Composable @@ -98,7 +155,7 @@ fun LyricsView( } } LaunchedEffect(key1 = currentLineIndex, key2 = currentLineHeight) { - if (currentLineIndex > -1 && currentLineHeight > 0) { + if (currentLineIndex > -1 && currentLineHeight > 0 && lyricsData.lyrics.syncType == "LINE_SYNCED") { val boxEnd = listState.layoutInfo.viewportEndOffset val boxStart = listState.layoutInfo.viewportStartOffset val viewPort = boxEnd - boxStart @@ -113,10 +170,12 @@ fun LyricsView( LazyColumn( state = listState, - modifier = Modifier.onGloballyPositioned { coordinates -> - columnHeightDp = with(localDensity) { coordinates.size.height.toDp() } - columnWidthDp = with(localDensity) { coordinates.size.width.toDp() } - }.fillMaxSize() + modifier = Modifier + .onGloballyPositioned { coordinates -> + columnHeightDp = with(localDensity) { coordinates.size.height.toDp() } + columnWidthDp = with(localDensity) { coordinates.size.width.toDp() } + } + .fillMaxSize() ) { items(lyricsData.lyrics.lines?.size ?: 0) { index -> val line = lyricsData.lyrics.lines?.getOrNull(index) @@ -172,16 +231,451 @@ fun LyricsLineItem( } } -fun textHeightCalculate( - text: String, - width: Dp, - style: TextStyle, - localDensity: Density, - textMeasurer: TextMeasurer -): Dp { - val heightDp = with(localDensity) { textMeasurer.measure(text, style).size.height.toDp() } - val widthDp = with(localDensity) { textMeasurer.measure(text, style).size.width.toDp() } - val lineNumber = ceil(widthDp.value.toDouble() / width.value).toInt() - Log.w("LyricsView", "lineNumber: $lineNumber") - return heightDp * lineNumber +@ExperimentalMaterial3Api +@UnstableApi +@Composable +fun FullscreenLyricsSheet( + sharedViewModel: SharedViewModel, + navController: NavController, + color: Color = Color(0xFF242424), + onDismiss: () -> Unit +) { + val screenDataState by sharedViewModel.nowPlayingScreenData.collectAsState() + val timelineState by sharedViewModel.timeline.collectAsState() + val controllerState by sharedViewModel.controllerState.collectAsState() + + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + val coroutineScope = rememberCoroutineScope() + + var sliderValue by rememberSaveable { + mutableFloatStateOf(0f) + } + LaunchedEffect(key1 = timelineState) { + sliderValue = if (timelineState.total > 0L) { + timelineState.current.toFloat() * 100 / timelineState.total.toFloat() + } else { + 0f + } + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + containerColor = color, + contentColor = Color.Transparent, + dragHandle = null, + scrimColor = Color.Black.copy(alpha = .5f), + sheetState = sheetState, + modifier = Modifier.fillMaxHeight(), + windowInsets = WindowInsets(0,0,0,0), + shape = RectangleShape, + ) { + Card( + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight(), + shape = RectangleShape, + colors = CardDefaults.cardColors().copy(containerColor = color), + ) { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors().copy( + containerColor = Color.Transparent + ), + title = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(id = R.string.now_playing_upper), + style = typo.bodyMedium, + color = Color.White + ) + Text( + text = screenDataState.nowPlayingTitle, + style = typo.labelMedium, + color = Color.White, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(align = Alignment.CenterVertically) + .basicMarquee(animationMode = MarqueeAnimationMode.Immediately) + .focusable() + ) + } + }, + navigationIcon = { + IconButton(onClick = { + coroutineScope.launch { sheetState.hide() } + }) { + Icon( + painter = painterResource(id = R.drawable.baseline_keyboard_arrow_down_24), + contentDescription = "", + tint = Color.White + ) + } + }, + actions = { + IconButton(onClick = {}, modifier = Modifier.alpha(0f)) { + Icon( + painter = painterResource(id = R.drawable.baseline_more_vert_24), + contentDescription = "", + tint = Color.White + ) + } + }, + ) + + Spacer(modifier = Modifier.height(20.dp)) + + + + Crossfade( + targetState = screenDataState.lyricsData != null, modifier = Modifier + .weight(1f) + .fillMaxHeight() + .fillMaxWidth() + .padding(horizontal = 50.dp) + ) { + if (it) { + screenDataState.lyricsData?.let { lyrics -> + LyricsView(lyricsData = lyrics, timeLine = sharedViewModel.timeline) { f -> + sharedViewModel.onUIEvent(UIEvent.UpdateProgress(f)) + } + } + } else { + Text( + text = stringResource(id = R.string.unavailable), + style = typo.bodyMedium, + color = Color.White, + modifier = Modifier.fillMaxSize(), + textAlign = TextAlign.Center + ) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + Box { + Column() + { + //Real Slider + Box( + Modifier + .padding( + top = 15.dp + ) + .padding(horizontal = 40.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(24.dp), + contentAlignment = Alignment.Center + ) { + Crossfade(timelineState.loading) { + if (it) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(5.dp) + .padding( + horizontal = 10.dp + ) + .clip( + RoundedCornerShape(8.dp) + ), + ) + } else { + LinearProgressIndicator( + progress = { timelineState.bufferedPercent.toFloat() / 100 }, + modifier = Modifier + .fillMaxWidth() + .height(5.dp) + .padding( + horizontal = 10.dp + ) + .clip( + RoundedCornerShape(8.dp) + ), + color = Color.Gray, + trackColor = Color.DarkGray, + ) + } + } + } + CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { + Slider( + value = sliderValue, + onValueChange = { + sharedViewModel.onUIEvent( + UIEvent.UpdateProgress(it) + ) + }, + valueRange = 0f..100f, + modifier = Modifier + .fillMaxWidth() + .align( + Alignment.TopCenter + ), + colors = SliderDefaults.colors().copy( + thumbColor = Color.White, + activeTrackColor = Color.White, + inactiveTrackColor = Color.Transparent + ), + thumb = { + SliderDefaults.Thumb( + modifier = Modifier + .size(24.dp) + .padding(6.dp), + thumbSize = DpSize(8.dp, 8.dp), + interactionSource = remember { + MutableInteractionSource() + }, + colors = SliderDefaults.colors().copy( + thumbColor = Color.White, + activeTrackColor = Color.White, + inactiveTrackColor = Color.Transparent + ), + enabled = true + ) + }, + ) + } + } + //Time Layout + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 40.dp) + ) { + Text( + text = if (timelineState.current >= 0L) formatDuration(timelineState.current) else stringResource(id = R.string.na_na), + style = typo.bodyMedium, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Left + ) + Text( + text = if (timelineState.total >= 0L) formatDuration(timelineState.total) else stringResource(id = R.string.na_na), + style = typo.bodyMedium, modifier = Modifier.weight(1f), textAlign = TextAlign.Right + ) + } + + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(5.dp) + ) + //Control Button Layout + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .height(96.dp) + .padding(horizontal = 40.dp) + ) { + FilledTonalIconButton( + colors = IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent + ), + modifier = Modifier + .size(48.dp) + .aspectRatio(1f) + .clip( + CircleShape + ), + onClick = { + sharedViewModel.onUIEvent(UIEvent.Shuffle) + } + ) { + Crossfade(targetState = controllerState.isShuffle, label = "Shuffle Button") { isShuffle -> + if (isShuffle) { + Icon( + imageVector = Icons.Filled.Shuffle, tint = Color.White, contentDescription = "", + modifier = Modifier.size(32.dp) + ) + } else { + Icon( + imageVector = Icons.Filled.Shuffle, tint = seed, contentDescription = "", + modifier = Modifier.size(32.dp) + ) + } + } + } + FilledTonalIconButton( + colors = IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent + ), + modifier = Modifier + .size(72.dp) + .aspectRatio(1f) + .clip( + CircleShape + ), + onClick = { + if (controllerState.isPreviousAvailable) { + sharedViewModel.onUIEvent(UIEvent.Previous) + } + } + ) { + Icon( + imageVector = Icons.Filled.SkipPrevious, + tint = if (controllerState.isPreviousAvailable) Color.White else Color.Gray, + contentDescription = "", + modifier = Modifier.size(52.dp), + ) + } + FilledTonalIconButton( + colors = IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent + ), + modifier = Modifier + .size(96.dp) + .aspectRatio(1f) + .clip( + CircleShape + ), + onClick = { + sharedViewModel.onUIEvent(UIEvent.PlayPause) + } + ) { + Crossfade(targetState = controllerState.isPlaying) { isPlaying -> + if (!isPlaying) { + Icon( + imageVector = Icons.Filled.PlayCircle, + tint = Color.White, + contentDescription = "", + modifier = Modifier.size(72.dp) + ) + } else { + Icon( + imageVector = Icons.Filled.PauseCircle, + tint = Color.White, + contentDescription = "", + modifier = Modifier.size(72.dp) + ) + } + } + } + FilledTonalIconButton( + colors = IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent + ), + modifier = Modifier + .size(72.dp) + .aspectRatio(1f) + .clip( + CircleShape + ), + onClick = { + if (controllerState.isNextAvailable) { + sharedViewModel.onUIEvent(UIEvent.Next) + } + } + ) { + Icon( + imageVector = Icons.Filled.SkipNext, + tint = if (controllerState.isNextAvailable) Color.White else Color.Gray, + contentDescription = "", + modifier = Modifier.size(52.dp) + ) + } + FilledTonalIconButton( + colors = IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent + ), + modifier = Modifier + .size(48.dp) + .aspectRatio(1f) + .clip( + CircleShape + ), + onClick = { + sharedViewModel.onUIEvent(UIEvent.Repeat) + } + ) { + Crossfade(targetState = controllerState.repeatState) { rs -> + when (rs) { + is RepeatState.None -> { + Icon( + imageVector = Icons.Filled.Repeat, tint = Color.White, contentDescription = "", + modifier = Modifier.size(32.dp) + ) + } + + RepeatState.All -> { + Icon( + imageVector = Icons.Filled.Repeat, tint = seed, contentDescription = "", + modifier = Modifier.size(32.dp) + ) + } + + RepeatState.One -> { + Icon( + imageVector = Icons.Filled.RepeatOne, tint = seed, contentDescription = "", + modifier = Modifier.size(32.dp) + ) + } + } + } + } + } + //List Bottom Buttons + //24.dp + Box( + modifier = Modifier + .height(32.dp) + .fillMaxWidth() + .padding(horizontal = 40.dp), + ) { + IconButton( + modifier = Modifier + .size(24.dp) + .aspectRatio(1f) + .align(Alignment.CenterStart) + .clip( + CircleShape + ), + onClick = { + navController.navigateSafe( + R.id.action_global_infoFragment + ) + } + ) { + Icon(imageVector = Icons.Outlined.Info, tint = Color.White, contentDescription = "") + } + Row( + Modifier.align(Alignment.CenterEnd) + ) { + Spacer(modifier = Modifier.size(8.dp)) + IconButton( + modifier = Modifier + .size(24.dp) + .aspectRatio(1f) + .clip( + CircleShape + ), + onClick = { + navController.navigateSafe( + R.id.action_global_queueFragment + ) + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.QueueMusic, + tint = Color.White, + contentDescription = "" + ) + } + } + } + } + } + Spacer(modifier = Modifier.height(20.dp)) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/component/MediaPlayerView.kt b/app/src/main/java/com/maxrave/simpmusic/ui/component/MediaPlayerView.kt index f7792058..613e4994 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/component/MediaPlayerView.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/component/MediaPlayerView.kt @@ -2,7 +2,6 @@ package com.maxrave.simpmusic.ui.component import android.util.Log import android.view.TextureView -import androidx.compose.foundation.border import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.runtime.Composable @@ -10,9 +9,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.C.VIDEO_SCALING_MODE_SCALE_TO_FIT import androidx.media3.common.MediaItem @@ -59,10 +56,7 @@ fun MediaPlayerView( exoPlayer.setVideoTextureView(it) } }, - modifier = modifier.wrapContentHeight().fillMaxWidth().border( - width = 1.dp, - color = Color.Red - ) + modifier = modifier.wrapContentHeight().fillMaxWidth() ) } diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt b/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt index 5bfe26e5..5e767287 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.MarqueeAnimationMode import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -34,20 +33,27 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -60,14 +66,15 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.text.isDigitsOnly import androidx.media3.common.util.UnstableApi import androidx.navigation.NavController -import androidx.wear.compose.material3.TextButton -import androidx.wear.compose.material3.ripple import com.maxrave.simpmusic.R import com.maxrave.simpmusic.common.DownloadState +import com.maxrave.simpmusic.data.dataStore.DataStoreManager import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity import com.maxrave.simpmusic.data.db.entities.SongEntity import com.maxrave.simpmusic.data.model.searchResult.songs.Artist @@ -75,6 +82,7 @@ import com.maxrave.simpmusic.extension.connectArtists import com.maxrave.simpmusic.extension.greyScale import com.maxrave.simpmusic.extension.navigateSafe import com.maxrave.simpmusic.extension.toTrack +import com.maxrave.simpmusic.ui.theme.seed import com.maxrave.simpmusic.ui.theme.typo import com.maxrave.simpmusic.viewModel.SharedViewModel import com.skydoves.landscapist.ImageOptions @@ -96,7 +104,7 @@ fun NowPlayingBottomSheet( onDelete: (() -> Unit)? = null, onDownload: (() -> Unit)? = null, onMainLyricsProvider: ((String) -> Unit)? = null, - onSleepTimer: (() -> Unit)? = null, + onSleepTimer: ((Int) -> Unit)? = null, getLocalPlaylist: () -> Unit, listLocalPlaylist: State?>, onAddToLocalPlaylist: (LocalPlaylistEntity) -> Unit = { _ -> }, @@ -113,6 +121,16 @@ fun NowPlayingBottomSheet( var addToAPlaylist by remember { mutableStateOf(false) } var artist by remember { mutableStateOf(false) } + var mainLyricsProvider by remember { + mutableStateOf(false) + } + var sleepTimer by remember { + mutableStateOf(false) + } + var sleepTimerWarning by remember { + mutableStateOf(false) + } + if (addToAPlaylist && listLocalPlaylist.value != null) { getLocalPlaylist() AddToPlaylistModalBottomSheet( @@ -138,6 +156,113 @@ fun NowPlayingBottomSheet( ) } + if (sleepTimer) { + SleepTimerBottomSheet(onDismiss = { sleepTimer = false }) { minutes: Int -> + if (onSleepTimer != null) { + onSleepTimer(minutes) + } + } + } + + if (sleepTimerWarning) { + AlertDialog( + containerColor = Color(0xFF242424), + onDismissRequest = { sleepTimerWarning = false }, + confirmButton = { + TextButton(onClick = { + sleepTimerWarning = false + sharedViewModel.stopSleepTimer() + Toast.makeText(context, context.getString(R.string.sleep_timer_off_done), Toast.LENGTH_SHORT).show() + }) { + Text(text = stringResource(id = R.string.yes), style = typo.labelSmall) + } + }, + dismissButton = { + TextButton(onClick = { sleepTimerWarning = false }) { + Text(text = stringResource(id = R.string.cancel), style = typo.labelSmall) + } + }, + title = { + Text(text = stringResource(id = R.string.warning), style = typo.labelSmall) + }, + text = { + Text(text = stringResource(id = R.string.sleep_timer_warning), style = typo.bodyMedium) + }, + ) + } + + if (mainLyricsProvider) { + var selected by remember { + mutableIntStateOf( + if (sharedViewModel.getLyricsProvider() == DataStoreManager.MUSIXMATCH) 0 else 1 + ) + } + + AlertDialog( + onDismissRequest = { mainLyricsProvider = false }, + containerColor = Color(0xFF242424), + title = { + Text( + text = stringResource(id = R.string.main_lyrics_provider), + style = typo.titleMedium, + ) + }, + text = { + Column { + Row( + modifier = Modifier + .padding(horizontal = 4.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selected == 0, + onClick = { + selected = 0 + } + ) + Spacer(modifier = Modifier.size(10.dp)) + Text(text = stringResource(id = R.string.musixmatch), style = typo.labelSmall) + } + Row( + modifier = Modifier + .padding(horizontal = 4.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selected == 1, + onClick = { + selected = 1 + } + ) + Spacer(modifier = Modifier.size(10.dp)) + Text(text = stringResource(id = R.string.youtube_transcript), style = typo.labelSmall) + } + } + }, + confirmButton = { + TextButton( + onClick = { + sharedViewModel.setLyricsProvider( + if (selected == 0) DataStoreManager.MUSIXMATCH else DataStoreManager.YOUTUBE + ) + mainLyricsProvider = false + }, + ) { + Text(text = stringResource(id = R.string.yes), style = typo.labelSmall) + } + }, + dismissButton = { + TextButton(onClick = { + mainLyricsProvider = false + }) { + Text(text = stringResource(id = R.string.cancel), style = typo.labelSmall) + } + } + ) + } + if (isBottomSheetVisible && songEntity.value != null) { ModalBottomSheet( onDismissRequest = onDismiss, @@ -149,9 +274,9 @@ fun NowPlayingBottomSheet( ) { Card( modifier = - Modifier - .fillMaxWidth() - .wrapContentHeight(), + Modifier + .fillMaxWidth() + .wrapContentHeight(), shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), colors = CardDefaults.cardColors().copy(containerColor = Color(0xFF242424)), ) { @@ -161,9 +286,9 @@ fun NowPlayingBottomSheet( Spacer(modifier = Modifier.height(5.dp)) Card( modifier = - Modifier - .width(60.dp) - .height(4.dp), + Modifier + .width(60.dp) + .height(4.dp), colors = CardDefaults.cardColors().copy( containerColor = Color(0xFF474545), @@ -173,10 +298,10 @@ fun NowPlayingBottomSheet( Spacer(modifier = Modifier.height(5.dp)) Row( modifier = - Modifier - .fillMaxWidth() - .height(65.dp) - .padding(10.dp), + Modifier + .fillMaxWidth() + .height(65.dp) + .padding(10.dp), verticalAlignment = Alignment.CenterVertically, ) { CoilImage( @@ -194,9 +319,9 @@ fun NowPlayingBottomSheet( ) }, modifier = - Modifier - .align(Alignment.CenterVertically) - .size(60.dp), + Modifier + .align(Alignment.CenterVertically) + .size(60.dp), ) Spacer(modifier = Modifier.width(20.dp)) Column( @@ -207,33 +332,33 @@ fun NowPlayingBottomSheet( style = typo.labelMedium, maxLines = 1, modifier = - Modifier - .wrapContentHeight(Alignment.CenterVertically) - .basicMarquee( - animationMode = MarqueeAnimationMode.Immediately, - ) - .focusable(), + Modifier + .wrapContentHeight(Alignment.CenterVertically) + .basicMarquee( + animationMode = MarqueeAnimationMode.Immediately, + ) + .focusable(), ) Text( text = songEntity.value?.artistName?.connectArtists() ?: "", style = typo.bodyMedium, maxLines = 1, modifier = - Modifier - .wrapContentHeight(Alignment.CenterVertically) - .basicMarquee( - animationMode = MarqueeAnimationMode.Immediately, - ) - .focusable(), + Modifier + .wrapContentHeight(Alignment.CenterVertically) + .basicMarquee( + animationMode = MarqueeAnimationMode.Immediately, + ) + .focusable(), ) } } Spacer(modifier = Modifier.height(5.dp)) HorizontalDivider( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), thickness = 1.dp, ) Spacer(modifier = Modifier.height(2.dp)) @@ -358,17 +483,33 @@ fun NowPlayingBottomSheet( icon = painterResource(id = R.drawable.baseline_lyrics_24), text = R.string.main_lyrics_provider, ) { - onMainLyricsProvider("") + mainLyricsProvider = true } } } Crossfade(targetState = onSleepTimer != null) { + val sleepTimerState by sharedViewModel.sleepTimerState.collectAsState() if (it && onSleepTimer != null) { - ActionButton( - icon = painterResource(id = R.drawable.baseline_access_alarm_24), - text = R.string.sleep_timer_off, - ) { - onSleepTimer() + Crossfade(targetState = sleepTimerState.timeRemaining > 0) { running -> + if (running) { + ActionButton( + icon = painterResource(id = R.drawable.baseline_access_alarm_24), + textString = stringResource(id = R.string.sleep_timer, sleepTimerState.timeRemaining.toString()), + text = null, + textColor = seed, + iconColor = seed, + ) { + sleepTimerWarning = true + } + } + else { + ActionButton( + icon = painterResource(id = R.drawable.baseline_access_alarm_24), + text = R.string.sleep_timer_off, + ) { + sleepTimer = true + } + } } } } @@ -395,25 +536,23 @@ fun ActionButton( icon: Painter, @StringRes text: Int?, textString: String? = null, + textColor: Color? = null, + iconColor: Color = Color.White, enable: Boolean = true, onClick: () -> Unit, ) { Box( modifier = - Modifier - .fillMaxWidth() - .wrapContentHeight(Alignment.CenterVertically) - .clickable( - interactionSource = - remember { - MutableInteractionSource() - }, - onClick = if (enable) onClick else ({}), - indication = ripple(), - ) - .apply { - if (!enable) greyScale() - }, + Modifier + .fillMaxWidth() + .wrapContentHeight(Alignment.CenterVertically) + .clickable { + if (enable) onClick() else { + } + } + .apply { + if (!enable) greyScale() + }, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -423,14 +562,14 @@ fun ActionButton( painter = icon, contentDescription = if (text != null) stringResource(text) else textString ?: "", modifier = - Modifier - .wrapContentSize( - Alignment.Center, - ) - .padding(12.dp), + Modifier + .wrapContentSize( + Alignment.Center, + ) + .padding(12.dp), colorFilter = if (enable) { - null + ColorFilter.tint(iconColor) } else { ColorFilter.colorMatrix( ColorMatrix().apply { @@ -444,11 +583,12 @@ fun ActionButton( Text( text = if (text != null) stringResource(text) else textString ?: "", - style = typo.bodyLarge, + style = typo.labelSmall, + color = textColor ?: Color.Unspecified, modifier = - Modifier - .padding(start = 10.dp) - .wrapContentHeight(Alignment.CenterVertically), + Modifier + .padding(start = 10.dp) + .wrapContentHeight(Alignment.CenterVertically), ) } } @@ -462,26 +602,19 @@ fun CheckBoxActionButton( var stateChecked by remember { mutableStateOf(defaultChecked) } Box( modifier = - Modifier - .wrapContentSize(align = Alignment.Center) - .clickable( - interactionSource = - remember { - MutableInteractionSource() - }, - onClick = { - stateChecked = !stateChecked - onChangeListener(stateChecked) - }, - indication = ripple(), - ), + Modifier + .wrapContentSize(align = Alignment.Center) + .clickable { + stateChecked = !stateChecked + onChangeListener(stateChecked) + }, ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = - Modifier - .padding(horizontal = 20.dp) - .fillMaxWidth(), + Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth(), ) { Box(Modifier.padding(10.dp)) { HeartCheckBox(checked = stateChecked, size = 30) @@ -495,11 +628,11 @@ fun CheckBoxActionButton( } else { stringResource(R.string.like) }, - style = typo.bodyLarge, + style = typo.labelSmall, modifier = - Modifier - .padding(start = 10.dp) - .wrapContentHeight(Alignment.CenterVertically), + Modifier + .padding(start = 10.dp) + .wrapContentHeight(Alignment.CenterVertically), ) } } @@ -513,16 +646,14 @@ fun HeartCheckBox( ) { Box( modifier = - Modifier - .size(size.dp) - .clip( - CircleShape, - ) - .clickable( - onClick = onStateChange ?: {}, - indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, - ), + Modifier + .size(size.dp) + .clip( + CircleShape, + ) + .clickable { + onStateChange ?: {} + }, ) { Crossfade(targetState = checked, modifier = Modifier.fillMaxSize()) { if (it) { @@ -530,18 +661,18 @@ fun HeartCheckBox( painter = painterResource(id = R.drawable.baseline_favorite_24), contentDescription = "Favorite checked", modifier = - Modifier - .fillMaxSize() - .padding(4.dp), + Modifier + .fillMaxSize() + .padding(4.dp), ) } else { Image( painter = painterResource(id = R.drawable.baseline_favorite_border_24), contentDescription = "Favorite unchecked", modifier = - Modifier - .fillMaxSize() - .padding(4.dp), + Modifier + .fillMaxSize() + .padding(4.dp), colorFilter = ColorFilter.tint(Color.White), ) } @@ -576,9 +707,9 @@ fun AddToPlaylistModalBottomSheet( ) { Card( modifier = - Modifier - .fillMaxWidth() - .wrapContentHeight(), + Modifier + .fillMaxWidth() + .wrapContentHeight(), shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), colors = CardDefaults.cardColors().copy(containerColor = Color(0xFF242424)), ) { @@ -588,9 +719,9 @@ fun AddToPlaylistModalBottomSheet( Spacer(modifier = Modifier.height(5.dp)) Card( modifier = - Modifier - .width(60.dp) - .height(4.dp), + Modifier + .width(60.dp) + .height(4.dp), colors = CardDefaults.cardColors().copy( containerColor = Color(0xFF474545), @@ -602,24 +733,20 @@ fun AddToPlaylistModalBottomSheet( items(listLocalPlaylist) { playlist -> Box( modifier = - Modifier - .fillMaxWidth() - .padding(20.dp) - .clickable( - onClick = { - onClick(playlist) - hideModalBottomSheet() - }, - indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, - ), + Modifier + .fillMaxWidth() + .padding(vertical = 3.dp) + .clickable { + onClick(playlist) + hideModalBottomSheet() + }, ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = - Modifier - .padding(12.dp) - .align(Alignment.CenterStart), + Modifier + .padding(12.dp) + .align(Alignment.CenterStart), ) { Image( painter = @@ -631,7 +758,7 @@ fun AddToPlaylistModalBottomSheet( Spacer(modifier = Modifier.width(10.dp)) Text( text = playlist.title, - style = typo.bodyLarge, + style = typo.labelSmall, ) } Crossfade( @@ -653,6 +780,81 @@ fun AddToPlaylistModalBottomSheet( } } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SleepTimerBottomSheet( + onDismiss: () -> Unit, + onSetTimer: (minutes: Int) -> Unit, +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val modelBottomSheetState = + rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + + var minutes by rememberSaveable { mutableIntStateOf(0) } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = modelBottomSheetState, + containerColor = Color.Transparent, + contentColor = Color.Transparent, + dragHandle = null, + scrimColor = Color.Black.copy(alpha = .5f), + ) { + Card( + modifier = + Modifier + .fillMaxWidth() + .wrapContentHeight(), + shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), + colors = CardDefaults.cardColors().copy(containerColor = Color(0xFF242424)), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(5.dp)) + Card( + modifier = + Modifier + .width(60.dp) + .height(4.dp), + colors = + CardDefaults.cardColors().copy( + containerColor = Color(0xFF474545), + ), + shape = RoundedCornerShape(50), + ) {} + Spacer(modifier = Modifier.height(10.dp)) + Text(text = stringResource(id = R.string.sleep_minutes), style = typo.labelSmall) + Spacer(modifier = Modifier.height(5.dp)) + OutlinedTextField( + value = minutes.toString(), + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + onValueChange = { if (it.isDigitsOnly() && it.isNotEmpty() && it.isNotBlank()) minutes = it.toInt() }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword) + ) + Spacer(modifier = Modifier.height(5.dp)) + TextButton(onClick = { + if (minutes > 0) { + onSetTimer(minutes) + coroutineScope.launch { modelBottomSheetState.hide() } + } + else { + Toast.makeText(context, context.getString(R.string.sleep_timer_set_error), Toast.LENGTH_SHORT).show() + } + }, modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + Text(text = stringResource(R.string.set), style = typo.labelSmall) + } + Spacer(modifier = Modifier.height(5.dp)) + } + } + } +} @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -680,9 +882,9 @@ fun ArtistModalBottomSheet( ) { Card( modifier = - Modifier - .fillMaxWidth() - .wrapContentHeight(), + Modifier + .fillMaxWidth() + .wrapContentHeight(), shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), colors = CardDefaults.cardColors().copy(containerColor = Color(0xFF242424)), ) { @@ -692,9 +894,9 @@ fun ArtistModalBottomSheet( Spacer(modifier = Modifier.height(5.dp)) Card( modifier = - Modifier - .width(60.dp) - .height(4.dp), + Modifier + .width(60.dp) + .height(4.dp), colors = CardDefaults.cardColors().copy( containerColor = Color(0xFF474545), @@ -706,30 +908,26 @@ fun ArtistModalBottomSheet( items(artists) { artist -> Box( modifier = - Modifier - .fillMaxWidth() - .clickable( - onClick = { - if (!artist.id.isNullOrBlank()) { - navController.navigateSafe( - R.id.action_global_artistFragment, - Bundle().apply { - putString("channelId", artist.id) - }, - ) - } - hideModalBottomSheet() - }, - indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, - ), + Modifier + .fillMaxWidth() + .clickable { + if (!artist.id.isNullOrBlank()) { + navController.navigateSafe( + R.id.action_global_artistFragment, + Bundle().apply { + putString("channelId", artist.id) + }, + ) + } + hideModalBottomSheet() + } ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = - Modifier - .padding(20.dp) - .align(Alignment.CenterStart), + Modifier + .padding(20.dp) + .align(Alignment.CenterStart), ) { Image( painter = @@ -741,7 +939,7 @@ fun ArtistModalBottomSheet( Spacer(modifier = Modifier.width(10.dp)) Text( text = artist.name, - style = typo.bodyLarge, + style = typo.labelSmall, ) } } @@ -819,9 +1017,9 @@ fun LocalPlaylistBottomSheet( ) { Card( modifier = - Modifier - .fillMaxWidth() - .wrapContentHeight(), + Modifier + .fillMaxWidth() + .wrapContentHeight(), shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), colors = CardDefaults.cardColors().copy(containerColor = Color(0xFF242424)), ) { @@ -831,9 +1029,9 @@ fun LocalPlaylistBottomSheet( Spacer(modifier = Modifier.height(5.dp)) Card( modifier = - Modifier - .width(60.dp) - .height(4.dp), + Modifier + .width(60.dp) + .height(4.dp), colors = CardDefaults.cardColors().copy( containerColor = Color(0xFF474545), @@ -847,7 +1045,9 @@ fun LocalPlaylistBottomSheet( label = { Text(text = stringResource(id = R.string.title)) }, - modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), ) Spacer(modifier = Modifier.height(5.dp)) TextButton( @@ -861,9 +1061,9 @@ fun LocalPlaylistBottomSheet( } }, modifier = - Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally), + Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally), ) { Text(text = stringResource(id = R.string.save)) } @@ -882,9 +1082,9 @@ fun LocalPlaylistBottomSheet( ) { Card( modifier = - Modifier - .fillMaxWidth() - .wrapContentHeight(), + Modifier + .fillMaxWidth() + .wrapContentHeight(), shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), colors = CardDefaults.cardColors().copy(containerColor = Color(0xFF242424)), ) { @@ -894,9 +1094,9 @@ fun LocalPlaylistBottomSheet( Spacer(modifier = Modifier.height(5.dp)) Card( modifier = - Modifier - .width(60.dp) - .height(4.dp), + Modifier + .width(60.dp) + .height(4.dp), colors = CardDefaults.cardColors().copy( containerColor = Color(0xFF474545), diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/FullscreenFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/FullscreenFragment.kt index 02bba8d8..adc07d22 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/FullscreenFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/FullscreenFragment.kt @@ -9,7 +9,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.compose.ui.platform.ComposeView import androidx.core.net.toUri import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -26,7 +25,6 @@ import androidx.media3.exoplayer.offline.DownloadService import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import coil.load -import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -34,7 +32,6 @@ import com.google.android.material.slider.Slider import com.maxrave.simpmusic.R import com.maxrave.simpmusic.adapter.artist.SeeArtistOfNowPlayingAdapter import com.maxrave.simpmusic.adapter.playlist.AddToAPlaylistAdapter -import com.maxrave.simpmusic.common.Config import com.maxrave.simpmusic.common.DownloadState import com.maxrave.simpmusic.common.LYRICS_PROVIDER import com.maxrave.simpmusic.data.dataStore.DataStoreManager @@ -46,6 +43,7 @@ import com.maxrave.simpmusic.databinding.BottomSheetNowPlayingBinding import com.maxrave.simpmusic.databinding.BottomSheetSeeArtistOfNowPlayingBinding import com.maxrave.simpmusic.databinding.BottomSheetSleepTimerBinding import com.maxrave.simpmusic.extension.connectArtists +import com.maxrave.simpmusic.extension.formatDuration import com.maxrave.simpmusic.extension.navigateSafe import com.maxrave.simpmusic.extension.removeConflicts import com.maxrave.simpmusic.extension.setEnabledAll @@ -59,6 +57,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.time.LocalDateTime @@ -84,11 +83,6 @@ class FullscreenFragment : Fragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - val activity = requireActivity() - val bottom = activity.findViewById(R.id.bottom_navigation_view) - val miniplayer = activity.findViewById(R.id.miniplayer) - bottom.visibility = View.GONE - miniplayer.visibility = View.GONE if (!viewModel.isFullScreen) { hideSystemUI() viewModel.isFullScreen = true @@ -183,50 +177,77 @@ class FullscreenFragment : Fragment() { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { val time = launch { - viewModel.progress.collect { - if (it in 0.0..1.0) { - binding.progressSong.value = it * 100 + viewModel.timeline.collect { + if (it.total >= 0L) { + val progress = it.current.toFloat()/it.total + binding.progressSong.value = progress * 100 + binding.tvCurrentTime.text = formatDuration(it.current) + binding.tvFullTime.text = formatDuration(it.total) + if (viewModel._lyrics.value?.data != null && viewModel.isSubtitle) { +// val temp = viewModel.getLyricsString(it) + val lyricsData = viewModel.nowPlayingScreenData.value.lyricsData + val lyrics = lyricsData?.lyrics + val translated = lyricsData?.translatedLyrics + val index = viewModel.getActiveLyrics(it.current) + if (index != null) { + if (lyrics?.lines?.get(0)?.words == "Lyrics not found") { + binding.subtitleView.visibility = View.GONE + } else { + lyrics.let { it1 -> + if (lyrics?.syncType == "LINE_SYNCED") { + if (index == -1) { + binding.subtitleView.visibility = View.GONE + } else { + binding.subtitleView.visibility = View.VISIBLE + binding.tvMainSubtitle.text = + it1?.lines?.get(index)?.words + if (translated != null) { + val line = + translated.lines?.find { line -> + line.startTimeMs == it1?.lines?.get(index)?.startTimeMs + } + if (line != null) { + binding.tvTranslatedSubtitle.visibility = + View.VISIBLE + binding.tvTranslatedSubtitle.text = + line.words + } else { + binding.tvTranslatedSubtitle.visibility = + View.GONE + } + } + } + } else if (lyrics?.syncType == "UNSYNCED") { + binding.subtitleView.visibility = View.GONE + } + } + } + } else { + binding.subtitleView.visibility = View.GONE + } + } else { + binding.subtitleView.visibility = View.GONE + } } - } - } - val timeString = - launch { - viewModel.progressString.collect { prog -> - binding.tvCurrentTime.text = prog - } - } - val duration = - launch { - viewModel.duration.collect { dur -> - if (dur < 0) { + else { + binding.progressSong.value = 0f + binding.tvCurrentTime.text = getString(R.string.na_na) binding.tvFullTime.text = getString(R.string.na_na) - } else { - binding.tvFullTime.text = viewModel.formatDuration(dur) - } - } - } - val isPlaying = - launch { - viewModel.isPlaying.collect { isPlaying -> - if (isPlaying) { - binding.btPlayPause.setImageResource(R.drawable.baseline_pause_circle_24) - } else { - binding.btPlayPause.setImageResource(R.drawable.baseline_play_circle_24) } } } val title = launch { - viewModel.simpleMediaServiceHandler?.nowPlaying?.collectLatest { - if (it != null) { - binding.toolbar.title = it.mediaMetadata.title.toString() - } + viewModel.nowPlayingScreenData.collectLatest { + binding.toolbar.title = it.nowPlayingTitle } } - val shuffle = + val job11 = launch { - viewModel.shuffleModeEnabled.collect { shuffle -> - when (shuffle) { + viewModel.simpleMediaServiceHandler?.controlState?.collect { controlState -> + setEnabledAll(binding.btPrevious, controlState.isPreviousAvailable) + setEnabledAll(binding.btNext, controlState.isNextAvailable) + when (controlState.isShuffle) { true -> { binding.btShuffle.setImageResource(R.drawable.baseline_shuffle_24_enable) } @@ -235,12 +256,7 @@ class FullscreenFragment : Fragment() { binding.btShuffle.setImageResource(R.drawable.baseline_shuffle_24) } } - } - } - val repeat = - launch { - viewModel.repeatMode.collect { repeatMode -> - when (repeatMode) { + when (controlState.repeatState) { RepeatState.None -> { binding.btRepeat.setImageResource(R.drawable.baseline_repeat_24) } @@ -253,73 +269,23 @@ class FullscreenFragment : Fragment() { binding.btRepeat.setImageResource(R.drawable.baseline_repeat_24_enable) } } - } - } - val job11 = - launch { - viewModel.simpleMediaServiceHandler?.controlState?.collect { controlState -> - setEnabledAll(binding.btPrevious, controlState.isPreviousAvailable) - setEnabledAll(binding.btNext, controlState.isNextAvailable) + if (controlState.isPlaying) { + binding.btPlayPause.setImageResource(R.drawable.baseline_pause_circle_24) + } else { + binding.btPlayPause.setImageResource(R.drawable.baseline_play_circle_24) + } } } val job5 = launch { viewModel.progressMillis.collect { - if (viewModel._lyrics.value?.data != null && viewModel.isSubtitle) { -// val temp = viewModel.getLyricsString(it) - val lyrics = viewModel._lyrics.value!!.data - val translated = viewModel.translateLyrics.value - val index = viewModel.getActiveLyrics(it) - if (index != null) { - if (lyrics?.lines?.get(0)?.words == "Lyrics not found") { - binding.subtitleView.visibility = View.GONE - } else { - lyrics.let { it1 -> - if (viewModel.getLyricsSyncState() == Config.SyncState.LINE_SYNCED) { - if (index == -1) { - binding.subtitleView.visibility = View.GONE - } else { - binding.subtitleView.visibility = View.VISIBLE - binding.tvMainSubtitle.text = - it1?.lines?.get(index)?.words - if (translated != null) { - val line = - translated.lines?.find { line -> - line.startTimeMs == it1?.lines?.get(index)?.startTimeMs - } - if (line != null) { - binding.tvTranslatedSubtitle.visibility = - View.VISIBLE - binding.tvTranslatedSubtitle.text = - line.words - } else { - binding.tvTranslatedSubtitle.visibility = - View.GONE - } - } - } - } else if (viewModel.getLyricsSyncState() == Config.SyncState.UNSYNCED) { - binding.subtitleView.visibility = View.GONE - } - } - } - } else { - binding.subtitleView.visibility = View.GONE - } - } else { - binding.subtitleView.visibility = View.GONE - } + } } job5.join() time.join() - timeString.join() - duration.join() - isPlaying.join() title.join() - repeat.join() job11.join() - shuffle.join() } } binding.toolbar.setOnMenuItemClickListener { menuItem -> @@ -334,7 +300,7 @@ class FullscreenFragment : Fragment() { val bottomSheetView = BottomSheetNowPlayingBinding.inflate(layoutInflater) with(bottomSheetView) { lifecycleScope.launch { - viewModel.simpleMediaServiceHandler?.sleepMinutes?.collect { min -> + viewModel.sleepTimerState.map { it.timeRemaining }.collect { min -> if (min > 0) { tvSleepTimer.text = getString(R.string.sleep_timer, min.toString()) @@ -438,7 +404,7 @@ class FullscreenFragment : Fragment() { } btSleepTimer.setOnClickListener { Log.w("Sleep Timer", "onClick") - if (viewModel.sleepTimerRunning.value == true) { + if (viewModel.sleepTimerState.value.timeRemaining > 0) { MaterialAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.warning)) .setMessage(getString(R.string.sleep_timer_warning)) diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt index 6d152240..77a91e84 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt @@ -1,7 +1,6 @@ package com.maxrave.simpmusic.ui.fragment.player import android.graphics.Color -import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -18,14 +17,8 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.media3.common.util.UnstableApi import androidx.navigation.findNavController -import com.google.android.material.bottomnavigation.BottomNavigationView -import com.maxrave.simpmusic.R -import com.maxrave.simpmusic.adapter.lyrics.LyricsAdapter -import com.maxrave.simpmusic.data.model.metadata.MetadataSong -import com.maxrave.simpmusic.databinding.FragmentNowPlayingBinding import com.maxrave.simpmusic.ui.screen.player.NowPlayingScreen import com.maxrave.simpmusic.ui.theme.AppTheme -import com.maxrave.simpmusic.utils.DisableTouchEventRecyclerView import com.maxrave.simpmusic.viewModel.SharedViewModel import dagger.hilt.android.AndroidEntryPoint @@ -34,24 +27,6 @@ import dagger.hilt.android.AndroidEntryPoint class NowPlayingFragment : Fragment() { val viewModel by activityViewModels() private lateinit var composeView: ComposeView - - private var _binding: FragmentNowPlayingBinding? = null - private val binding get() = _binding!! - private var metadataCurSong: MetadataSong? = null - - private var videoId: String? = null - private var from: String? = null - private var type: String? = null - private var index: Int? = null - private var downloaded: Int? = null - private var playlistId: String? = null - - private var gradientDrawable: GradientDrawable? = null - private var lyricsBackground: Int? = null - - private lateinit var lyricsAdapter: LyricsAdapter - private lateinit var lyricsFullAdapter: LyricsAdapter - private lateinit var disableScrolling: DisableTouchEventRecyclerView // private var overlayJob: Job? = null // // private var canvasOverlayJob: Job? = null @@ -183,6 +158,7 @@ class NowPlayingFragment : Fragment() { // // bottom.visibility = View.GONE // miniplayer.visibility = View.GONE + composeView.apply { setContent { AppTheme { @@ -2361,43 +2337,6 @@ class NowPlayingFragment : Fragment() { // canvasOverlayJob?.cancel() } - fun parseTimestampToMilliseconds(timestamp: String): Double { - val parts = timestamp.split(":") - val totalSeconds = - when (parts.size) { - 2 -> { - try { - val minutes = parts[0].toDouble() - val seconds = parts[1].toDouble() - (minutes * 60 + seconds) - } catch (e: NumberFormatException) { - // Handle parsing error - e.printStackTrace() - return 0.0 - } - } - - 3 -> { - try { - val hours = parts[0].toDouble() - val minutes = parts[1].toDouble() - val seconds = parts[2].toDouble() - (hours * 3600 + minutes * 60 + seconds) - } catch (e: NumberFormatException) { - // Handle parsing error - e.printStackTrace() - return 0.0 - } - } - - else -> { - // Handle incorrect format - return 0.0 - } - } - return totalSeconds * 1000 - } - private inline fun View.getDimensions( crossinline onDimensionsReady: ( w: Int, diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt index af2ca34f..dc6888f1 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt @@ -137,17 +137,21 @@ class QueueFragment: BottomSheetDialogFragment() { binding.tvSongTitle.isSelected = true binding.tvSongArtist.text = it.mediaItem.mediaMetadata.artist binding.tvSongArtist.isSelected = true + if (viewModel.simpleMediaServiceHandler?.stateFlow?.first() == StateSource.STATE_INITIALIZED || + viewModel.simpleMediaServiceHandler?.stateFlow?.first() == StateSource.STATE_INITIALIZING){ + val index = it.songEntity?.videoId?.let { it1 -> queueAdapter.getIndexOf(it1) } + if (index != null) { + binding.rvQueue.smoothScrollToPosition(index) + queueAdapter.setCurrentPlaying(index) + } + } } } } val job3 = launch { viewModel.simpleMediaServiceHandler?.currentSongIndex?.collect{ index -> Log.d("QueueFragment", "onViewCreated: $index") - if (viewModel.simpleMediaServiceHandler?.stateFlow?.first() == StateSource.STATE_INITIALIZED || - viewModel.simpleMediaServiceHandler?.stateFlow?.first() == StateSource.STATE_INITIALIZING){ - binding.rvQueue.smoothScrollToPosition(index) - queueAdapter.setCurrentPlaying(index) - } + } } // val job4 = launch { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/screen/library/PlaylistScreen.kt b/app/src/main/java/com/maxrave/simpmusic/ui/screen/library/PlaylistScreen.kt index b8ad6d63..b9bbebc7 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/screen/library/PlaylistScreen.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/screen/library/PlaylistScreen.kt @@ -130,9 +130,7 @@ import com.skydoves.landscapist.palette.rememberPaletteState import com.skydoves.landscapist.placeholder.placeholder.PlaceholderPlugin import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.runBlocking import java.time.format.DateTimeFormatter @UnstableApi @@ -735,7 +733,7 @@ fun PlaylistScreen( index = index, from = "Playlist \"${localPlaylist?.title}\"" ) - if (!runBlocking { sharedViewModel.shuffleModeEnabled.first() }) { + if (!sharedViewModel.controllerState.value.isShuffle) { sharedViewModel.onUIEvent(UIEvent.Shuffle) } } else { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt b/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt index 93523844..1895e88a 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.QueueMusic import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Forward5 import androidx.compose.material.icons.filled.PauseCircle import androidx.compose.material.icons.filled.PlayCircle @@ -93,8 +94,12 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.offline.DownloadRequest +import androidx.media3.exoplayer.offline.DownloadService import androidx.navigation.NavController import com.maxrave.simpmusic.R +import com.maxrave.simpmusic.common.DownloadState +import com.maxrave.simpmusic.data.db.entities.PairSongLocalPlaylist import com.maxrave.simpmusic.extension.GradientAngle import com.maxrave.simpmusic.extension.GradientOffset import com.maxrave.simpmusic.extension.formatDuration @@ -102,12 +107,16 @@ import com.maxrave.simpmusic.extension.getBrushListColorFromPalette import com.maxrave.simpmusic.extension.getScreenSizeInfo import com.maxrave.simpmusic.extension.navigateSafe import com.maxrave.simpmusic.extension.parseTimestampToMilliseconds +import com.maxrave.simpmusic.extension.removeConflicts import com.maxrave.simpmusic.service.RepeatState +import com.maxrave.simpmusic.service.test.download.MusicDownloadService import com.maxrave.simpmusic.ui.component.CenterLoadingBox import com.maxrave.simpmusic.ui.component.DescriptionView +import com.maxrave.simpmusic.ui.component.FullscreenLyricsSheet import com.maxrave.simpmusic.ui.component.HeartCheckBox import com.maxrave.simpmusic.ui.component.LyricsView import com.maxrave.simpmusic.ui.component.MediaPlayerView +import com.maxrave.simpmusic.ui.component.NowPlayingBottomSheet import com.maxrave.simpmusic.ui.theme.AppTheme import com.maxrave.simpmusic.ui.theme.md_theme_dark_background import com.maxrave.simpmusic.ui.theme.overlay @@ -126,6 +135,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.mapNotNull +import java.time.LocalDateTime @UnstableApi @ExperimentalMaterial3Api @@ -145,8 +156,10 @@ fun NowPlayingScreen( val controllerState by sharedViewModel.controllerState.collectAsState() val screenDataState by sharedViewModel.nowPlayingScreenData.collectAsState() val timelineState by sharedViewModel.timeline.collectAsState() + val listLiked by sharedViewModel.listYouTubeLiked.collectAsState(initial = arrayListOf()) val playlistName by sharedViewModel.from.collectAsState(initial = "") + val songEntity = sharedViewModel.simpleMediaServiceHandler?.nowPlayingState?.mapNotNull { it.songEntity }?.collectAsState(initial = null) //State val mainScrollState = rememberScrollState() @@ -155,6 +168,14 @@ fun NowPlayingScreen( mutableStateOf(true) } + var showSheet by rememberSaveable { + mutableStateOf(false) + } + + var showFullscreenLyrics by rememberSaveable { + mutableStateOf(false) + } + //Palette state var palette by rememberPaletteState(null) val startColor = remember { @@ -289,6 +310,93 @@ fun NowPlayingScreen( } } + if (showSheet && songEntity != null) { + NowPlayingBottomSheet( + isBottomSheetVisible = showSheet, + onDismiss = { + showSheet = false + }, + navController = navController, + sharedViewModel = sharedViewModel, + songEntity = songEntity, + onToggleLike = { sharedViewModel.onUIEvent(UIEvent.ToggleLike) }, + getLocalPlaylist = { sharedViewModel.getAllLocalPlaylist() }, + listLocalPlaylist = sharedViewModel.localPlaylist.collectAsState(), + onDownload = { + songEntity.value?.let { song -> + sharedViewModel.updateDownloadState( + song.videoId, + DownloadState.STATE_PREPARING, + ) + val downloadRequest = + DownloadRequest + .Builder( + song.videoId, + song.videoId.toUri(), + ).setData(song.title.toByteArray()) + .setCustomCacheKey(song.videoId) + .build() + DownloadService.sendAddDownload( + context, + MusicDownloadService::class.java, + downloadRequest, + false, + ) + } + }, + onSleepTimer = { + sharedViewModel.setSleepTimer(it) + }, + onMainLyricsProvider = { provider -> + sharedViewModel.setLyricsProvider(provider) + }, + onAddToLocalPlaylist = { playlist -> + val song = songEntity.value ?: return@NowPlayingBottomSheet + val tempTrack = ArrayList() + if (playlist.tracks != null) { + tempTrack.addAll(playlist.tracks) + } + if (!tempTrack.contains( + song.videoId, + ) && + playlist.syncedWithYouTubePlaylist == 1 && + playlist.youtubePlaylistId != null + ) { + sharedViewModel.addToYouTubePlaylist( + playlist.id, + playlist.youtubePlaylistId, + song.videoId, + ) + } + if (!tempTrack.contains(song.videoId)) { + sharedViewModel.insertPairSongLocalPlaylist( + PairSongLocalPlaylist( + playlistId = playlist.id, + songId = song.videoId, + position = playlist.tracks?.size ?: 0, + inPlaylist = LocalDateTime.now(), + ), + ) + tempTrack.add(song.videoId) + } + sharedViewModel.updateLocalPlaylistTracks( + tempTrack.removeConflicts(), + playlist.id, + ) + } + ) + } + + if (showFullscreenLyrics) { + FullscreenLyricsSheet( + sharedViewModel = sharedViewModel, + color = startColor.value, + navController = navController + ) { + showFullscreenLyrics = false + } + } + Column( Modifier .verticalScroll( @@ -369,25 +477,9 @@ fun NowPlayingScreen( } } -// Box( -// modifier = -// if (screenDataState.canvasData != null && showHideControlLayout) { -// Modifier.fillMaxSize().background( -// Brush.verticalGradient( -// colorStops = arrayOf( -// 0.9f to overlay, -// 1f to Color.Black -// ), -// ) -// ) -// } -// else { -// Modifier.fillMaxSize().background(Color.Transparent) -// } -// ) } - TopAppBar( + TopAppBar ( modifier = Modifier .align(Alignment.TopCenter) .onGloballyPositioned { @@ -433,7 +525,9 @@ fun NowPlayingScreen( } }, actions = { - IconButton(onClick = { }) { + IconButton(onClick = { + showSheet = true + }) { Icon( painter = painterResource(id = R.drawable.baseline_more_vert_24), contentDescription = "", @@ -964,16 +1058,39 @@ fun NowPlayingScreen( Row( Modifier.align(Alignment.CenterEnd) ) { - IconButton( - modifier = Modifier - .size(24.dp) - .aspectRatio(1f) - .clip( - CircleShape - ), - onClick = { /*TODO*/ } + Crossfade( + targetState = !listLiked.isNullOrEmpty() && listLiked?.contains(screenDataState.songInfoData?.videoId) == true ) { - Icon(imageVector = Icons.Filled.Add, tint = Color.White, contentDescription = "") + if (it){ + IconButton( + modifier = Modifier + .size(24.dp) + .aspectRatio(1f) + .clip( + CircleShape + ), + onClick = { + sharedViewModel.addToYouTubeLiked() + } + ) { + Icon(imageVector = Icons.Filled.Done, tint = Color.White, contentDescription = "") + } + } + else { + IconButton( + modifier = Modifier + .size(24.dp) + .aspectRatio(1f) + .clip( + CircleShape + ), + onClick = { + sharedViewModel.addToYouTubeLiked() + } + ) { + Icon(imageVector = Icons.Filled.Add, tint = Color.White, contentDescription = "") + } + } } Spacer(modifier = Modifier.size(8.dp)) IconButton( @@ -1098,7 +1215,9 @@ fun NowPlayingScreen( ) CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { TextButton( - onClick = { /*TODO*/ }, contentPadding = PaddingValues(0.dp), + onClick = { + showFullscreenLyrics = true + }, contentPadding = PaddingValues(0.dp), modifier = Modifier .height(20.dp) .width(40.dp) @@ -1281,6 +1400,7 @@ fun NowPlayingScreen( ) } } + Spacer(modifier = Modifier.height(10.dp)) } } } diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt index 2fd9d011..3fba0c1c 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt @@ -5,8 +5,6 @@ import android.content.Intent import android.graphics.drawable.GradientDrawable import android.util.Log import android.widget.Toast -import androidx.appcompat.content.res.AppCompatResources -import androidx.core.graphics.drawable.toBitmap import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -19,8 +17,6 @@ import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager -import coil.ImageLoader -import coil.request.ImageRequest import com.maxrave.kotlinytmusicscraper.YouTube import com.maxrave.kotlinytmusicscraper.models.YouTubeLocale import com.maxrave.kotlinytmusicscraper.models.response.spotify.CanvasResponse @@ -56,7 +52,6 @@ import com.maxrave.simpmusic.data.model.metadata.Lyrics import com.maxrave.simpmusic.data.model.metadata.MetadataSong import com.maxrave.simpmusic.data.repository.MainRepository import com.maxrave.simpmusic.di.DownloadCache -import com.maxrave.simpmusic.extension.getScreenSize import com.maxrave.simpmusic.extension.isSong import com.maxrave.simpmusic.extension.isVideo import com.maxrave.simpmusic.extension.toListName @@ -73,9 +68,9 @@ import com.maxrave.simpmusic.service.QueueData import com.maxrave.simpmusic.service.RepeatState import com.maxrave.simpmusic.service.SimpleMediaServiceHandler import com.maxrave.simpmusic.service.SimpleMediaState +import com.maxrave.simpmusic.service.SleepTimerState import com.maxrave.simpmusic.service.test.download.DownloadUtils import com.maxrave.simpmusic.service.test.notification.NotifyWork -import com.maxrave.simpmusic.ui.widget.BasicWidget import com.maxrave.simpmusic.utils.Resource import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -171,21 +166,12 @@ constructor( private var lyricsFormat: MutableLiveData> = MutableLiveData() var lyricsFull = MutableLiveData() - private var _translateLyrics: MutableStateFlow = MutableStateFlow(null) - val translateLyrics: StateFlow = _translateLyrics - - private var _shuffleModeEnabled = MutableStateFlow(false) - val shuffleModeEnabled: StateFlow = _shuffleModeEnabled - - private var _repeatMode = MutableStateFlow(RepeatState.None) - val repeatMode: StateFlow = _repeatMode - // SponsorBlock private var _skipSegments: MutableStateFlow?> = MutableStateFlow(null) val skipSegments: StateFlow?> = _skipSegments - private var _sleepTimerRunning: MutableLiveData = MutableLiveData(false) - val sleepTimerRunning: LiveData = _sleepTimerRunning + private var _sleepTimerState = MutableStateFlow(SleepTimerState(false, 0)) + val sleepTimerState: StateFlow = _sleepTimerState private var watchTimeList: ArrayList = arrayListOf() @@ -220,8 +206,6 @@ constructor( var playlistId: MutableStateFlow = MutableStateFlow(null) - private var downloadImageForWidgetJob: Job? = null - private var _listYouTubeLiked: MutableStateFlow?> = MutableStateFlow(null) val listYouTubeLiked: SharedFlow?> = _listYouTubeLiked.asSharedFlow() @@ -257,8 +241,6 @@ constructor( ) val nowPlayingScreenData: StateFlow = _nowPlayingScreenData - private val basicWidget = BasicWidget.instance - init { viewModelScope.launch { val timeLineJob = launch { @@ -368,7 +350,6 @@ constructor( ) } } - updateWidget(state, handler) } } // initJob = @@ -403,10 +384,6 @@ constructor( is SimpleMediaState.Playing -> { isPlaying.value = mediaState.isPlaying - basicWidget.updatePlayingState( - context, - mediaState.isPlaying, - ) } is SimpleMediaState.Progress -> { @@ -572,7 +549,11 @@ constructor( _controllerState.value = it } } - controllerJob.join() + val sleepTimerJob = launch { + handler.sleepTimerState.collectLatest { + _sleepTimerState.value = it + } + } // val getDurationJob = launch { // combine(nowPlayingState, mediaState){ nowPlayingState, mediaState -> // Pair(nowPlayingState, mediaState) @@ -589,14 +570,14 @@ constructor( // } // } // getDurationJob.join() - val job3 = - launch { - handler.controlState.collectLatest { controlState -> - _shuffleModeEnabled.value = controlState.isShuffle - _repeatMode.value = controlState.repeatState - isPlaying.value = controlState.isPlaying - } - } +// val job3 = +// launch { +// handler.controlState.collectLatest { controlState -> +// _shuffleModeEnabled.value = controlState.isShuffle +// _repeatMode.value = controlState.repeatState +// isPlaying.value = controlState.isPlaying +// } +// } // val job10 = // launch { // nowPlayingMediaItem.collectLatest { now -> @@ -607,8 +588,10 @@ constructor( // } // } job1.join() + controllerJob.join() + sleepTimerJob.join() // job2.join() - job3.join() +// job3.join() // nowPlayingJob.join() // job10.join() } @@ -617,62 +600,6 @@ constructor( } } - private fun updateWidget(nowPlaying: NowPlayingTrackState, handler: SimpleMediaServiceHandler) { - basicWidget.performUpdate( - context, - handler, - null, - ) - downloadImageForWidgetJob?.cancel() - downloadImageForWidgetJob = - viewModelScope.launch { - val p = getScreenSize(context) - val widgetImageSize = p.x.coerceAtMost(p.y) - val imageRequest = - ImageRequest.Builder(context) - .data(nowPlaying.mediaItem.mediaMetadata.artworkUri) - .size(widgetImageSize) - .placeholder(R.drawable.holder_video) - .target( - onSuccess = { drawable -> - basicWidget.updateImage( - context, - drawable.toBitmap( - widgetImageSize, - widgetImageSize, - ), - ) - }, - onStart = { holder -> - if (holder != null) { - basicWidget.updateImage( - context, - holder.toBitmap( - widgetImageSize, - widgetImageSize, - ), - ) - } - }, - onError = { - AppCompatResources.getDrawable( - context, - R.drawable.holder_video, - ) - ?.let { it1 -> - basicWidget.updateImage( - context, - it1.toBitmap( - widgetImageSize, - widgetImageSize, - ), - ) - } - }, - ).build() - ImageLoader(context).execute(imageRequest) - } - } private fun getYouTubeLiked() { viewModelScope.launch { @@ -793,17 +720,11 @@ constructor( } fun setSleepTimer(minutes: Int) { - _sleepTimerRunning.value = true - simpleMediaServiceHandler!!.sleepStart(minutes) + simpleMediaServiceHandler?.sleepStart(minutes) } fun stopSleepTimer() { - _sleepTimerRunning.value = false - simpleMediaServiceHandler!!.sleepStop() - } - - fun updateLikeInNotification(liked: Boolean) { - simpleMediaServiceHandler?.like(liked) + simpleMediaServiceHandler?.sleepStop() } private var _downloadState: MutableStateFlow = MutableStateFlow(null) @@ -1008,7 +929,7 @@ constructor( } fun getCurrentMediaItemIndex(): Int { - return simpleMediaServiceHandler?.currentIndex() ?: 0 + return runBlocking { simpleMediaServiceHandler?.currentSongIndex?.first() } ?: 0 } @UnstableApi @@ -1038,8 +959,6 @@ constructor( } } } - mainRepository.updateSongInLibrary(LocalDateTime.now(), track.videoId) - mainRepository.updateListenCount(track.videoId) track.durationSeconds?.let { mainRepository.updateDurationSeconds( it, @@ -1273,7 +1192,6 @@ constructor( _lyrics.value = (Resource.Error("reset")) lyricsFormat.postValue(arrayListOf()) lyricsFull.postValue("") - _translateLyrics.value = null } fun updateDownloadState( @@ -1544,7 +1462,7 @@ constructor( private fun updateLyrics( videoId: String, lyrics: Lyrics?, isTranslatedLyrics: Boolean, lyricsProvider: LyricsProvider = LyricsProvider.MUSIXMATCH ) { - if (_nowPlayingState.value?.mediaItem?.mediaId == videoId) { + if (_nowPlayingState.value?.songEntity?.videoId == videoId) { when (isTranslatedLyrics) { true -> { _nowPlayingScreenData.update { diff --git a/app/src/main/res/anim/btt.xml b/app/src/main/res/anim/btt.xml new file mode 100644 index 00000000..f666d174 --- /dev/null +++ b/app/src/main/res/anim/btt.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/ttb.xml b/app/src/main/res/anim/ttb.xml new file mode 100644 index 00000000..90d14db6 --- /dev/null +++ b/app/src/main/res/anim/ttb.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2313445e..55536bcf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -338,4 +338,5 @@ Repeat One Repeat All Repeat Off + YouTube Transcript \ No newline at end of file