Initial commit

This commit is contained in:
vfsfitvnm
2022-06-02 18:59:18 +02:00
commit 1e673ad582
160 changed files with 10800 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
package it.vfsfitvnm.vimusic.utils
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Timeline
fun Player.forcePlay(mediaItem: MediaItem) {
setMediaItem(mediaItem, true)
playWhenReady = true
prepare()
}
fun Player.forcePlayAtIndex(mediaItems: List<MediaItem>, mediaItemIndex: Int) {
if (mediaItems.isEmpty()) return
setMediaItems(mediaItems, true)
playWhenReady = true
seekToDefaultPosition(mediaItemIndex)
prepare()
}
fun Player.forcePlayFromBeginning(mediaItems: List<MediaItem>) =
forcePlayAtIndex(mediaItems, 0)
val Player.lastMediaItem: MediaItem?
get() = mediaItemCount.takeIf { it > 0 }?.let { it - 1 }?.let(::getMediaItemAt)
val Timeline.mediaItems: List<MediaItem>
get() = (0 until windowCount).map { index ->
getWindow(index, Timeline.Window()).mediaItem
}
fun Player.addNext(mediaItem: MediaItem) {
addMediaItem(currentMediaItemIndex + 1, mediaItem)
}
fun Player.enqueue(mediaItem: MediaItem) {
addMediaItem(mediaItemCount, mediaItem)
}
fun Player.enqueue(mediaItems: List<MediaItem>) {
addMediaItems(mediaItemCount, mediaItems)
}

View File

@@ -0,0 +1,92 @@
package it.vfsfitvnm.vimusic.utils
import android.os.Handler
import android.os.Looper
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.media3.common.*
import androidx.media3.session.MediaController
import kotlin.math.absoluteValue
open class PlayerState(val mediaController: MediaController) : Player.Listener {
private val handler = Handler(Looper.getMainLooper())
var currentPosition by mutableStateOf(mediaController.currentPosition)
var duration by mutableStateOf(mediaController.duration)
private set
val progress: Float
get() = currentPosition.toFloat() / duration.absoluteValue
var playbackState by mutableStateOf(mediaController.playbackState)
private set
var mediaItemIndex by mutableStateOf(mediaController.currentMediaItemIndex)
private set
var mediaItem by mutableStateOf(mediaController.currentMediaItem)
private set
var mediaMetadata by mutableStateOf(mediaController.mediaMetadata)
private set
var isPlaying by mutableStateOf(mediaController.isPlaying)
private set
var playWhenReady by mutableStateOf(mediaController.playWhenReady)
private set
var repeatMode by mutableStateOf(mediaController.repeatMode)
private set
var error by mutableStateOf(mediaController.playerError)
var mediaItems by mutableStateOf(mediaController.currentTimeline.mediaItems)
private set
init {
handler.post(object : Runnable {
override fun run() {
duration = mediaController.duration
currentPosition = mediaController.currentPosition
handler.postDelayed(this, 500)
}
})
}
override fun onPlaybackStateChanged(playbackState: Int) {
this.playbackState = playbackState
}
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
this.mediaMetadata = mediaMetadata
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
this.isPlaying = isPlaying
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
this.playWhenReady = playWhenReady
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
this.mediaItem = mediaItem
mediaItemIndex = mediaController.currentMediaItemIndex
}
override fun onRepeatModeChanged(repeatMode: Int) {
this.repeatMode = repeatMode
}
override fun onPlayerError(playbackException: PlaybackException) {
error = playbackException
}
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
mediaItems = timeline.mediaItems
mediaItemIndex = mediaController.currentMediaItemIndex
}
}

View File

@@ -0,0 +1,108 @@
package it.vfsfitvnm.vimusic.utils
import android.content.Context
import android.content.SharedPreferences
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.media3.common.Player
import it.vfsfitvnm.vimusic.enums.SongCollection
import it.vfsfitvnm.youtubemusic.YouTube
@Stable
class Preferences(holder: SharedPreferences) : SharedPreferences by holder {
var searchFilter by preference("searchFilter", YouTube.Item.Song.Filter.value)
var repeatMode by preference("repeatMode", Player.REPEAT_MODE_OFF)
var homePageSongCollection by preference("homePageSongCollection", SongCollection.MostPlayed)
}
val LocalPreferences = staticCompositionLocalOf<Preferences> { TODO() }
@Composable
fun rememberPreferences(): Preferences {
val context = LocalContext.current
return remember {
Preferences(context.getSharedPreferences("preferences", Context.MODE_PRIVATE))
}
}
private fun SharedPreferences.preference(key: String, defaultValue: Boolean) =
mutableStateOf(value = getBoolean(key, defaultValue)) {
edit {
putBoolean(key, it)
}
}
private fun SharedPreferences.preference(key: String, defaultValue: Int) =
mutableStateOf(value = getInt(key, defaultValue)) {
edit {
putInt(key, it)
}
}
private fun SharedPreferences.preference(key: String, defaultValue: Long) =
mutableStateOf(value = getLong(key, defaultValue)) {
edit {
putLong(key, it)
}
}
private fun SharedPreferences.preference(key: String, defaultValue: Float) =
mutableStateOf(value = getFloat(key, defaultValue)) {
edit {
putFloat(key, it)
}
}
private fun SharedPreferences.preference(key: String, defaultValue: String) =
mutableStateOf(value = getString(key, defaultValue)!!) {
edit {
putString(key, it)
}
}
private fun SharedPreferences.preference(key: String, defaultValue: Set<String>) =
mutableStateOf(value = getStringSet(key, defaultValue)!!) {
edit {
putStringSet(key, it)
}
}
private fun SharedPreferences.preference(key: String, defaultValue: Dp) =
mutableStateOf(value = getFloat(key, defaultValue.value).dp) {
edit {
putFloat(key, it.value)
}
}
private fun SharedPreferences.preference(key: String, defaultValue: TextUnit) =
mutableStateOf(value = getFloat(key, defaultValue.value).sp) {
edit {
putFloat(key, it.value)
}
}
private inline fun <reified T : Enum<T>> SharedPreferences.preference(
key: String,
defaultValue: T
) =
mutableStateOf(value = enumValueOf<T>(getString(key, defaultValue.name)!!)) {
edit {
putString(key, it.name)
}
}
private fun <T> mutableStateOf(value: T, onStructuralInequality: (newValue: T) -> Unit) =
mutableStateOf(
value = value,
policy = object : SnapshotMutationPolicy<T> {
override fun equivalent(a: T, b: T): Boolean {
val areEquals = a == b
if (!areEquals) onStructuralInequality(b)
return areEquals
}
})

View File

@@ -0,0 +1,19 @@
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@file:OptIn(InternalComposeApi::class)
package it.vfsfitvnm.vimusic.utils
import androidx.compose.runtime.*
import kotlinx.coroutines.CoroutineScope
@Composable
@NonRestartableComposable
fun relaunchableEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
): () -> Unit {
val applyContext = currentComposer.applyCoroutineContext
val launchedEffect = remember(key1) { LaunchedEffectImpl(applyContext, block) }
return launchedEffect::onRemembered
}

View File

@@ -0,0 +1,11 @@
package it.vfsfitvnm.vimusic.utils
class RingBuffer<T>(val size: Int, init: (index: Int) -> T) {
private val list = MutableList(2, init)
private var index = 0
fun getOrNull(index: Int): T? = list.getOrNull(index)
fun append(element: T) = list.set(index++ % size, element)
}

View File

@@ -0,0 +1,43 @@
package it.vfsfitvnm.vimusic.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
fun TextStyle.style(style: FontStyle) = copy(fontStyle = style)
fun TextStyle.weight(weight: FontWeight) = copy(fontWeight = weight)
fun TextStyle.align(align: TextAlign) = copy(textAlign = align)
fun TextStyle.color(color: Color) = copy(color = color)
inline val TextStyle.italic: TextStyle
get() = style(FontStyle.Italic)
inline val TextStyle.medium: TextStyle
get() = weight(FontWeight.Medium)
inline val TextStyle.semiBold: TextStyle
get() = weight(FontWeight.SemiBold)
inline val TextStyle.bold: TextStyle
get() = weight(FontWeight.Bold)
inline val TextStyle.center: TextStyle
get() = align(TextAlign.Center)
inline val TextStyle.secondary: TextStyle
@Composable
@ReadOnlyComposable
get() = color(LocalColorPalette.current.textSecondary)
inline val TextStyle.disabled: TextStyle
@Composable
@ReadOnlyComposable
get() = color(LocalColorPalette.current.textDisabled)

View File

@@ -0,0 +1,129 @@
package it.vfsfitvnm.vimusic.utils
import androidx.compose.runtime.*
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import com.google.common.util.concurrent.ListenableFuture
import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.*
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.sync.Mutex
class YoutubePlayer(mediaController: MediaController) : PlayerState(mediaController) {
object Radio {
var isActive by mutableStateOf(false)
var listener: Listener? = null
private var videoId: String? = null
private var playlistId: String? = null
private var playlistSetVideoId: String? = null
private var parameters: String? = null
var nextContinuation by mutableStateOf<Outcome<String?>>(Outcome.Initial)
fun setup(videoId: String? = null, playlistId: String? = null, playlistSetVideoId: String? = null, parameters: String? = null) {
this.videoId = videoId
this.playlistId = playlistId
this.playlistSetVideoId = playlistSetVideoId
this.parameters = parameters
isActive = true
nextContinuation = Outcome.Initial
}
fun setup(watchEndpoint: NavigationEndpoint.Endpoint.Watch?) {
setup(
videoId = watchEndpoint?.videoId,
playlistId = watchEndpoint?.playlistId,
parameters = watchEndpoint?.params,
playlistSetVideoId = watchEndpoint?.playlistSetVideoId
)
listener?.process(true)
}
suspend fun process(player: Player, force: Boolean = false, play: Boolean = false) {
if (!isActive) return
if (!force && !play) {
val isFirstSong = withContext(Dispatchers.Main) {
player.mediaItemCount == 0 || (player.currentMediaItemIndex == 0 && player.mediaItemCount == 1)
}
val isNearEndSong = withContext(Dispatchers.Main) {
player.mediaItemCount - player.currentMediaItemIndex <= 3
}
if (!isFirstSong && !isNearEndSong) {
return
}
}
val token = nextContinuation.valueOrNull
nextContinuation = Outcome.Loading
nextContinuation = withContext(Dispatchers.IO) {
YouTube.next(
videoId = videoId ?: withContext(Dispatchers.Main) {
player.lastMediaItem?.mediaId ?: error("This should not happen")
},
playlistId = playlistId,
params = parameters,
playlistSetVideoId = playlistSetVideoId,
continuation = token
)
}.map { nextResult ->
nextResult.items?.map(it.vfsfitvnm.youtubemusic.YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
withContext(Dispatchers.Main) {
if (play) {
player.forcePlayFromBeginning(mediaItems)
} else {
player.addMediaItems(mediaItems.drop(if (token == null) 1 else 0))
}
}
}
nextResult.continuation?.takeUnless { token == nextResult.continuation }
}.recoverWith(token)
}
fun reset() {
videoId = null
playlistId = null
playlistSetVideoId = null
parameters = null
isActive = false
nextContinuation = Outcome.Initial
}
interface Listener {
fun process(play: Boolean)
}
}
}
val LocalYoutubePlayer = compositionLocalOf<YoutubePlayer?> { null }
@Composable
fun rememberYoutubePlayer(
mediaControllerFuture: ListenableFuture<MediaController>,
repeatMode: Int
): YoutubePlayer? {
val mediaController by produceState<MediaController?>(initialValue = null) {
value = mediaControllerFuture.await().also {
it.repeatMode = repeatMode
}
}
val playerState = remember(mediaController) {
YoutubePlayer(mediaController ?: return@remember null).also {
// TODO: should we remove the listener later on?
mediaController?.addListener(it)
}
}
return playerState
}

View File

@@ -0,0 +1,24 @@
package it.vfsfitvnm.vimusic.utils
import android.view.HapticFeedbackConstants
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalView
@Composable
fun rememberHapticFeedback(): HapticFeedback {
val view = LocalView.current
return remember {
object : HapticFeedback {
override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) {
view.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
)
}
}
}
}

View File

@@ -0,0 +1,172 @@
package it.vfsfitvnm.vimusic.utils
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.internal
import it.vfsfitvnm.vimusic.models.Info
import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.models.SongWithAuthors
import it.vfsfitvnm.vimusic.models.SongWithInfo
import it.vfsfitvnm.youtubemusic.YouTube
fun Context.shareAsYouTubeSong(mediaItem: MediaItem) {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaItem.mediaId}")
}
startActivity(Intent.createChooser(sendIntent, null))
}
fun Database.insert(mediaItem: MediaItem): Song {
return internal.runInTransaction<Song> {
Database.song(mediaItem.mediaId)?.let {
return@runInTransaction it
}
val albumInfo = mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId ->
Info(
text = mediaItem.mediaMetadata.albumTitle!!.toString(),
browseId = albumId
)
}
val albumInfoId = albumInfo?.let { insert(it) }
val authorsInfo =
mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames")?.let { artistNames ->
mediaItem.mediaMetadata.extras!!.getStringArrayList("artistIds")?.let { artistIds ->
artistNames.mapIndexed { index, artistName ->
Info(
text = artistName,
browseId = artistIds.getOrNull(index)
)
}
}
}
val song = Song(
id = mediaItem.mediaId,
title = mediaItem.mediaMetadata.title!!.toString(),
albumInfoId = albumInfoId,
durationText = mediaItem.mediaMetadata.extras?.getString("durationText")!!,
thumbnailUrl = mediaItem.mediaMetadata.artworkUri!!.toString()
)
insert(song)
val authorsInfoId = authorsInfo?.let { insert(authorsInfo) }
authorsInfoId?.forEach { authorInfoId ->
insert(
SongWithAuthors(
songId = mediaItem.mediaId,
authorInfoId = authorInfoId
)
)
}
return@runInTransaction song
}
}
val YouTube.Item.Song.asMediaItem: MediaItem
get() = MediaItem.Builder()
.setMediaId(info.endpoint!!.videoId)
.setUri(info.endpoint!!.videoId)
.setCustomCacheKey(info.endpoint!!.videoId)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(info.name)
.setArtist(authors.joinToString("") { it.name })
.setAlbumTitle(album?.name)
.setArtworkUri(thumbnail.url.toUri())
.setExtras(
bundleOf(
"videoId" to info.endpoint!!.videoId,
"albumId" to album?.endpoint?.browseId,
"durationText" to durationText,
"artistNames" to authors.map { it.name },
"artistIds" to authors.map { it.endpoint?.browseId },
)
)
.build()
)
.build()
val YouTube.Item.Video.asMediaItem: MediaItem
get() = MediaItem.Builder()
.setMediaId(info.endpoint!!.videoId)
.setUri(info.endpoint!!.videoId)
.setCustomCacheKey(info.endpoint!!.videoId)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(info.name)
.setArtist(authors.joinToString("") { it.name })
.setArtworkUri(thumbnail.url.toUri())
.setExtras(
bundleOf(
"videoId" to info.endpoint!!.videoId,
"durationText" to durationText,
"artistNames" to if (isOfficialMusicVideo) authors.map { it.name } else null,
"artistIds" to if (isOfficialMusicVideo) authors.map { it.endpoint?.browseId } else null,
)
)
.build()
)
.build()
val SongWithInfo.asMediaItem: MediaItem
get() = MediaItem.Builder()
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(song.title)
.setArtist(authors?.joinToString("") { it.text })
.setAlbumTitle(album?.text)
.setArtworkUri(song.thumbnailUrl?.toUri())
.setExtras(
bundleOf(
"videoId" to song.id,
"albumId" to album?.browseId,
"artistNames" to authors?.map { it.text },
"artistIds" to authors?.map { it.browseId },
"durationText" to song.durationText
)
)
.build()
)
.setMediaId(song.id)
.build()
fun YouTube.AlbumItem.toMediaItem(
albumId: String,
album: YouTube.Album
): MediaItem? {
return MediaItem.Builder()
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(info.name)
.setArtist((authors ?: album.authors).joinToString("") { it.name })
.setAlbumTitle(album.title)
.setArtworkUri(album.thumbnail.url.toUri())
.setExtras(
bundleOf(
"videoId" to info.endpoint?.videoId,
"playlistId" to info.endpoint?.playlistId,
"albumId" to albumId,
"durationText" to durationText,
"artistNames" to (authors ?: album.authors).map { it.name },
"artistIds" to (authors ?: album.authors).map { it.endpoint?.browseId }
)
)
.build()
)
.setMediaId(info.endpoint?.videoId ?: return null)
.build()
}