Drop androidx.media3
This commit is contained in:
@@ -86,15 +86,12 @@ dependencies {
|
|||||||
implementation(libs.accompanist.systemuicontroller)
|
implementation(libs.accompanist.systemuicontroller)
|
||||||
|
|
||||||
implementation(libs.android.media)
|
implementation(libs.android.media)
|
||||||
implementation(libs.media3.session)
|
implementation(libs.exoplayer)
|
||||||
implementation(libs.media3.exoplayer)
|
|
||||||
|
|
||||||
implementation(libs.room)
|
implementation(libs.room)
|
||||||
kapt(libs.room.compiler)
|
kapt(libs.room.compiler)
|
||||||
|
|
||||||
implementation(projects.youtubeMusic)
|
implementation(projects.youtubeMusic)
|
||||||
|
|
||||||
implementation(libs.guava.coroutines)
|
|
||||||
|
|
||||||
coreLibraryDesugaring(libs.desugaring)
|
coreLibraryDesugaring(libs.desugaring)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
@@ -63,12 +64,23 @@
|
|||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".services.PlayerService"
|
android:name=".services.PlayerService"
|
||||||
android:foregroundServiceType="mediaPlayback"
|
android:exported="false"
|
||||||
android:exported="false">
|
android:foregroundServiceType="mediaPlayback">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
||||||
<action android:name="android.media.browse.MediaBrowserService" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="androidx.media.session.MediaButtonReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".services.PlayerService$StopServiceBroadcastReceiver"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package it.vfsfitvnm.vimusic
|
package it.vfsfitvnm.vimusic
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
@@ -27,10 +31,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.media3.common.Player
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import com.valentinilk.shimmer.LocalShimmerTheme
|
import com.valentinilk.shimmer.LocalShimmerTheme
|
||||||
import com.valentinilk.shimmer.defaultShimmerTheme
|
import com.valentinilk.shimmer.defaultShimmerTheme
|
||||||
import it.vfsfitvnm.vimusic.enums.ColorPaletteMode
|
import it.vfsfitvnm.vimusic.enums.ColorPaletteMode
|
||||||
|
import it.vfsfitvnm.vimusic.services.PlayerService
|
||||||
import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu
|
import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState
|
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState
|
||||||
@@ -42,11 +48,34 @@ import it.vfsfitvnm.vimusic.ui.views.PlayerView
|
|||||||
import it.vfsfitvnm.vimusic.utils.*
|
import it.vfsfitvnm.vimusic.utils.*
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@ExperimentalFoundationApi
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
private val serviceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
if (service is PlayerService.Binder) {
|
||||||
|
this@MainActivity.binder = service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
binder = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var binder by mutableStateOf<PlayerService.Binder?>(null)
|
||||||
private var uri by mutableStateOf<Uri?>(null, neverEqualPolicy())
|
private var uri by mutableStateOf<Uri?>(null, neverEqualPolicy())
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
bindService(intent<PlayerService>(), serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
startService(intent<PlayerService>())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
unbindService(serviceConnection)
|
||||||
|
super.onStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -112,7 +141,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
LocalColorPalette provides colorPalette,
|
LocalColorPalette provides colorPalette,
|
||||||
LocalShimmerTheme provides shimmerTheme,
|
LocalShimmerTheme provides shimmerTheme,
|
||||||
LocalTypography provides rememberTypography(colorPalette.text),
|
LocalTypography provides rememberTypography(colorPalette.text),
|
||||||
LocalYoutubePlayer provides rememberYoutubePlayer((application as MainApplication).mediaControllerFuture),
|
LocalPlayerServiceBinder provides binder,
|
||||||
LocalMenuState provides rememberMenuState(),
|
LocalMenuState provides rememberMenuState(),
|
||||||
LocalHapticFeedback provides rememberHapticFeedback()
|
LocalHapticFeedback provides rememberHapticFeedback()
|
||||||
) {
|
) {
|
||||||
@@ -151,3 +180,5 @@ class MainActivity : ComponentActivity() {
|
|||||||
uri = intent?.data
|
uri = intent?.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val LocalPlayerServiceBinder = staticCompositionLocalOf<PlayerService.Binder?> { null }
|
||||||
@@ -1,31 +1,16 @@
|
|||||||
package it.vfsfitvnm.vimusic
|
package it.vfsfitvnm.vimusic
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ComponentName
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.media3.session.MediaController
|
|
||||||
import androidx.media3.session.SessionToken
|
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import coil.disk.DiskCache
|
import coil.disk.DiskCache
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import it.vfsfitvnm.vimusic.services.PlayerService
|
|
||||||
import it.vfsfitvnm.vimusic.utils.preferences
|
import it.vfsfitvnm.vimusic.utils.preferences
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@ExperimentalFoundationApi
|
|
||||||
class MainApplication : Application(), ImageLoaderFactory {
|
class MainApplication : Application(), ImageLoaderFactory {
|
||||||
lateinit var mediaControllerFuture: ListenableFuture<MediaController>
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
DatabaseInitializer()
|
DatabaseInitializer()
|
||||||
|
|
||||||
val sessionToken = SessionToken(this, ComponentName(this, PlayerService::class.java))
|
|
||||||
mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
|
|||||||
@@ -2,30 +2,28 @@ package it.vfsfitvnm.vimusic.services
|
|||||||
|
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.os.SystemClock
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.os.bundleOf
|
import androidx.media.session.MediaButtonReceiver
|
||||||
import androidx.media3.common.*
|
import androidx.media3.common.*
|
||||||
import androidx.media3.common.util.Util
|
|
||||||
import androidx.media3.database.StandaloneDatabaseProvider
|
import androidx.media3.database.StandaloneDatabaseProvider
|
||||||
import androidx.media3.datasource.DataSource
|
import androidx.media3.datasource.DataSource
|
||||||
import androidx.media3.datasource.DefaultHttpDataSource
|
import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
import androidx.media3.datasource.ResolvingDataSource
|
import androidx.media3.datasource.ResolvingDataSource
|
||||||
|
import androidx.media3.datasource.cache.Cache
|
||||||
import androidx.media3.datasource.cache.CacheDataSource
|
import androidx.media3.datasource.cache.CacheDataSource
|
||||||
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
||||||
import androidx.media3.datasource.cache.SimpleCache
|
import androidx.media3.datasource.cache.SimpleCache
|
||||||
@@ -34,83 +32,63 @@ import androidx.media3.exoplayer.analytics.AnalyticsListener
|
|||||||
import androidx.media3.exoplayer.analytics.PlaybackStats
|
import androidx.media3.exoplayer.analytics.PlaybackStats
|
||||||
import androidx.media3.exoplayer.analytics.PlaybackStatsListener
|
import androidx.media3.exoplayer.analytics.PlaybackStatsListener
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
import androidx.media3.session.*
|
|
||||||
import androidx.media3.session.MediaNotification.ActionFactory
|
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.google.common.collect.ImmutableList
|
|
||||||
import com.google.common.util.concurrent.Futures
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.MainActivity
|
import it.vfsfitvnm.vimusic.MainActivity
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.internal
|
import it.vfsfitvnm.vimusic.internal
|
||||||
import it.vfsfitvnm.vimusic.utils.*
|
import it.vfsfitvnm.vimusic.utils.*
|
||||||
import it.vfsfitvnm.youtubemusic.Outcome
|
import it.vfsfitvnm.youtubemusic.Outcome
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
val StartRadioCommand = SessionCommand("StartRadioCommand", Bundle.EMPTY)
|
class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback {
|
||||||
val StartArtistRadioCommand = SessionCommand("StartArtistRadioCommand", Bundle.EMPTY)
|
private lateinit var mediaSession: MediaSessionCompat
|
||||||
val StopRadioCommand = SessionCommand("StopRadioCommand", Bundle.EMPTY)
|
|
||||||
|
|
||||||
val GetCacheSizeCommand = SessionCommand("GetCacheSizeCommand", Bundle.EMPTY)
|
|
||||||
val GetSongCacheSizeCommand = SessionCommand("GetSongCacheSizeCommand", Bundle.EMPTY)
|
|
||||||
|
|
||||||
val DeleteSongCacheCommand = SessionCommand("DeleteSongCacheCommand", Bundle.EMPTY)
|
|
||||||
|
|
||||||
val SetSkipSilenceCommand = SessionCommand("SetSkipSilenceCommand", Bundle.EMPTY)
|
|
||||||
|
|
||||||
val GetAudioSessionIdCommand = SessionCommand("GetAudioSessionIdCommand", Bundle.EMPTY)
|
|
||||||
|
|
||||||
val SetSleepTimerCommand = SessionCommand("SetSleepTimerCommand", Bundle.EMPTY)
|
|
||||||
val GetSleepTimerMillisLeftCommand = SessionCommand("GetSleepTimerMillisLeftCommand", Bundle.EMPTY)
|
|
||||||
val CancelSleepTimerCommand = SessionCommand("CancelSleepTimerCommand", Bundle.EMPTY)
|
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@ExperimentalFoundationApi
|
|
||||||
class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotification.Provider,
|
|
||||||
PlaybackStatsListener.Callback, Player.Listener {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val NotificationId = 1001
|
|
||||||
private const val NotificationChannelId = "default_channel_id"
|
|
||||||
|
|
||||||
private const val SleepTimerNotificationId = 1002
|
|
||||||
private const val SleepTimerNotificationChannelId = "sleep_timer_channel_id"
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var cache: SimpleCache
|
private lateinit var cache: SimpleCache
|
||||||
|
|
||||||
private lateinit var player: ExoPlayer
|
private lateinit var player: ExoPlayer
|
||||||
|
|
||||||
private lateinit var mediaSession: MediaSession
|
private val stateBuilder = PlaybackStateCompat.Builder()
|
||||||
|
.setActions(
|
||||||
|
PlaybackStateCompat.ACTION_PLAY or
|
||||||
|
PlaybackStateCompat.ACTION_PAUSE or
|
||||||
|
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
||||||
|
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||||
|
PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||||
|
PlaybackStateCompat.ACTION_SEEK_TO
|
||||||
|
)
|
||||||
|
|
||||||
|
private val metadataBuilder = MediaMetadataCompat.Builder()
|
||||||
|
|
||||||
private lateinit var notificationManager: NotificationManager
|
private lateinit var notificationManager: NotificationManager
|
||||||
|
|
||||||
|
private var timerJob: TimerJob? = null
|
||||||
|
|
||||||
private var notificationThumbnailSize: Int = 0
|
private var notificationThumbnailSize: Int = 0
|
||||||
private var lastArtworkUri: Uri? = null
|
private var lastArtworkUri: Uri? = null
|
||||||
private var lastBitmap: Bitmap? = null
|
private var lastBitmap: Bitmap? = null
|
||||||
|
|
||||||
private var radio: YoutubePlayer.Radio? = null
|
private var radio: YoutubePlayer.Radio? = null
|
||||||
|
|
||||||
private var sleepTimerJob: Job? = null
|
|
||||||
private var sleepTimerRealtime: Long? = null
|
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO) + Job()
|
private val coroutineScope = CoroutineScope(Dispatchers.IO) + Job()
|
||||||
|
|
||||||
private val songPendingLoudnessDb = mutableMapOf<String, Float?>()
|
private val songPendingLoudnessDb = mutableMapOf<String, Float?>()
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?) = Binder()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
notificationThumbnailSize = (256 * resources.displayMetrics.density).roundToInt()
|
notificationThumbnailSize = (256 * resources.displayMetrics.density).roundToInt()
|
||||||
|
|
||||||
|
lastBitmap = resources.getDrawable(R.drawable.disc_placeholder, null)
|
||||||
|
?.toBitmap(notificationThumbnailSize, notificationThumbnailSize)
|
||||||
|
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
setMediaNotificationProvider(this)
|
|
||||||
|
|
||||||
val cacheEvictor = LeastRecentlyUsedCacheEvictor(preferences.exoPlayerDiskCacheMaxSizeBytes)
|
val cacheEvictor = LeastRecentlyUsedCacheEvictor(preferences.exoPlayerDiskCacheMaxSizeBytes)
|
||||||
cache = SimpleCache(cacheDir, cacheEvictor, StandaloneDatabaseProvider(this))
|
cache = SimpleCache(cacheDir, cacheEvictor, StandaloneDatabaseProvider(this))
|
||||||
@@ -132,153 +110,39 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific
|
|||||||
player.repeatMode = preferences.repeatMode
|
player.repeatMode = preferences.repeatMode
|
||||||
player.skipSilenceEnabled = preferences.skipSilence
|
player.skipSilenceEnabled = preferences.skipSilence
|
||||||
player.playWhenReady = true
|
player.playWhenReady = true
|
||||||
|
player.addListener(this)
|
||||||
player.addAnalyticsListener(PlaybackStatsListener(false, this))
|
player.addAnalyticsListener(PlaybackStatsListener(false, this))
|
||||||
|
|
||||||
mediaSession = MediaSession.Builder(this, player)
|
mediaSession = MediaSessionCompat(this, "PlayerService")
|
||||||
.withSessionActivity()
|
mediaSession.setCallback(SessionCallback(player))
|
||||||
.setCallback(this)
|
mediaSession.setPlaybackState(stateBuilder.build())
|
||||||
.build()
|
mediaSession.isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
player.addListener(this)
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
MediaButtonReceiver.handleIntent(mediaSession, intent)
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
|
if (!player.playWhenReady) {
|
||||||
|
notificationManager.cancel(NotificationId)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onTaskRemoved(rootIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
player.removeListener(this)
|
||||||
|
player.stop()
|
||||||
player.release()
|
player.release()
|
||||||
|
mediaSession.isActive = false
|
||||||
mediaSession.release()
|
mediaSession.release()
|
||||||
cache.release()
|
cache.release()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession {
|
|
||||||
return mediaSession
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConnect(
|
|
||||||
session: MediaSession,
|
|
||||||
controller: MediaSession.ControllerInfo
|
|
||||||
): MediaSession.ConnectionResult {
|
|
||||||
val sessionCommands = SessionCommands.Builder()
|
|
||||||
.add(StartRadioCommand)
|
|
||||||
.add(StartArtistRadioCommand)
|
|
||||||
.add(StopRadioCommand)
|
|
||||||
.add(GetCacheSizeCommand)
|
|
||||||
.add(GetSongCacheSizeCommand)
|
|
||||||
.add(DeleteSongCacheCommand)
|
|
||||||
.add(SetSkipSilenceCommand)
|
|
||||||
.add(GetAudioSessionIdCommand)
|
|
||||||
.add(SetSleepTimerCommand)
|
|
||||||
.add(GetSleepTimerMillisLeftCommand)
|
|
||||||
.add(CancelSleepTimerCommand)
|
|
||||||
.build()
|
|
||||||
val playerCommands = Player.Commands.Builder().addAllCommands().build()
|
|
||||||
return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCustomCommand(
|
|
||||||
session: MediaSession,
|
|
||||||
controller: MediaSession.ControllerInfo,
|
|
||||||
customCommand: SessionCommand,
|
|
||||||
args: Bundle
|
|
||||||
): ListenableFuture<SessionResult> {
|
|
||||||
when (customCommand) {
|
|
||||||
StartRadioCommand, StartArtistRadioCommand -> {
|
|
||||||
radio = null
|
|
||||||
YoutubePlayer.Radio(
|
|
||||||
videoId = args.getString("videoId"),
|
|
||||||
playlistId = args.getString("playlistId"),
|
|
||||||
playlistSetVideoId = args.getString("playlistSetVideoId"),
|
|
||||||
parameters = args.getString("params"),
|
|
||||||
).let {
|
|
||||||
coroutineScope.launch(Dispatchers.Main) {
|
|
||||||
when (customCommand) {
|
|
||||||
StartRadioCommand -> player.addMediaItems(it.process().drop(1))
|
|
||||||
StartArtistRadioCommand -> player.forcePlayFromBeginning(it.process())
|
|
||||||
}
|
|
||||||
radio = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StopRadioCommand -> radio = null
|
|
||||||
GetCacheSizeCommand -> {
|
|
||||||
return Futures.immediateFuture(
|
|
||||||
SessionResult(
|
|
||||||
SessionResult.RESULT_SUCCESS,
|
|
||||||
bundleOf("cacheSize" to cache.cacheSpace)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
GetSongCacheSizeCommand -> {
|
|
||||||
return Futures.immediateFuture(
|
|
||||||
SessionResult(
|
|
||||||
SessionResult.RESULT_SUCCESS,
|
|
||||||
bundleOf("cacheSize" to cache.getCachedBytes(
|
|
||||||
args.getString("videoId") ?: "",
|
|
||||||
0,
|
|
||||||
C.LENGTH_UNSET.toLong()
|
|
||||||
))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DeleteSongCacheCommand -> {
|
|
||||||
args.getString("videoId")?.let { videoId ->
|
|
||||||
cache.removeResource(videoId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SetSkipSilenceCommand -> {
|
|
||||||
player.skipSilenceEnabled = args.getBoolean("skipSilence")
|
|
||||||
}
|
|
||||||
GetAudioSessionIdCommand -> {
|
|
||||||
return Futures.immediateFuture(
|
|
||||||
SessionResult(
|
|
||||||
SessionResult.RESULT_SUCCESS,
|
|
||||||
bundleOf("audioSessionId" to player.audioSessionId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SetSleepTimerCommand -> {
|
|
||||||
val delayMillis = args.getLong("delayMillis", 2000)
|
|
||||||
|
|
||||||
sleepTimerJob = coroutineScope.launch {
|
|
||||||
sleepTimerRealtime = SystemClock.elapsedRealtime() + delayMillis
|
|
||||||
delay(delayMillis)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
val notification = NotificationCompat.Builder(
|
|
||||||
this@PlayerService,
|
|
||||||
SleepTimerNotificationChannelId
|
|
||||||
)
|
|
||||||
.setContentTitle("Sleep timer ended")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.setShowWhen(true)
|
|
||||||
.setSmallIcon(R.drawable.app_icon)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
notificationManager.notify(SleepTimerNotificationId, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GetSleepTimerMillisLeftCommand -> {
|
|
||||||
return Futures.immediateFuture(sleepTimerRealtime?.let {
|
|
||||||
(SessionResult(
|
|
||||||
SessionResult.RESULT_SUCCESS,
|
|
||||||
bundleOf("millisLeft" to it - SystemClock.elapsedRealtime())
|
|
||||||
))
|
|
||||||
} ?: SessionResult(SessionResult.RESULT_ERROR_INVALID_STATE))
|
|
||||||
}
|
|
||||||
CancelSleepTimerCommand -> {
|
|
||||||
sleepTimerJob?.cancel()
|
|
||||||
sleepTimerJob = null
|
|
||||||
sleepTimerRealtime = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCustomCommand(session, controller, customCommand, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlaybackStatsReady(
|
override fun onPlaybackStatsReady(
|
||||||
eventTime: AnalyticsListener.EventTime,
|
eventTime: AnalyticsListener.EventTime,
|
||||||
playbackStats: PlaybackStats
|
playbackStats: PlaybackStats
|
||||||
@@ -308,63 +172,62 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific
|
|||||||
player.volume = player.currentMediaItem?.mediaId?.let { mediaId ->
|
player.volume = player.currentMediaItem?.mediaId?.let { mediaId ->
|
||||||
songPendingLoudnessDb.getOrElse(mediaId) {
|
songPendingLoudnessDb.getOrElse(mediaId) {
|
||||||
player.currentMediaItem?.mediaMetadata?.extras?.getFloat("loudnessDb")
|
player.currentMediaItem?.mediaMetadata?.extras?.getFloat("loudnessDb")
|
||||||
}
|
}?.takeIf { it > 0 }?.let { loudnessDb ->
|
||||||
?.takeIf { it > 0 }
|
(1f - (0.01f + loudnessDb / 14)).coerceIn(0.1f, 1f)
|
||||||
?.let { loudnessDb ->
|
|
||||||
(1f - (0.01f + loudnessDb / 15)).coerceIn(0.1f, 1f)
|
|
||||||
}
|
}
|
||||||
} ?: 1f
|
} ?: 1f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAddMediaItems(
|
override fun onPositionDiscontinuity(
|
||||||
mediaSession: MediaSession,
|
oldPosition: Player.PositionInfo,
|
||||||
controller: MediaSession.ControllerInfo,
|
newPosition: Player.PositionInfo,
|
||||||
mediaItems: List<MediaItem>
|
@Player.DiscontinuityReason reason: Int
|
||||||
): ListenableFuture<List<MediaItem>> {
|
) {
|
||||||
return Futures.immediateFuture(
|
stateBuilder
|
||||||
mediaItems.map { mediaItem ->
|
.setState(PlaybackStateCompat.STATE_NONE, newPosition.positionMs, 1f)
|
||||||
mediaItem.buildUpon()
|
.setBufferedPosition(player.bufferedPosition)
|
||||||
.setUri(mediaItem.mediaId)
|
|
||||||
.setCustomCacheKey(mediaItem.mediaId)
|
updateNotification()
|
||||||
.build()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createNotification(
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
session: MediaSession,
|
stateBuilder
|
||||||
customLayout: ImmutableList<CommandButton>,
|
.setState(
|
||||||
actionFactory: ActionFactory,
|
if (isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED,
|
||||||
onNotificationChangedCallback: MediaNotification.Provider.Callback
|
player.currentPosition,
|
||||||
): MediaNotification {
|
1f
|
||||||
fun invalidate() {
|
|
||||||
onNotificationChangedCallback.onNotificationChanged(
|
|
||||||
createNotification(
|
|
||||||
session,
|
|
||||||
customLayout,
|
|
||||||
actionFactory,
|
|
||||||
onNotificationChangedCallback
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
.setBufferedPosition(player.bufferedPosition)
|
||||||
|
|
||||||
|
updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateNotification() {
|
||||||
|
if (player.duration != C.TIME_UNSET) {
|
||||||
|
metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player.duration)
|
||||||
|
mediaSession.setMetadata(metadataBuilder.build())
|
||||||
|
}
|
||||||
|
mediaSession.setPlaybackState(stateBuilder.build())
|
||||||
|
createNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotification() {
|
||||||
fun NotificationCompat.Builder.addMediaAction(
|
fun NotificationCompat.Builder.addMediaAction(
|
||||||
@DrawableRes resId: Int,
|
@DrawableRes resId: Int,
|
||||||
@StringRes stringId: Int,
|
description: String,
|
||||||
@Player.Command command: Int
|
@PlaybackStateCompat.MediaKeyAction command: Long
|
||||||
): NotificationCompat.Builder {
|
): NotificationCompat.Builder {
|
||||||
return addAction(
|
return addAction(
|
||||||
actionFactory.createMediaAction(
|
NotificationCompat.Action(
|
||||||
mediaSession,
|
resId,
|
||||||
IconCompat.createWithResource(this@PlayerService, resId),
|
description,
|
||||||
getString(stringId),
|
MediaButtonReceiver.buildMediaButtonPendingIntent(this@PlayerService, command)
|
||||||
command
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaMetadata = mediaSession.player.mediaMetadata
|
val mediaMetadata = player.mediaMetadata
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(applicationContext, NotificationChannelId)
|
val builder = NotificationCompat.Builder(applicationContext, NotificationChannelId)
|
||||||
.setContentTitle(mediaMetadata.title)
|
.setContentTitle(mediaMetadata.title)
|
||||||
@@ -375,72 +238,75 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific
|
|||||||
.setShowWhen(false)
|
.setShowWhen(false)
|
||||||
.setSmallIcon(R.drawable.app_icon)
|
.setSmallIcon(R.drawable.app_icon)
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
.setContentIntent(mediaSession.sessionActivity)
|
.setContentIntent(activityPendingIntent<MainActivity>())
|
||||||
.setDeleteIntent(
|
.setDeleteIntent(broadCastPendingIntent<StopServiceBroadcastReceiver>())
|
||||||
actionFactory.createMediaActionPendingIntent(
|
.setChannelId(NotificationChannelId)
|
||||||
mediaSession,
|
|
||||||
Player.COMMAND_STOP.toLong()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
.setStyle(
|
.setStyle(
|
||||||
androidx.media.app.NotificationCompat.MediaStyle()
|
androidx.media.app.NotificationCompat.MediaStyle()
|
||||||
.setShowActionsInCompactView(0, 1, 2)
|
.setShowActionsInCompactView(0, 1, 2)
|
||||||
.setMediaSession(mediaSession.sessionCompatToken as android.support.v4.media.session.MediaSessionCompat.Token)
|
.setMediaSession(mediaSession.sessionToken)
|
||||||
)
|
)
|
||||||
.addMediaAction(
|
.addMediaAction(
|
||||||
R.drawable.play_skip_back,
|
R.drawable.play_skip_back,
|
||||||
R.string.media3_controls_seek_to_previous_description,
|
"Skip back",
|
||||||
Player.COMMAND_SEEK_TO_PREVIOUS
|
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||||
).addMediaAction(
|
).addMediaAction(
|
||||||
if (mediaSession.player.playbackState == Player.STATE_ENDED || !mediaSession.player.playWhenReady) R.drawable.play else R.drawable.pause,
|
if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) R.drawable.play else R.drawable.pause,
|
||||||
if (mediaSession.player.playbackState == Player.STATE_ENDED || !mediaSession.player.playWhenReady) R.string.media3_controls_play_description else R.string.media3_controls_pause_description,
|
if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) "Play" else "Pause",
|
||||||
Player.COMMAND_PLAY_PAUSE
|
if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) PlaybackStateCompat.ACTION_PLAY else PlaybackStateCompat.ACTION_PAUSE
|
||||||
)
|
)
|
||||||
.addMediaAction(
|
.addMediaAction(
|
||||||
R.drawable.play_skip_forward,
|
R.drawable.play_skip_forward,
|
||||||
R.string.media3_controls_seek_to_next_description,
|
"Skip forward",
|
||||||
Player.COMMAND_SEEK_TO_NEXT
|
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
||||||
)
|
)
|
||||||
|
|
||||||
if (lastArtworkUri != mediaMetadata.artworkUri) {
|
if (lastArtworkUri != mediaMetadata.artworkUri) {
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
lastArtworkUri = mediaMetadata.artworkUri
|
||||||
lastBitmap = Coil.imageLoader(applicationContext).execute(
|
|
||||||
|
Coil.imageLoader(applicationContext).enqueue(
|
||||||
ImageRequest.Builder(applicationContext)
|
ImageRequest.Builder(applicationContext)
|
||||||
.data(mediaMetadata.artworkUri.thumbnail(notificationThumbnailSize))
|
.data(mediaMetadata.artworkUri.thumbnail(notificationThumbnailSize))
|
||||||
.build()
|
.listener(
|
||||||
).drawable?.let {
|
onError = { _, _ ->
|
||||||
lastArtworkUri = mediaMetadata.artworkUri
|
lastBitmap = resources.getDrawable(R.drawable.disc_placeholder, null)
|
||||||
(it as BitmapDrawable).bitmap
|
|
||||||
} ?: resources.getDrawable(R.drawable.disc_placeholder, null)
|
|
||||||
?.toBitmap(notificationThumbnailSize, notificationThumbnailSize)
|
?.toBitmap(notificationThumbnailSize, notificationThumbnailSize)
|
||||||
|
notificationManager.notify(NotificationId, builder.setLargeIcon(lastBitmap).build())
|
||||||
withContext(Dispatchers.Main) {
|
},
|
||||||
invalidate()
|
onSuccess = { _, result ->
|
||||||
}
|
lastBitmap = (result.drawable as BitmapDrawable).bitmap
|
||||||
|
notificationManager.notify(NotificationId, builder.setLargeIcon(lastBitmap).build())
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaNotification(NotificationId, builder.build())
|
val notificationCompat = builder.build()
|
||||||
}
|
startForeground(NotificationId, notificationCompat)
|
||||||
|
|
||||||
override fun handleCustomCommand(
|
if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) {
|
||||||
session: MediaSession,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
action: String,
|
stopForeground(STOP_FOREGROUND_DETACH)
|
||||||
extras: Bundle
|
} else {
|
||||||
): Boolean = false
|
stopForeground(false)
|
||||||
|
}
|
||||||
|
notificationManager.notify(NotificationId, notificationCompat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
if (Util.SDK_INT < 26) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
|
||||||
with(notificationManager) {
|
with(notificationManager) {
|
||||||
if (getNotificationChannel(NotificationChannelId) == null) {
|
if (getNotificationChannel(NotificationChannelId) == null) {
|
||||||
createNotificationChannel(
|
createNotificationChannel(
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
NotificationChannelId,
|
NotificationChannelId,
|
||||||
getString(R.string.default_notification_channel_name),
|
"Now playing",
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -504,10 +370,12 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific
|
|||||||
|
|
||||||
if (mediaItem?.mediaId == videoId) {
|
if (mediaItem?.mediaId == videoId) {
|
||||||
Database.internal.queryExecutor.execute {
|
Database.internal.queryExecutor.execute {
|
||||||
Database.update(Database.insert(mediaItem).copy(
|
Database.update(
|
||||||
|
Database.insert(mediaItem).copy(
|
||||||
loudnessDb = loudnessDb,
|
loudnessDb = loudnessDb,
|
||||||
contentLength = format.contentLength
|
contentLength = format.contentLength
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,14 +421,99 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MediaSession.Builder.withSessionActivity(): MediaSession.Builder {
|
inner class Binder : android.os.Binder() {
|
||||||
return setSessionActivity(
|
val player: ExoPlayer
|
||||||
PendingIntent.getActivity(
|
get() = this@PlayerService.player
|
||||||
this@PlayerService,
|
|
||||||
0,
|
val cache: Cache
|
||||||
Intent(this@PlayerService, MainActivity::class.java),
|
get() = this@PlayerService.cache
|
||||||
if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0
|
|
||||||
)
|
val sleepTimerMillisLeft: StateFlow<Long?>?
|
||||||
|
get() = timerJob?.millisLeft
|
||||||
|
|
||||||
|
fun startSleepTimer(delayMillis: Long) {
|
||||||
|
timerJob?.cancel()
|
||||||
|
|
||||||
|
timerJob = coroutineScope.timer(delayMillis) {
|
||||||
|
val notification = NotificationCompat
|
||||||
|
.Builder(this@PlayerService, SleepTimerNotificationChannelId)
|
||||||
|
.setContentTitle("Sleep timer ended")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setSmallIcon(R.drawable.app_icon)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
notificationManager.notify(SleepTimerNotificationId, notification)
|
||||||
|
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelSleepTimer() {
|
||||||
|
timerJob?.cancel()
|
||||||
|
timerJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startRadio(
|
||||||
|
endpoint: NavigationEndpoint.Endpoint.Watch?,
|
||||||
|
) {
|
||||||
|
startRadio(
|
||||||
|
videoId = endpoint?.videoId,
|
||||||
|
playlistId = endpoint?.playlistId,
|
||||||
|
playlistSetVideoId = endpoint?.playlistSetVideoId,
|
||||||
|
parameters = endpoint?.params,
|
||||||
|
justAdd = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startRadio(
|
||||||
|
videoId: String?,
|
||||||
|
playlistId: String? = null,
|
||||||
|
playlistSetVideoId: String? = null,
|
||||||
|
parameters: String? = null,
|
||||||
|
justAdd: Boolean = true
|
||||||
|
) {
|
||||||
|
radio = null
|
||||||
|
YoutubePlayer.Radio(
|
||||||
|
videoId, playlistId, playlistSetVideoId, parameters
|
||||||
|
).let {
|
||||||
|
coroutineScope.launch(Dispatchers.Main) {
|
||||||
|
if (justAdd) {
|
||||||
|
player.addMediaItems(it.process().drop(1))
|
||||||
|
} else {
|
||||||
|
player.forcePlayFromBeginning(it.process())
|
||||||
|
}
|
||||||
|
radio = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopRadio() {
|
||||||
|
radio = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SessionCallback(private val player: Player) : MediaSessionCompat.Callback() {
|
||||||
|
override fun onPlay() = player.play()
|
||||||
|
override fun onPause() = player.pause()
|
||||||
|
override fun onSkipToPrevious() = player.seekToPrevious()
|
||||||
|
override fun onSkipToNext() = player.seekToNext()
|
||||||
|
override fun onSeekTo(pos: Long) = player.seekTo(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
class StopServiceBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
context.stopService(context.intent<PlayerService>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NotificationId = 1001
|
||||||
|
private const val NotificationChannelId = "default_channel_id"
|
||||||
|
|
||||||
|
private const val SleepTimerNotificationId = 1002
|
||||||
|
private const val SleepTimerNotificationChannelId = "sleep_timer_channel_id"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,12 @@ import androidx.media3.common.Player
|
|||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.route.empty
|
import it.vfsfitvnm.route.empty
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.internal
|
import it.vfsfitvnm.vimusic.internal
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
||||||
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
||||||
import it.vfsfitvnm.vimusic.services.DeleteSongCacheCommand
|
|
||||||
import it.vfsfitvnm.vimusic.services.StartRadioCommand
|
|
||||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute
|
import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.rememberCreatePlaylistRoute
|
import it.vfsfitvnm.vimusic.ui.screens.rememberCreatePlaylistRoute
|
||||||
@@ -66,7 +64,7 @@ fun InHistoryMediaItemMenu(
|
|||||||
// https://issuetracker.google.com/issues/226410236
|
// https://issuetracker.google.com/issues/226410236
|
||||||
onDismiss: () -> Unit = LocalMenuState.current.let { it::hide }
|
onDismiss: () -> Unit = LocalMenuState.current.let { it::hide }
|
||||||
) {
|
) {
|
||||||
val mediaController = LocalYoutubePlayer.current?.mediaController
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -82,7 +80,7 @@ fun InHistoryMediaItemMenu(
|
|||||||
},
|
},
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
mediaController?.sendCustomCommand(DeleteSongCacheCommand, bundleOf("videoId" to song.song.id))
|
binder?.cache?.removeResource(song.song.id)
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
Database.delete(song.song)
|
Database.delete(song.song)
|
||||||
}
|
}
|
||||||
@@ -147,32 +145,24 @@ fun NonQueuedMediaItemMenu(
|
|||||||
onDeleteFromDatabase: (() -> Unit)? = null,
|
onDeleteFromDatabase: (() -> Unit)? = null,
|
||||||
onRemoveFromFavorites: (() -> Unit)? = null,
|
onRemoveFromFavorites: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val player = LocalYoutubePlayer.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
BaseMediaItemMenu(
|
BaseMediaItemMenu(
|
||||||
mediaItem = mediaItem,
|
mediaItem = mediaItem,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
onStartRadio = {
|
onStartRadio = {
|
||||||
player?.mediaController?.run {
|
binder?.player?.forcePlay(mediaItem)
|
||||||
forcePlay(mediaItem)
|
binder?.startRadio(videoId = mediaItem.mediaId, playlistId = mediaItem.mediaMetadata.extras?.getString("playlistId"))
|
||||||
sendCustomCommand(StartRadioCommand, bundleOf(
|
|
||||||
"videoId" to mediaItem.mediaId,
|
|
||||||
"playlistId" to mediaItem.mediaMetadata.extras?.getString("playlistId")
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onPlaySingle = {
|
onPlaySingle = {
|
||||||
player?.mediaController?.run {
|
binder?.player?.forcePlay(mediaItem)
|
||||||
sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
},
|
||||||
forcePlay(mediaItem)
|
onPlayNext = {
|
||||||
}
|
binder?.player?.addNext(mediaItem)
|
||||||
|
},
|
||||||
|
onEnqueue = {
|
||||||
|
binder?.player?.enqueue(mediaItem)
|
||||||
},
|
},
|
||||||
onPlayNext = if (player?.playbackState == Player.STATE_READY) ({
|
|
||||||
player.mediaController.addNext(mediaItem)
|
|
||||||
}) else null,
|
|
||||||
onEnqueue = if (player?.playbackState == Player.STATE_READY) ({
|
|
||||||
player.mediaController.enqueue(mediaItem)
|
|
||||||
}) else null,
|
|
||||||
onRemoveFromPlaylist = onRemoveFromPlaylist,
|
onRemoveFromPlaylist = onRemoveFromPlaylist,
|
||||||
onDeleteFromDatabase = onDeleteFromDatabase,
|
onDeleteFromDatabase = onDeleteFromDatabase,
|
||||||
onRemoveFromFavorites = onRemoveFromFavorites,
|
onRemoveFromFavorites = onRemoveFromFavorites,
|
||||||
@@ -190,14 +180,14 @@ fun QueuedMediaItemMenu(
|
|||||||
onDismiss: () -> Unit = LocalMenuState.current.let { it::hide },
|
onDismiss: () -> Unit = LocalMenuState.current.let { it::hide },
|
||||||
onGlobalRouteEmitted: (() -> Unit)? = null
|
onGlobalRouteEmitted: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val player = LocalYoutubePlayer.current
|
val player = LocalPlayerServiceBinder.current?.player
|
||||||
|
|
||||||
BaseMediaItemMenu(
|
BaseMediaItemMenu(
|
||||||
mediaItem = mediaItem,
|
mediaItem = mediaItem,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
onRemoveFromQueue = if (player?.mediaItemIndex != indexInQueue) ({
|
onRemoveFromQueue = {
|
||||||
player?.mediaController?.removeMediaItem(indexInQueue)
|
player?.removeMediaItem(indexInQueue)
|
||||||
}) else null,
|
},
|
||||||
onGlobalRouteEmitted = onGlobalRouteEmitted,
|
onGlobalRouteEmitted = onGlobalRouteEmitted,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens
|
package it.vfsfitvnm.vimusic.ui.screens
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -27,10 +26,9 @@ import coil.compose.AsyncImage
|
|||||||
import com.valentinilk.shimmer.shimmer
|
import com.valentinilk.shimmer.shimmer
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
||||||
import it.vfsfitvnm.vimusic.services.StartArtistRadioCommand
|
|
||||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
|
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||||
@@ -79,7 +77,8 @@ fun ArtistScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
host {
|
host {
|
||||||
val player = LocalYoutubePlayer.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val colorPalette = LocalColorPalette.current
|
val colorPalette = LocalColorPalette.current
|
||||||
val typography = LocalTypography.current
|
val typography = LocalTypography.current
|
||||||
@@ -160,10 +159,7 @@ fun ArtistScreen(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player?.mediaController?.sendCustomCommand(
|
binder?.startRadio(artist.shuffleEndpoint)
|
||||||
StartArtistRadioCommand,
|
|
||||||
artist.shuffleEndpoint.asBundle
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||||
.background(
|
.background(
|
||||||
@@ -180,10 +176,7 @@ fun ArtistScreen(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player?.mediaController?.sendCustomCommand(
|
binder?.startRadio(artist.radioEndpoint)
|
||||||
StartArtistRadioCommand,
|
|
||||||
artist.radioEndpoint.asBundle
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||||
.background(
|
.background(
|
||||||
@@ -224,14 +217,8 @@ fun ArtistScreen(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(enabled = songs.isNotEmpty()) {
|
.clickable(enabled = songs.isNotEmpty()) {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
binder?.player?.forcePlayFromBeginning(songs.shuffled().map(SongWithInfo::asMediaItem))
|
||||||
it.forcePlayFromBeginning(
|
|
||||||
songs
|
|
||||||
.shuffled()
|
|
||||||
.map(SongWithInfo::asMediaItem)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||||
.size(20.dp)
|
.size(20.dp)
|
||||||
@@ -248,10 +235,8 @@ fun ArtistScreen(
|
|||||||
song = song,
|
song = song,
|
||||||
thumbnailSize = songThumbnailSizePx,
|
thumbnailSize = songThumbnailSizePx,
|
||||||
onClick = {
|
onClick = {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
binder?.player?.forcePlayAtIndex(songs.map(SongWithInfo::asMediaItem), index)
|
||||||
it.forcePlayAtIndex(songs.map(SongWithInfo::asMediaItem), index)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
menuContent = {
|
menuContent = {
|
||||||
InHistoryMediaItemMenu(song = song)
|
InHistoryMediaItemMenu(song = song)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens
|
package it.vfsfitvnm.vimusic.ui.screens
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -32,15 +31,18 @@ import androidx.compose.ui.zIndex
|
|||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.route.fastFade
|
import it.vfsfitvnm.route.fastFade
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.SongCollection
|
import it.vfsfitvnm.vimusic.enums.SongCollection
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
import it.vfsfitvnm.vimusic.models.SearchQuery
|
import it.vfsfitvnm.vimusic.models.SearchQuery
|
||||||
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
||||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.*
|
import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
|
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
|
||||||
import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
|
import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
|
||||||
@@ -144,7 +146,7 @@ fun HomeScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
host {
|
host {
|
||||||
val player = LocalYoutubePlayer.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val thumbnailSize = remember {
|
val thumbnailSize = remember {
|
||||||
@@ -357,10 +359,8 @@ fun HomeScreen() {
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(enabled = songCollection.isNotEmpty()) {
|
.clickable(enabled = songCollection.isNotEmpty()) {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
binder?.player?.forcePlayFromBeginning(songCollection.shuffled().map(SongWithInfo::asMediaItem))
|
||||||
it.forcePlayFromBeginning(songCollection.shuffled().map(SongWithInfo::asMediaItem))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||||
.size(20.dp)
|
.size(20.dp)
|
||||||
@@ -379,10 +379,8 @@ fun HomeScreen() {
|
|||||||
song = song,
|
song = song,
|
||||||
thumbnailSize = thumbnailSize,
|
thumbnailSize = thumbnailSize,
|
||||||
onClick = {
|
onClick = {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
binder?.player?.forcePlayAtIndex(songCollection.map(SongWithInfo::asMediaItem), index)
|
||||||
it.forcePlayAtIndex(songCollection.map(SongWithInfo::asMediaItem), index)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
menuContent = {
|
menuContent = {
|
||||||
when (preferences.homePageSongCollection) {
|
when (preferences.homePageSongCollection) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens
|
package it.vfsfitvnm.vimusic.ui.screens
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -18,16 +17,15 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.media3.common.Player
|
|
||||||
import com.valentinilk.shimmer.ShimmerBounds
|
import com.valentinilk.shimmer.ShimmerBounds
|
||||||
import com.valentinilk.shimmer.rememberShimmer
|
import com.valentinilk.shimmer.rememberShimmer
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.internal
|
import it.vfsfitvnm.vimusic.internal
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
||||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.Error
|
import it.vfsfitvnm.vimusic.ui.components.Error
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
import it.vfsfitvnm.vimusic.ui.components.Message
|
import it.vfsfitvnm.vimusic.ui.components.Message
|
||||||
@@ -70,7 +68,7 @@ fun IntentUriScreen(uri: Uri) {
|
|||||||
val menuState = LocalMenuState.current
|
val menuState = LocalMenuState.current
|
||||||
val colorPalette = LocalColorPalette.current
|
val colorPalette = LocalColorPalette.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val player = LocalYoutubePlayer.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val shimmer = rememberShimmer(shimmerBounds = ShimmerBounds.Window)
|
val shimmer = rememberShimmer(shimmerBounds = ShimmerBounds.Window)
|
||||||
@@ -164,14 +162,13 @@ fun IntentUriScreen(uri: Uri) {
|
|||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon = R.drawable.time,
|
icon = R.drawable.time,
|
||||||
text = "Enqueue",
|
text = "Enqueue",
|
||||||
enabled = player?.playbackState == Player.STATE_READY,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
menuState.hide()
|
menuState.hide()
|
||||||
|
|
||||||
items.valueOrNull
|
items.valueOrNull
|
||||||
?.map(YouTube.Item.Song::asMediaItem)
|
?.map(YouTube.Item.Song::asMediaItem)
|
||||||
?.let { mediaItems ->
|
?.let { mediaItems ->
|
||||||
player?.mediaController?.enqueue(
|
binder?.player?.enqueue(
|
||||||
mediaItems
|
mediaItems
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -238,10 +235,8 @@ fun IntentUriScreen(uri: Uri) {
|
|||||||
song = item,
|
song = item,
|
||||||
thumbnailSizePx = density.run { 54.dp.roundToPx() },
|
thumbnailSizePx = density.run { 54.dp.roundToPx() },
|
||||||
onClick = {
|
onClick = {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
binder?.player?.forcePlayAtIndex(currentItems.value.map(YouTube.Item.Song::asMediaItem), index)
|
||||||
it.forcePlayAtIndex(currentItems.value.map(YouTube.Item.Song::asMediaItem), index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens
|
package it.vfsfitvnm.vimusic.ui.screens
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -22,16 +21,15 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.media3.common.Player
|
|
||||||
import it.vfsfitvnm.reordering.rememberReorderingState
|
import it.vfsfitvnm.reordering.rememberReorderingState
|
||||||
import it.vfsfitvnm.reordering.verticalDragAfterLongPressToReorder
|
import it.vfsfitvnm.reordering.verticalDragAfterLongPressToReorder
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
||||||
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
||||||
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
||||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.*
|
import it.vfsfitvnm.vimusic.ui.components.themed.*
|
||||||
@@ -76,7 +74,7 @@ fun LocalPlaylistScreen(
|
|||||||
val hapticFeedback = LocalHapticFeedback.current
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
val menuState = LocalMenuState.current
|
val menuState = LocalMenuState.current
|
||||||
|
|
||||||
val player = LocalYoutubePlayer.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val colorPalette = LocalColorPalette.current
|
val colorPalette = LocalColorPalette.current
|
||||||
val typography = LocalTypography.current
|
val typography = LocalTypography.current
|
||||||
|
|
||||||
@@ -163,14 +161,10 @@ fun LocalPlaylistScreen(
|
|||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon = R.drawable.time,
|
icon = R.drawable.time,
|
||||||
text = "Enqueue",
|
text = "Enqueue",
|
||||||
enabled = playlistWithSongs.songs.isNotEmpty() && player?.playbackState == Player.STATE_READY,
|
enabled = playlistWithSongs.songs.isNotEmpty(),
|
||||||
onClick = {
|
onClick = {
|
||||||
menuState.hide()
|
menuState.hide()
|
||||||
player?.mediaController?.enqueue(
|
binder?.player?.enqueue(playlistWithSongs.songs.map(SongWithInfo::asMediaItem))
|
||||||
playlistWithSongs.songs.map(
|
|
||||||
SongWithInfo::asMediaItem
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -234,10 +228,8 @@ fun LocalPlaylistScreen(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
binder?.player?.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem).shuffled())
|
||||||
it.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem).shuffled())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||||
.background(
|
.background(
|
||||||
@@ -254,10 +246,8 @@ fun LocalPlaylistScreen(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
binder?.player?.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem))
|
||||||
it.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||||
.background(
|
.background(
|
||||||
@@ -280,10 +270,8 @@ fun LocalPlaylistScreen(
|
|||||||
song = song,
|
song = song,
|
||||||
thumbnailSize = thumbnailSize,
|
thumbnailSize = thumbnailSize,
|
||||||
onClick = {
|
onClick = {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
binder?.player?.forcePlayAtIndex(playlistWithSongs.songs.map(SongWithInfo::asMediaItem), index)
|
||||||
it.forcePlayAtIndex(playlistWithSongs.songs.map(SongWithInfo::asMediaItem), index)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
menuContent = {
|
menuContent = {
|
||||||
InPlaylistMediaItemMenu(
|
InPlaylistMediaItemMenu(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens
|
package it.vfsfitvnm.vimusic.ui.screens
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -25,17 +24,16 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.media3.common.Player
|
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.valentinilk.shimmer.shimmer
|
import com.valentinilk.shimmer.shimmer
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||||
import it.vfsfitvnm.vimusic.internal
|
import it.vfsfitvnm.vimusic.internal
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
||||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
|
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
||||||
@@ -87,7 +85,8 @@ fun PlaylistOrAlbumScreen(
|
|||||||
host {
|
host {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val player = LocalYoutubePlayer.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
val colorPalette = LocalColorPalette.current
|
val colorPalette = LocalColorPalette.current
|
||||||
val typography = LocalTypography.current
|
val typography = LocalTypography.current
|
||||||
val menuState = LocalMenuState.current
|
val menuState = LocalMenuState.current
|
||||||
@@ -142,7 +141,6 @@ fun PlaylistOrAlbumScreen(
|
|||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon = R.drawable.time,
|
icon = R.drawable.time,
|
||||||
text = "Enqueue",
|
text = "Enqueue",
|
||||||
enabled = player?.playbackState == Player.STATE_READY,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
menuState.hide()
|
menuState.hide()
|
||||||
playlistOrAlbum.valueOrNull?.let { album ->
|
playlistOrAlbum.valueOrNull?.let { album ->
|
||||||
@@ -151,7 +149,7 @@ fun PlaylistOrAlbumScreen(
|
|||||||
song.toMediaItem(browseId, album)
|
song.toMediaItem(browseId, album)
|
||||||
}
|
}
|
||||||
?.let { mediaItems ->
|
?.let { mediaItems ->
|
||||||
player?.mediaController?.enqueue(
|
binder?.player?.enqueue(
|
||||||
mediaItems
|
mediaItems
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -282,15 +280,13 @@ fun PlaylistOrAlbumScreen(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
|
||||||
playlistOrAlbum.items
|
playlistOrAlbum.items
|
||||||
?.shuffled()
|
?.shuffled()
|
||||||
?.mapNotNull { song ->
|
?.mapNotNull { song ->
|
||||||
song.toMediaItem(browseId, playlistOrAlbum)
|
song.toMediaItem(browseId, playlistOrAlbum)
|
||||||
}?.let { mediaItems ->
|
}?.let { mediaItems ->
|
||||||
it.forcePlayFromBeginning(mediaItems)
|
binder?.player?.forcePlayFromBeginning(mediaItems)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||||
@@ -308,13 +304,11 @@ fun PlaylistOrAlbumScreen(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
|
||||||
playlistOrAlbum.items?.mapNotNull { song ->
|
playlistOrAlbum.items?.mapNotNull { song ->
|
||||||
song.toMediaItem(browseId, playlistOrAlbum)
|
song.toMediaItem(browseId, playlistOrAlbum)
|
||||||
}?.let { mediaItems ->
|
}?.let { mediaItems ->
|
||||||
it.forcePlayFromBeginning(mediaItems)
|
binder?.player?.forcePlayFromBeginning(mediaItems)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||||
@@ -340,13 +334,11 @@ fun PlaylistOrAlbumScreen(
|
|||||||
authors = (song.authors ?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name },
|
authors = (song.authors ?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name },
|
||||||
durationText = song.durationText,
|
durationText = song.durationText,
|
||||||
onClick = {
|
onClick = {
|
||||||
player?.mediaController?.let {
|
binder?.stopRadio()
|
||||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
|
||||||
playlistOrAlbum.valueOrNull?.items?.mapNotNull { song ->
|
playlistOrAlbum.valueOrNull?.items?.mapNotNull { song ->
|
||||||
song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!)
|
song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!)
|
||||||
}?.let { mediaItems ->
|
}?.let { mediaItems ->
|
||||||
it.forcePlayAtIndex(mediaItems, index)
|
binder?.player?.forcePlayAtIndex(mediaItems, index)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startContent = {
|
startContent = {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens
|
package it.vfsfitvnm.vimusic.ui.screens
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -26,16 +25,15 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.valentinilk.shimmer.Shimmer
|
import com.valentinilk.shimmer.Shimmer
|
||||||
import com.valentinilk.shimmer.ShimmerBounds
|
import com.valentinilk.shimmer.ShimmerBounds
|
||||||
import com.valentinilk.shimmer.rememberShimmer
|
import com.valentinilk.shimmer.rememberShimmer
|
||||||
import com.valentinilk.shimmer.shimmer
|
import com.valentinilk.shimmer.shimmer
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||||
import it.vfsfitvnm.vimusic.services.StartRadioCommand
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.*
|
import it.vfsfitvnm.vimusic.ui.components.*
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
@@ -45,7 +43,6 @@ import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|||||||
import it.vfsfitvnm.vimusic.utils.*
|
import it.vfsfitvnm.vimusic.utils.*
|
||||||
import it.vfsfitvnm.youtubemusic.Outcome
|
import it.vfsfitvnm.youtubemusic.Outcome
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -60,7 +57,7 @@ fun SearchResultScreen(
|
|||||||
val colorPalette = LocalColorPalette.current
|
val colorPalette = LocalColorPalette.current
|
||||||
val typography = LocalTypography.current
|
val typography = LocalTypography.current
|
||||||
val preferences = LocalPreferences.current
|
val preferences = LocalPreferences.current
|
||||||
val player = LocalYoutubePlayer.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
@@ -218,13 +215,13 @@ fun SearchResultScreen(
|
|||||||
is YouTube.Item.Album -> playlistOrAlbumRoute(item.info.endpoint!!.browseId)
|
is YouTube.Item.Album -> playlistOrAlbumRoute(item.info.endpoint!!.browseId)
|
||||||
is YouTube.Item.Artist -> artistRoute(item.info.endpoint!!.browseId)
|
is YouTube.Item.Artist -> artistRoute(item.info.endpoint!!.browseId)
|
||||||
is YouTube.Item.Playlist -> playlistOrAlbumRoute(item.info.endpoint!!.browseId)
|
is YouTube.Item.Playlist -> playlistOrAlbumRoute(item.info.endpoint!!.browseId)
|
||||||
is YouTube.Item.Song -> player?.mediaController?.let {
|
is YouTube.Item.Song -> {
|
||||||
it.forcePlay(item.asMediaItem)
|
binder?.player?.forcePlay(item.asMediaItem)
|
||||||
it.sendCustomCommand(StartRadioCommand, item.info.endpoint.asBundle)
|
binder?.startRadio(item.info.endpoint)
|
||||||
}
|
}
|
||||||
is YouTube.Item.Video -> player?.mediaController?.let {
|
is YouTube.Item.Video -> {
|
||||||
it.forcePlay(item.asMediaItem)
|
binder?.player?.forcePlay(item.asMediaItem)
|
||||||
it.sendCustomCommand(StartRadioCommand, item.info.endpoint.asBundle)
|
binder?.startRadio(item.info.endpoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -573,13 +570,3 @@ fun SmallArtistItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val NavigationEndpoint.Endpoint.Watch?.asBundle: Bundle
|
|
||||||
get() = this?.let {
|
|
||||||
bundleOf(
|
|
||||||
"videoId" to videoId,
|
|
||||||
"playlistId" to playlistId,
|
|
||||||
"playlistSetVideoId" to playlistSetVideoId,
|
|
||||||
"params" to params,
|
|
||||||
)
|
|
||||||
} ?: Bundle.EMPTY
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.settings
|
package it.vfsfitvnm.vimusic.ui.screens.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
@@ -16,19 +15,17 @@ import androidx.compose.ui.unit.dp
|
|||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.annotation.ExperimentalCoilApi
|
import coil.annotation.ExperimentalCoilApi
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.services.GetCacheSizeCommand
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.SeekBar
|
import it.vfsfitvnm.vimusic.ui.components.SeekBar
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.*
|
import it.vfsfitvnm.vimusic.ui.screens.*
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
|
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
|
||||||
import it.vfsfitvnm.vimusic.utils.LocalPreferences
|
import it.vfsfitvnm.vimusic.utils.LocalPreferences
|
||||||
import it.vfsfitvnm.vimusic.utils.LocalYoutubePlayer
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.guava.await
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalCoilApi::class)
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
@@ -58,9 +55,7 @@ fun OtherSettingsScreen() {
|
|||||||
val colorPalette = LocalColorPalette.current
|
val colorPalette = LocalColorPalette.current
|
||||||
val typography = LocalTypography.current
|
val typography = LocalTypography.current
|
||||||
val preferences = LocalPreferences.current
|
val preferences = LocalPreferences.current
|
||||||
val mediaController = LocalYoutubePlayer.current?.mediaController
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
val coilDiskCache = Coil.imageLoader(context).diskCache
|
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -97,7 +92,7 @@ fun OtherSettingsScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
coilDiskCache?.let { diskCache ->
|
Coil.imageLoader(context).diskCache?.let { diskCache ->
|
||||||
var diskCacheSize by remember(diskCache) {
|
var diskCacheSize by remember(diskCache) {
|
||||||
mutableStateOf(diskCache.size)
|
mutableStateOf(diskCache.size)
|
||||||
}
|
}
|
||||||
@@ -168,9 +163,11 @@ fun OtherSettingsScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaController?.let { mediaController ->
|
binder?.cache?.let { cache ->
|
||||||
val diskCacheSize by produceState(initialValue = 0L) {
|
val diskCacheSize by remember {
|
||||||
value = mediaController.sendCustomCommand(GetCacheSizeCommand, Bundle.EMPTY).await().extras.getLong("cacheSize")
|
derivedStateOf {
|
||||||
|
cache.cacheSpace
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var scrubbingDiskCacheMaxSize by remember {
|
var scrubbingDiskCacheMaxSize by remember {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package it.vfsfitvnm.vimusic.ui.screens.settings
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.audiofx.AudioEffect
|
import android.media.audiofx.AudioEffect
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -18,12 +17,9 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.media3.common.C
|
|
||||||
import androidx.media3.session.SessionResult
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.services.*
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.ChunkyButton
|
import it.vfsfitvnm.vimusic.ui.components.ChunkyButton
|
||||||
import it.vfsfitvnm.vimusic.ui.components.Pager
|
import it.vfsfitvnm.vimusic.ui.components.Pager
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
||||||
@@ -32,10 +28,10 @@ import it.vfsfitvnm.vimusic.ui.components.themed.DefaultDialog
|
|||||||
import it.vfsfitvnm.vimusic.ui.screens.*
|
import it.vfsfitvnm.vimusic.ui.screens.*
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
|
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
|
||||||
import it.vfsfitvnm.vimusic.utils.*
|
import it.vfsfitvnm.vimusic.utils.LocalPreferences
|
||||||
import kotlinx.coroutines.delay
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
import kotlinx.coroutines.guava.await
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@@ -64,39 +60,23 @@ fun PlayerSettingsScreen() {
|
|||||||
val colorPalette = LocalColorPalette.current
|
val colorPalette = LocalColorPalette.current
|
||||||
val typography = LocalTypography.current
|
val typography = LocalTypography.current
|
||||||
val preferences = LocalPreferences.current
|
val preferences = LocalPreferences.current
|
||||||
val mediaController = LocalYoutubePlayer.current?.mediaController
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
val activityResultLauncher =
|
val activityResultLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
val audioSessionId by produceState(initialValue = C.AUDIO_SESSION_ID_UNSET, mediaController) {
|
val audioSessionId = remember(binder) {
|
||||||
val hasEqualizer = context.packageManager.resolveActivity(
|
val hasEqualizer = context.packageManager.resolveActivity(
|
||||||
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL),
|
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL),
|
||||||
0
|
0
|
||||||
) != null
|
) != null
|
||||||
|
|
||||||
if (hasEqualizer) {
|
if (hasEqualizer) binder?.player?.audioSessionId else null
|
||||||
value =
|
|
||||||
mediaController?.sendCustomCommand(GetAudioSessionIdCommand, Bundle.EMPTY)
|
|
||||||
?.await()?.extras?.getInt("audioSessionId", C.AUDIO_SESSION_ID_UNSET)
|
|
||||||
?: C.AUDIO_SESSION_ID_UNSET
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var sleepTimerMillisLeft by remember {
|
val sleepTimerMillisLeft by (binder?.sleepTimerMillisLeft
|
||||||
mutableStateOf<Long?>(null)
|
?: flowOf(null)).collectAsState(initial = null)
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(mediaController) {
|
|
||||||
while (isActive) {
|
|
||||||
sleepTimerMillisLeft =
|
|
||||||
mediaController?.syncCommand(GetSleepTimerMillisLeftCommand)
|
|
||||||
?.takeIf { it.resultCode == SessionResult.RESULT_SUCCESS }
|
|
||||||
?.extras?.getLong("millisLeft")
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isShowingSleepTimerDialog by remember {
|
var isShowingSleepTimerDialog by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
@@ -112,8 +92,7 @@ fun PlayerSettingsScreen() {
|
|||||||
isShowingSleepTimerDialog = false
|
isShowingSleepTimerDialog = false
|
||||||
},
|
},
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
mediaController?.syncCommand(CancelSleepTimerCommand)
|
binder?.cancelSleepTimer()
|
||||||
sleepTimerMillisLeft = null
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -199,14 +178,7 @@ fun PlayerSettingsScreen() {
|
|||||||
shape = RoundedCornerShape(36.dp),
|
shape = RoundedCornerShape(36.dp),
|
||||||
isEnabled = hours > 0 || minutes > 0,
|
isEnabled = hours > 0 || minutes > 0,
|
||||||
onClick = {
|
onClick = {
|
||||||
mediaController?.syncCommand(
|
binder?.startSleepTimer((hours * 60 + minutes * 15) * 60 * 1000L)
|
||||||
SetSleepTimerCommand,
|
|
||||||
bundleOf("delayMillis" to (hours * 60 + minutes * 15) * 60 * 1000L)
|
|
||||||
)
|
|
||||||
sleepTimerMillisLeft =
|
|
||||||
mediaController?.syncCommand(GetSleepTimerMillisLeftCommand)?.extras?.getLong(
|
|
||||||
"millisLeft"
|
|
||||||
)
|
|
||||||
isShowingSleepTimerDialog = false
|
isShowingSleepTimerDialog = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -253,10 +225,7 @@ fun PlayerSettingsScreen() {
|
|||||||
text = "Skip silent parts during playback",
|
text = "Skip silent parts during playback",
|
||||||
isChecked = preferences.skipSilence,
|
isChecked = preferences.skipSilence,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
mediaController?.sendCustomCommand(
|
binder?.player?.skipSilenceEnabled = it
|
||||||
SetSkipSilenceCommand,
|
|
||||||
bundleOf("skipSilence" to it)
|
|
||||||
)
|
|
||||||
preferences.skipSilence = it
|
preferences.skipSilence = it
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -284,7 +253,7 @@ fun PlayerSettingsScreen() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
isEnabled = audioSessionId != C.AUDIO_SESSION_ID_UNSET && audioSessionId != AudioEffect.ERROR_BAD_VALUE
|
isEnabled = audioSessionId != null
|
||||||
)
|
)
|
||||||
|
|
||||||
SettingsEntry(
|
SettingsEntry(
|
||||||
|
|||||||
@@ -6,14 +6,15 @@ import androidx.compose.animation.fadeIn
|
|||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
@@ -25,33 +26,29 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import com.valentinilk.shimmer.ShimmerBounds
|
import com.valentinilk.shimmer.ShimmerBounds
|
||||||
import com.valentinilk.shimmer.rememberShimmer
|
import com.valentinilk.shimmer.rememberShimmer
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.Error
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.MusicBars
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.SmallSongItemShimmer
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LightColorPalette
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
|
||||||
import it.vfsfitvnm.vimusic.utils.LocalYoutubePlayer
|
|
||||||
import it.vfsfitvnm.vimusic.utils.YoutubePlayer
|
|
||||||
import it.vfsfitvnm.reordering.rememberReorderingState
|
import it.vfsfitvnm.reordering.rememberReorderingState
|
||||||
import it.vfsfitvnm.reordering.verticalDragAfterLongPressToReorder
|
import it.vfsfitvnm.reordering.verticalDragAfterLongPressToReorder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||||
import it.vfsfitvnm.youtubemusic.Outcome
|
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
|
||||||
import kotlinx.coroutines.launch
|
import it.vfsfitvnm.vimusic.ui.components.MusicBars
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LightColorPalette
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
||||||
|
import it.vfsfitvnm.vimusic.utils.PlayerState
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun CurrentPlaylistView(
|
fun CurrentPlaylistView(
|
||||||
|
player: Player?,
|
||||||
|
playerState: PlayerState?,
|
||||||
layoutState: BottomSheetState,
|
layoutState: BottomSheetState,
|
||||||
onGlobalRouteEmitted: () -> Unit,
|
onGlobalRouteEmitted: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val player = LocalYoutubePlayer.current
|
|
||||||
val colorPalette = LocalColorPalette.current
|
val colorPalette = LocalColorPalette.current
|
||||||
|
|
||||||
val thumbnailSize = remember {
|
val thumbnailSize = remember {
|
||||||
@@ -61,30 +58,26 @@ fun CurrentPlaylistView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val isPaused by derivedStateOf {
|
val isPaused by derivedStateOf {
|
||||||
player?.playbackState == Player.STATE_ENDED || player?.playWhenReady == false
|
playerState?.playbackState == Player.STATE_ENDED || playerState?.playWhenReady == false
|
||||||
}
|
}
|
||||||
|
|
||||||
val shimmer = rememberShimmer(shimmerBounds = ShimmerBounds.Window)
|
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val lazyListState =
|
val lazyListState =
|
||||||
rememberLazyListState(initialFirstVisibleItemIndex = player?.mediaItemIndex ?: 0)
|
rememberLazyListState(initialFirstVisibleItemIndex = playerState?.mediaItemIndex ?: 0)
|
||||||
|
|
||||||
val reorderingState = rememberReorderingState(player?.mediaItems ?: emptyList())
|
val reorderingState = rememberReorderingState(playerState?.mediaItems ?: emptyList())
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.nestedScroll(remember {
|
.nestedScroll(remember {
|
||||||
layoutState.nestedScrollConnection(player?.mediaItemIndex == 0)
|
layoutState.nestedScrollConnection(playerState?.mediaItemIndex == 0)
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = player?.mediaItems ?: emptyList()
|
items = playerState?.mediaItems ?: emptyList()
|
||||||
) { index, mediaItem ->
|
) { index, mediaItem ->
|
||||||
val isPlayingThisMediaItem by derivedStateOf {
|
val isPlayingThisMediaItem by derivedStateOf {
|
||||||
player?.mediaItemIndex == index
|
playerState?.mediaItemIndex == index
|
||||||
}
|
}
|
||||||
|
|
||||||
SongItem(
|
SongItem(
|
||||||
@@ -93,13 +86,13 @@ fun CurrentPlaylistView(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (isPlayingThisMediaItem) {
|
if (isPlayingThisMediaItem) {
|
||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
player?.mediaController?.play()
|
player?.play()
|
||||||
} else {
|
} else {
|
||||||
player?.mediaController?.pause()
|
player?.pause()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
player?.mediaController?.playWhenReady = true
|
player?.playWhenReady = true
|
||||||
player?.mediaController?.seekToDefaultPosition(index)
|
player?.seekToDefaultPosition(index)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
menuContent = {
|
menuContent = {
|
||||||
@@ -151,7 +144,7 @@ fun CurrentPlaylistView(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDragEnd = { reachedIndex ->
|
onDragEnd = { reachedIndex ->
|
||||||
player?.mediaController?.moveMediaItem(index, reachedIndex)
|
player?.moveMediaItem(index, reachedIndex)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -165,7 +158,7 @@ fun CurrentPlaylistView(
|
|||||||
// SideEffect {
|
// SideEffect {
|
||||||
// coroutineScope.launch {
|
// coroutineScope.launch {
|
||||||
// YoutubePlayer.Radio.process(
|
// YoutubePlayer.Radio.process(
|
||||||
// player.mediaController,
|
// playerState.mediaController,
|
||||||
// force = true
|
// force = true
|
||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
@@ -194,7 +187,7 @@ fun CurrentPlaylistView(
|
|||||||
// error = nextContinuation.error,
|
// error = nextContinuation.error,
|
||||||
// onRetry = {
|
// onRetry = {
|
||||||
// coroutineScope.launch {
|
// coroutineScope.launch {
|
||||||
// YoutubePlayer.Radio.process(player.mediaController, force = true)
|
// YoutubePlayer.Radio.process(playerState.mediaController, force = true)
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.media3.common.Player
|
||||||
import it.vfsfitvnm.route.Route
|
import it.vfsfitvnm.route.Route
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.route.empty
|
import it.vfsfitvnm.route.empty
|
||||||
@@ -44,13 +45,13 @@ import kotlinx.coroutines.withContext
|
|||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun PlayerBottomSheet(
|
fun PlayerBottomSheet(
|
||||||
|
player: Player?,
|
||||||
|
playerState: PlayerState?,
|
||||||
layoutState: BottomSheetState,
|
layoutState: BottomSheetState,
|
||||||
song: Song?,
|
song: Song?,
|
||||||
onGlobalRouteEmitted: () -> Unit,
|
onGlobalRouteEmitted: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val player = LocalYoutubePlayer.current ?: return
|
|
||||||
|
|
||||||
val colorPalette = LocalColorPalette.current
|
val colorPalette = LocalColorPalette.current
|
||||||
val typography = LocalTypography.current
|
val typography = LocalTypography.current
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ fun PlayerBottomSheet(
|
|||||||
|
|
||||||
var route by rememberRoute()
|
var route by rememberRoute()
|
||||||
|
|
||||||
var nextOutcome by remember(player.mediaItem!!.mediaId) {
|
var nextOutcome by remember(playerState?.mediaItem?.mediaId) {
|
||||||
mutableStateOf<Outcome<YouTube.NextResult>>(Outcome.Initial)
|
mutableStateOf<Outcome<YouTube.NextResult>>(Outcome.Initial)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,13 +184,19 @@ fun PlayerBottomSheet(
|
|||||||
coroutineScope.launch(Dispatchers.Main) {
|
coroutineScope.launch(Dispatchers.Main) {
|
||||||
lyricsOutcome = Outcome.Loading
|
lyricsOutcome = Outcome.Loading
|
||||||
|
|
||||||
|
val mediaItem = player?.currentMediaItem!!
|
||||||
|
|
||||||
if (nextOutcome.isEvaluable) {
|
if (nextOutcome.isEvaluable) {
|
||||||
nextOutcome = Outcome.Loading
|
nextOutcome = Outcome.Loading
|
||||||
|
|
||||||
|
|
||||||
|
val mediaItemIndex = player.currentMediaItemIndex
|
||||||
|
|
||||||
nextOutcome = withContext(Dispatchers.IO) {
|
nextOutcome = withContext(Dispatchers.IO) {
|
||||||
YouTube.next(
|
YouTube.next(
|
||||||
player.mediaItem!!.mediaId,
|
mediaItem.mediaId,
|
||||||
player.mediaItem!!.mediaMetadata.extras?.getString("playlistId"),
|
mediaItem.mediaMetadata.extras?.getString("playlistId"),
|
||||||
player.mediaItemIndex
|
mediaItemIndex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +207,7 @@ fun PlayerBottomSheet(
|
|||||||
lyrics ?: ""
|
lyrics ?: ""
|
||||||
}.map { lyrics ->
|
}.map { lyrics ->
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
(song ?: player.mediaItem?.let(Database::insert))?.let {
|
(song ?: mediaItem.let(Database::insert)).let {
|
||||||
Database.update(it.copy(lyrics = lyrics))
|
Database.update(it.copy(lyrics = lyrics))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +216,7 @@ fun PlayerBottomSheet(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSearchOnline = {
|
onSearchOnline = {
|
||||||
player.mediaMetadata.let {
|
player?.mediaMetadata?.let {
|
||||||
context.startActivity(Intent(Intent.ACTION_WEB_SEARCH).apply {
|
context.startActivity(Intent(Intent.ACTION_WEB_SEARCH).apply {
|
||||||
putExtra(
|
putExtra(
|
||||||
SearchManager.QUERY,
|
SearchManager.QUERY,
|
||||||
@@ -219,8 +226,9 @@ fun PlayerBottomSheet(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLyricsUpdate = { lyrics ->
|
onLyricsUpdate = { lyrics ->
|
||||||
|
val mediaItem = player?.currentMediaItem
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
(song ?: player.mediaItem?.let(Database::insert))?.let {
|
(song ?: mediaItem?.let(Database::insert))?.let {
|
||||||
Database.update(it.copy(lyrics = lyrics))
|
Database.update(it.copy(lyrics = lyrics))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,6 +238,8 @@ fun PlayerBottomSheet(
|
|||||||
|
|
||||||
host {
|
host {
|
||||||
CurrentPlaylistView(
|
CurrentPlaylistView(
|
||||||
|
player = player,
|
||||||
|
playerState = playerState,
|
||||||
layoutState = layoutState,
|
layoutState = layoutState,
|
||||||
onGlobalRouteEmitted = onGlobalRouteEmitted,
|
onGlobalRouteEmitted = onGlobalRouteEmitted,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -30,15 +30,16 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.datasource.cache.Cache
|
||||||
|
import androidx.media3.datasource.cache.CacheSpan
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||||
import it.vfsfitvnm.vimusic.services.GetSongCacheSizeCommand
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.*
|
import it.vfsfitvnm.vimusic.ui.components.*
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu
|
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette
|
import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette
|
||||||
@@ -67,12 +68,15 @@ fun PlayerView(
|
|||||||
val typography = LocalTypography.current
|
val typography = LocalTypography.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val player = LocalYoutubePlayer.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val player = binder?.player
|
||||||
|
val playerState = rememberYoutubePlayer(player)
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
player?.mediaItem ?: return
|
playerState?.mediaItem ?: return
|
||||||
|
|
||||||
val smallThumbnailSize = remember {
|
val smallThumbnailSize = remember {
|
||||||
density.run { 64.dp.roundToPx() }
|
density.run { 64.dp.roundToPx() }
|
||||||
@@ -108,7 +112,7 @@ fun PlayerView(
|
|||||||
y = 1.dp.toPx()
|
y = 1.dp.toPx()
|
||||||
),
|
),
|
||||||
end = Offset(
|
end = Offset(
|
||||||
x = ((size.width - offset) * player.progress) + offset,
|
x = ((size.width - offset) * playerState.progress) + offset,
|
||||||
y = 1.dp.toPx()
|
y = 1.dp.toPx()
|
||||||
),
|
),
|
||||||
strokeWidth = 2.dp.toPx()
|
strokeWidth = 2.dp.toPx()
|
||||||
@@ -116,7 +120,7 @@ fun PlayerView(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = player.mediaMetadata.artworkUri.thumbnail(smallThumbnailSize),
|
model = playerState.mediaMetadata.artworkUri.thumbnail(smallThumbnailSize),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -129,13 +133,13 @@ fun PlayerView(
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = player.mediaMetadata.title?.toString() ?: "",
|
text = playerState.mediaMetadata.title?.toString() ?: "",
|
||||||
style = typography.xs.semiBold,
|
style = typography.xs.semiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = player.mediaMetadata.artist?.toString() ?: "",
|
text = playerState.mediaMetadata.artist?.toString() ?: "",
|
||||||
style = typography.xs,
|
style = typography.xs,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@@ -143,16 +147,16 @@ fun PlayerView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
player.playbackState == Player.STATE_ENDED || !player.playWhenReady -> Image(
|
playerState.playbackState == Player.STATE_ENDED || !playerState.playWhenReady -> Image(
|
||||||
painter = painterResource(R.drawable.play),
|
painter = painterResource(R.drawable.play),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
if (player.playbackState == Player.STATE_IDLE) {
|
if (playerState.playbackState == Player.STATE_IDLE) {
|
||||||
player.mediaController.prepare()
|
player?.prepare()
|
||||||
}
|
}
|
||||||
player.mediaController.play()
|
player?.play()
|
||||||
}
|
}
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@@ -164,7 +168,7 @@ fun PlayerView(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player.mediaController.pause()
|
player?.pause()
|
||||||
}
|
}
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@@ -175,8 +179,9 @@ fun PlayerView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val song by remember(player.mediaItem?.mediaId) {
|
val song by remember(playerState.mediaItem?.mediaId) {
|
||||||
player.mediaItem?.mediaId?.let(Database::songFlow)?.distinctUntilChanged() ?: flowOf(
|
playerState.mediaItem?.mediaId?.let(Database::songFlow)?.distinctUntilChanged()
|
||||||
|
?: flowOf(
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}.collectAsState(initial = null, context = Dispatchers.IO)
|
}.collectAsState(initial = null, context = Dispatchers.IO)
|
||||||
@@ -192,7 +197,7 @@ fun PlayerView(
|
|||||||
.padding(bottom = 72.dp)
|
.padding(bottom = 72.dp)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
var scrubbingPosition by remember(player.mediaItemIndex) {
|
var scrubbingPosition by remember(playerState.mediaItemIndex) {
|
||||||
mutableStateOf<Long?>(null)
|
mutableStateOf<Long?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,8 +216,8 @@ fun PlayerView(
|
|||||||
.clickable {
|
.clickable {
|
||||||
menuState.display {
|
menuState.display {
|
||||||
QueuedMediaItemMenu(
|
QueuedMediaItemMenu(
|
||||||
mediaItem = player.mediaItem ?: MediaItem.EMPTY,
|
mediaItem = playerState.mediaItem ?: MediaItem.EMPTY,
|
||||||
indexInQueue = player.mediaItemIndex,
|
indexInQueue = playerState.mediaItemIndex,
|
||||||
onDismiss = menuState::hide,
|
onDismiss = menuState::hide,
|
||||||
onGlobalRouteEmitted = layoutState.collapse
|
onGlobalRouteEmitted = layoutState.collapse
|
||||||
)
|
)
|
||||||
@@ -223,9 +228,9 @@ fun PlayerView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.error == null) {
|
if (playerState.error == null) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = player.mediaItemIndex,
|
targetState = playerState.mediaItemIndex,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
val slideDirection =
|
val slideDirection =
|
||||||
if (targetState > initialState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right
|
if (targetState > initialState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right
|
||||||
@@ -240,7 +245,7 @@ fun PlayerView(
|
|||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
) {
|
) {
|
||||||
val artworkUri = remember(it) {
|
val artworkUri = remember(it) {
|
||||||
player.mediaController.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail(
|
player?.getMediaItemAt(it)?.mediaMetadata?.artworkUri.thumbnail(
|
||||||
thumbnailSizePx
|
thumbnailSizePx
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -273,13 +278,37 @@ fun PlayerView(
|
|||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
) {
|
) {
|
||||||
val cachedPercentage = remember(song?.contentLength) {
|
var cachedBytes by remember(song?.id) {
|
||||||
song?.contentLength?.let { contentLength ->
|
mutableStateOf(binder?.cache?.getCachedBytes(song?.id ?: "", 0, -1) ?: 0L)
|
||||||
player.mediaController.syncCommand(
|
}
|
||||||
GetSongCacheSizeCommand,
|
|
||||||
bundleOf("videoId" to song?.id)
|
DisposableEffect(song?.id) {
|
||||||
).extras.getLong("cacheSize").toFloat() / contentLength * 100
|
val listener = object : Cache.Listener {
|
||||||
}?.roundToInt() ?: 0
|
override fun onSpanAdded(cache: Cache, span: CacheSpan) {
|
||||||
|
cachedBytes += span.length
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSpanRemoved(cache: Cache, span: CacheSpan) {
|
||||||
|
cachedBytes -= span.length
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSpanTouched(
|
||||||
|
cache: Cache,
|
||||||
|
oldSpan: CacheSpan,
|
||||||
|
newSpan: CacheSpan
|
||||||
|
) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
song?.id?.let { key ->
|
||||||
|
binder?.cache?.addListener(key, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
song?.id?.let { key ->
|
||||||
|
binder?.cache?.removeListener(key, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -321,7 +350,7 @@ fun PlayerView(
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "${player.volume.times(100).roundToInt()}%",
|
text = "${playerState.volume.times(100).roundToInt()}%",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
@@ -332,12 +361,21 @@ fun PlayerView(
|
|||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = song?.contentLength?.let { contentLength ->
|
text = song?.contentLength?.let { contentLength ->
|
||||||
Formatter.formatShortFileSize(context, contentLength)
|
Formatter.formatShortFileSize(
|
||||||
|
context,
|
||||||
|
contentLength
|
||||||
|
)
|
||||||
} ?: "Unknown",
|
} ?: "Unknown",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "$cachedPercentage%",
|
text = buildString {
|
||||||
|
append(Formatter.formatShortFileSize(context, cachedBytes))
|
||||||
|
|
||||||
|
song?.contentLength?.let { contentLenght ->
|
||||||
|
append(" (${(cachedBytes.toFloat() / contentLenght * 100).roundToInt()}%)")
|
||||||
|
}
|
||||||
|
},
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -354,13 +392,17 @@ fun PlayerView(
|
|||||||
onClick = {
|
onClick = {
|
||||||
song?.let { song ->
|
song?.let { song ->
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
YouTube.player(song.id).map { body ->
|
YouTube
|
||||||
|
.player(song.id)
|
||||||
|
.map { body ->
|
||||||
Database.update(
|
Database.update(
|
||||||
song.copy(
|
song.copy(
|
||||||
loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat(),
|
loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat(),
|
||||||
contentLength = body.streamingData?.adaptiveFormats?.findLast { format ->
|
contentLength = body.streamingData?.adaptiveFormats
|
||||||
|
?.findLast { format ->
|
||||||
format.itag == 251 || format.itag == 140
|
format.itag == 251 || format.itag == 140
|
||||||
}?.let(PlayerResponse.StreamingData.AdaptiveFormat::contentLength)
|
}
|
||||||
|
?.let(PlayerResponse.StreamingData.AdaptiveFormat::contentLength)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -387,18 +429,18 @@ fun PlayerView(
|
|||||||
.size(thumbnailSizeDp)
|
.size(thumbnailSizeDp)
|
||||||
) {
|
) {
|
||||||
Error(
|
Error(
|
||||||
error = Outcome.Error.Unhandled(player.error!!),
|
error = Outcome.Error.Unhandled(playerState.error!!),
|
||||||
onRetry = {
|
onRetry = {
|
||||||
player.mediaController.playWhenReady = true
|
player?.playWhenReady = true
|
||||||
player.mediaController.prepare()
|
player?.prepare()
|
||||||
player.error = null
|
playerState.error = null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicText(
|
BasicText(
|
||||||
text = player.mediaMetadata.title?.toString() ?: "",
|
text = playerState.mediaMetadata.title?.toString() ?: "",
|
||||||
style = typography.l.bold,
|
style = typography.l.bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@@ -407,7 +449,7 @@ fun PlayerView(
|
|||||||
)
|
)
|
||||||
|
|
||||||
BasicText(
|
BasicText(
|
||||||
text = player.mediaMetadata.extras?.getStringArrayList("artistNames")
|
text = playerState.mediaMetadata.extras?.getStringArrayList("artistNames")
|
||||||
?.joinToString("") ?: "",
|
?.joinToString("") ?: "",
|
||||||
style = typography.s.semiBold.secondary,
|
style = typography.s.semiBold.secondary,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -417,24 +459,24 @@ fun PlayerView(
|
|||||||
)
|
)
|
||||||
|
|
||||||
SeekBar(
|
SeekBar(
|
||||||
value = scrubbingPosition ?: player.currentPosition,
|
value = scrubbingPosition ?: playerState.currentPosition,
|
||||||
minimumValue = 0,
|
minimumValue = 0,
|
||||||
maximumValue = player.duration,
|
maximumValue = playerState.duration,
|
||||||
onDragStart = {
|
onDragStart = {
|
||||||
scrubbingPosition = it
|
scrubbingPosition = it
|
||||||
},
|
},
|
||||||
onDrag = { delta ->
|
onDrag = { delta ->
|
||||||
scrubbingPosition = if (player.duration != C.TIME_UNSET) {
|
scrubbingPosition = if (playerState.duration != C.TIME_UNSET) {
|
||||||
scrubbingPosition?.plus(delta)?.coerceIn(0, player.duration)
|
scrubbingPosition?.plus(delta)?.coerceIn(0, playerState.duration)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
player.mediaController.seekTo(
|
scrubbingPosition?.let { scrubbingPosition ->
|
||||||
scrubbingPosition ?: player.mediaController.currentPosition
|
player?.seekTo(scrubbingPosition)
|
||||||
)
|
playerState.currentPosition = scrubbingPosition
|
||||||
player.currentPosition = player.mediaController.currentPosition
|
}
|
||||||
scrubbingPosition = null
|
scrubbingPosition = null
|
||||||
},
|
},
|
||||||
color = colorPalette.text,
|
color = colorPalette.text,
|
||||||
@@ -456,16 +498,16 @@ fun PlayerView(
|
|||||||
) {
|
) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = DateUtils.formatElapsedTime(
|
text = DateUtils.formatElapsedTime(
|
||||||
(scrubbingPosition ?: player.currentPosition) / 1000
|
(scrubbingPosition ?: playerState.currentPosition) / 1000
|
||||||
),
|
),
|
||||||
style = typography.xxs.semiBold,
|
style = typography.xxs.semiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (player.duration != C.TIME_UNSET) {
|
if (playerState.duration != C.TIME_UNSET) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = DateUtils.formatElapsedTime(player.duration / 1000),
|
text = DateUtils.formatElapsedTime(playerState.duration / 1000),
|
||||||
style = typography.xxs.semiBold,
|
style = typography.xxs.semiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@@ -488,7 +530,7 @@ fun PlayerView(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
(song ?: player.mediaItem?.let(Database::insert))?.let {
|
(song ?: playerState.mediaItem?.let(Database::insert))?.let {
|
||||||
Database.update(it.toggleLike())
|
Database.update(it.toggleLike())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -503,24 +545,24 @@ fun PlayerView(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player.mediaController.seekToPrevious()
|
player?.seekToPrevious()
|
||||||
}
|
}
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.size(32.dp)
|
.size(32.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
player.playbackState == Player.STATE_ENDED || !player.playWhenReady -> Image(
|
playerState.playbackState == Player.STATE_ENDED || !playerState.playWhenReady -> Image(
|
||||||
painter = painterResource(R.drawable.play_circle),
|
painter = painterResource(R.drawable.play_circle),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
if (player.playbackState == Player.STATE_IDLE) {
|
if (player?.playbackState == Player.STATE_IDLE) {
|
||||||
player.mediaController.prepare()
|
player.prepare()
|
||||||
}
|
}
|
||||||
|
|
||||||
player.mediaController.play()
|
player?.play()
|
||||||
}
|
}
|
||||||
.size(64.dp)
|
.size(64.dp)
|
||||||
)
|
)
|
||||||
@@ -530,7 +572,7 @@ fun PlayerView(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player.mediaController.pause()
|
player?.pause()
|
||||||
}
|
}
|
||||||
.size(64.dp)
|
.size(64.dp)
|
||||||
)
|
)
|
||||||
@@ -542,7 +584,7 @@ fun PlayerView(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player.mediaController.seekToNext()
|
player?.seekToNext()
|
||||||
}
|
}
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.size(32.dp)
|
.size(32.dp)
|
||||||
@@ -551,7 +593,7 @@ fun PlayerView(
|
|||||||
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
if (player.repeatMode == Player.REPEAT_MODE_ONE) {
|
if (playerState.repeatMode == Player.REPEAT_MODE_ONE) {
|
||||||
R.drawable.repeat_one
|
R.drawable.repeat_one
|
||||||
} else {
|
} else {
|
||||||
R.drawable.repeat
|
R.drawable.repeat
|
||||||
@@ -559,7 +601,7 @@ fun PlayerView(
|
|||||||
),
|
),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(
|
colorFilter = ColorFilter.tint(
|
||||||
if (player.repeatMode == Player.REPEAT_MODE_OFF) {
|
if (playerState.repeatMode == Player.REPEAT_MODE_OFF) {
|
||||||
colorPalette.textDisabled
|
colorPalette.textDisabled
|
||||||
} else {
|
} else {
|
||||||
colorPalette.text
|
colorPalette.text
|
||||||
@@ -567,10 +609,13 @@ fun PlayerView(
|
|||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
player.mediaController.repeatMode =
|
player?.repeatMode
|
||||||
(player.mediaController.repeatMode + 2) % 3
|
?.plus(2)
|
||||||
|
?.mod(3)
|
||||||
preferences.repeatMode = player.mediaController.repeatMode
|
?.let { repeatMode ->
|
||||||
|
player.repeatMode = repeatMode
|
||||||
|
preferences.repeatMode = repeatMode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.size(28.dp)
|
.size(28.dp)
|
||||||
@@ -579,6 +624,8 @@ fun PlayerView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
PlayerBottomSheet(
|
PlayerBottomSheet(
|
||||||
|
player = player,
|
||||||
|
playerState = playerState,
|
||||||
layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound - 128.dp),
|
layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound - 128.dp),
|
||||||
onGlobalRouteEmitted = layoutState.collapse,
|
onGlobalRouteEmitted = layoutState.collapse,
|
||||||
song = song,
|
song = song,
|
||||||
|
|||||||
23
app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Context.kt
Normal file
23
app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Context.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.utils
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
inline fun <reified T> Context.intent(): Intent =
|
||||||
|
Intent(this@Context, T::class.java)
|
||||||
|
|
||||||
|
inline fun <reified T: BroadcastReceiver> Context.broadCastPendingIntent(
|
||||||
|
requestCode: Int = 0,
|
||||||
|
flags: Int = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0,
|
||||||
|
): PendingIntent =
|
||||||
|
PendingIntent.getBroadcast(this, requestCode, intent<T>(), flags)
|
||||||
|
|
||||||
|
inline fun <reified T: Activity> Context.activityPendingIntent(
|
||||||
|
requestCode: Int = 0,
|
||||||
|
flags: Int = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0,
|
||||||
|
): PendingIntent =
|
||||||
|
PendingIntent.getActivity(this, requestCode, intent<T>(), flags)
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.utils
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.media3.session.MediaController
|
|
||||||
import androidx.media3.session.SessionCommand
|
|
||||||
import androidx.media3.session.SessionResult
|
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
|
||||||
import kotlinx.coroutines.guava.await
|
|
||||||
|
|
||||||
|
|
||||||
suspend fun MediaController.send(command: SessionCommand, args: Bundle = Bundle.EMPTY): SessionResult {
|
|
||||||
return sendCustomCommand(command, args).await()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MediaController.command(command: SessionCommand, args: Bundle = Bundle.EMPTY, listener: ((SessionResult) -> Unit)? = null) {
|
|
||||||
val future = sendCustomCommand(command, args)
|
|
||||||
listener?.let {
|
|
||||||
future.addListener({ it(future.get()) }, MoreExecutors.directExecutor())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MediaController.syncCommand(command: SessionCommand, args: Bundle = Bundle.EMPTY): SessionResult {
|
|
||||||
return sendCustomCommand(command, args).get()
|
|
||||||
}
|
|
||||||
@@ -6,10 +6,9 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.media3.common.*
|
import androidx.media3.common.*
|
||||||
import androidx.media3.session.MediaController
|
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
open class PlayerState(val mediaController: MediaController) : Player.Listener {
|
open class PlayerState(val mediaController: Player) : Player.Listener {
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
var currentPosition by mutableStateOf(mediaController.currentPosition)
|
var currentPosition by mutableStateOf(mediaController.currentPosition)
|
||||||
@@ -52,7 +51,6 @@ open class PlayerState(val mediaController: MediaController) : Player.Listener {
|
|||||||
init {
|
init {
|
||||||
handler.post(object : Runnable {
|
handler.post(object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
duration = mediaController.duration
|
|
||||||
currentPosition = mediaController.currentPosition
|
currentPosition = mediaController.currentPosition
|
||||||
handler.postDelayed(this, 500)
|
handler.postDelayed(this, 500)
|
||||||
}
|
}
|
||||||
@@ -64,6 +62,7 @@ open class PlayerState(val mediaController: MediaController) : Player.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
duration = mediaController.duration
|
||||||
this.playbackState = playbackState
|
this.playbackState = playbackState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/TimerJob.kt
Normal file
43
app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/TimerJob.kt
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.utils
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
interface TimerJob {
|
||||||
|
val millisLeft: StateFlow<Long?>
|
||||||
|
fun cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CoroutineScope.timer(delayMillis: Long, onCompletion: () -> Unit): TimerJob {
|
||||||
|
val millisLeft = MutableStateFlow<Long?>(delayMillis)
|
||||||
|
|
||||||
|
val job = launch {
|
||||||
|
while (isActive && millisLeft.value != null) {
|
||||||
|
delay(1000)
|
||||||
|
millisLeft.emit(millisLeft.value?.minus(1000)?.takeIf { it > 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val disposableHandle = job.invokeOnCompletion {
|
||||||
|
if (it == null) {
|
||||||
|
onCompletion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : TimerJob {
|
||||||
|
override val millisLeft: StateFlow<Long?>
|
||||||
|
get() = millisLeft.asStateFlow()
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
millisLeft.value = null
|
||||||
|
disposableHandle.dispose()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,13 @@ package it.vfsfitvnm.vimusic.utils
|
|||||||
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.common.Player
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import it.vfsfitvnm.youtubemusic.Outcome
|
import it.vfsfitvnm.youtubemusic.Outcome
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.guava.await
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class YoutubePlayer(mediaController: MediaController) : PlayerState(mediaController) {
|
class YoutubePlayer(mediaController: Player) : PlayerState(mediaController) {
|
||||||
data class Radio(
|
data class Radio(
|
||||||
private val videoId: String? = null,
|
private val videoId: String? = null,
|
||||||
private val playlistId: String? = null,
|
private val playlistId: String? = null,
|
||||||
@@ -45,22 +43,13 @@ class YoutubePlayer(mediaController: MediaController) : PlayerState(mediaControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val LocalYoutubePlayer = compositionLocalOf<YoutubePlayer?> { null }
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberYoutubePlayer(
|
fun rememberYoutubePlayer(
|
||||||
mediaControllerFuture: ListenableFuture<MediaController>
|
player: Player?
|
||||||
): YoutubePlayer? {
|
): YoutubePlayer? {
|
||||||
val mediaController by produceState<MediaController?>(initialValue = null) {
|
return remember(player) {
|
||||||
value = mediaControllerFuture.await()
|
YoutubePlayer(player ?: return@remember null).also {
|
||||||
}
|
player.addListener(it)
|
||||||
|
|
||||||
val playerState = remember(mediaController) {
|
|
||||||
YoutubePlayer(mediaController ?: return@remember null).also {
|
|
||||||
// TODO: should we remove the listener later on?
|
|
||||||
mediaController?.addListener(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return playerState
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ val SongWithInfo.asMediaItem: MediaItem
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.setMediaId(song.id)
|
.setMediaId(song.id)
|
||||||
|
.setUri(song.id)
|
||||||
|
.setCustomCacheKey(song.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun YouTube.PlaylistOrAlbum.Item.toMediaItem(
|
fun YouTube.PlaylistOrAlbum.Item.toMediaItem(
|
||||||
@@ -172,6 +174,8 @@ fun YouTube.PlaylistOrAlbum.Item.toMediaItem(
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.setMediaId(info.endpoint?.videoId ?: return null)
|
.setMediaId(info.endpoint?.videoId ?: return null)
|
||||||
|
.setUri(info.endpoint?.videoId ?: return null)
|
||||||
|
.setCustomCacheKey(info.endpoint?.videoId ?: return null)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ dependencyResolutionManagement {
|
|||||||
alias("room-compiler").to("androidx.room", "room-compiler").versionRef("room")
|
alias("room-compiler").to("androidx.room", "room-compiler").versionRef("room")
|
||||||
|
|
||||||
version("media3", "1.0.0-beta01")
|
version("media3", "1.0.0-beta01")
|
||||||
alias("media3-session").to("androidx.media3", "media3-session").versionRef("media3")
|
alias("exoplayer").to("androidx.media3", "media3-exoplayer").versionRef("media3")
|
||||||
alias("media3-exoplayer").to("androidx.media3", "media3-exoplayer").versionRef("media3")
|
|
||||||
|
|
||||||
version("ktor", "2.0.2")
|
version("ktor", "2.0.2")
|
||||||
alias("ktor-client-core").to("io.ktor", "ktor-client-core").versionRef("ktor")
|
alias("ktor-client-core").to("io.ktor", "ktor-client-core").versionRef("ktor")
|
||||||
@@ -49,8 +48,6 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
alias("brotli").to("org.brotli", "dec").version("0.1.2")
|
alias("brotli").to("org.brotli", "dec").version("0.1.2")
|
||||||
|
|
||||||
alias("guava-coroutines").to("org.jetbrains.kotlinx", "kotlinx-coroutines-guava").version("1.6.2")
|
|
||||||
|
|
||||||
alias("desugaring").to("com.android.tools", "desugar_jdk_libs").version("1.1.5")
|
alias("desugaring").to("com.android.tools", "desugar_jdk_libs").version("1.1.5")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user