package it.vfsfitvnm.vimusic import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.content.SharedPreferences import android.graphics.Bitmap import android.os.Build import android.os.Bundle import android.os.IBinder import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.systemBars import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material.ripple.RippleTheme import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import androidx.media3.common.MediaItem import androidx.media3.common.Player import com.valentinilk.shimmer.LocalShimmerTheme import com.valentinilk.shimmer.defaultShimmerTheme import it.vfsfitvnm.vimusic.enums.ColorPaletteMode import it.vfsfitvnm.vimusic.enums.ColorPaletteName import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.service.PlayerService import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.home.HomeScreen import it.vfsfitvnm.vimusic.ui.screens.player.Player import it.vfsfitvnm.vimusic.ui.screens.playlistRoute import it.vfsfitvnm.vimusic.ui.styling.Appearance import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.typographyOf import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.getEnum import it.vfsfitvnm.vimusic.utils.intent import it.vfsfitvnm.vimusic.utils.listener import it.vfsfitvnm.vimusic.utils.preferences import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody import it.vfsfitvnm.youtubemusic.requests.playlistPage import it.vfsfitvnm.youtubemusic.requests.song import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext 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(null) override fun onStart() { super.onStart() bindService(intent(), serviceConnection, Context.BIND_AUTO_CREATE) } override fun onStop() { unbindService(serviceConnection) super.onStop() } @OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) val launchedFromNotification = intent?.extras?.getBoolean("expandPlayerBottomSheet") == true setContent { val coroutineScope = rememberCoroutineScope() val isSystemInDarkTheme = isSystemInDarkTheme() var appearance by rememberSaveable(isSystemInDarkTheme, stateSaver = Appearance.Companion) { with(preferences) { val colorPaletteName = getEnum(colorPaletteNameKey, ColorPaletteName.Dynamic) val colorPaletteMode = getEnum(colorPaletteModeKey, ColorPaletteMode.System) val thumbnailRoundness = getEnum(thumbnailRoundnessKey, ThumbnailRoundness.Light) val colorPalette = colorPaletteOf(colorPaletteName, colorPaletteMode, isSystemInDarkTheme) setSystemBarAppearance(colorPalette.isDark) mutableStateOf( Appearance( colorPalette = colorPalette, typography = typographyOf(colorPalette.text), thumbnailShape = thumbnailRoundness.shape() ) ) } } DisposableEffect(binder, isSystemInDarkTheme) { var bitmapListenerJob: Job? = null fun setDynamicPalette(colorPaletteMode: ColorPaletteMode) { val isDark = colorPaletteMode == ColorPaletteMode.Dark || (colorPaletteMode == ColorPaletteMode.System && isSystemInDarkTheme) binder?.setBitmapListener { bitmap: Bitmap? -> if (bitmap == null) { val colorPalette = colorPaletteOf( ColorPaletteName.Dynamic, colorPaletteMode, isSystemInDarkTheme ) setSystemBarAppearance(colorPalette.isDark) appearance = appearance.copy( colorPalette = colorPalette, typography = typographyOf(colorPalette.text) ) return@setBitmapListener } bitmapListenerJob = coroutineScope.launch(Dispatchers.IO) { dynamicColorPaletteOf(bitmap, isDark)?.let { withContext(Dispatchers.Main) { setSystemBarAppearance(it.isDark) } appearance = appearance.copy( colorPalette = it, typography = typographyOf(it.text) ) } } } } val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key -> when (key) { colorPaletteNameKey, colorPaletteModeKey -> { val colorPaletteName = sharedPreferences.getEnum( colorPaletteNameKey, ColorPaletteName.Dynamic ) val colorPaletteMode = sharedPreferences.getEnum( colorPaletteModeKey, ColorPaletteMode.System ) if (colorPaletteName == ColorPaletteName.Dynamic) { setDynamicPalette(colorPaletteMode) } else { bitmapListenerJob?.cancel() binder?.setBitmapListener(null) val colorPalette = colorPaletteOf( colorPaletteName, colorPaletteMode, isSystemInDarkTheme ) setSystemBarAppearance(colorPalette.isDark) appearance = appearance.copy( colorPalette = colorPalette, typography = typographyOf(colorPalette.text), ) } } thumbnailRoundnessKey -> { val thumbnailRoundness = sharedPreferences.getEnum(key, ThumbnailRoundness.Light) appearance = appearance.copy( thumbnailShape = thumbnailRoundness.shape() ) } } } with(preferences) { registerOnSharedPreferenceChangeListener(listener) val colorPaletteName = getEnum(colorPaletteNameKey, ColorPaletteName.Dynamic) if (colorPaletteName == ColorPaletteName.Dynamic) { setDynamicPalette(getEnum(colorPaletteModeKey, ColorPaletteMode.System)) } onDispose { bitmapListenerJob?.cancel() binder?.setBitmapListener(null) unregisterOnSharedPreferenceChangeListener(listener) } } } val rippleTheme = remember(appearance.colorPalette.text, appearance.colorPalette.isDark) { object : RippleTheme { @Composable override fun defaultColor(): Color = RippleTheme.defaultRippleColor( contentColor = appearance.colorPalette.text, lightTheme = !appearance.colorPalette.isDark ) @Composable override fun rippleAlpha(): RippleAlpha = RippleTheme.defaultRippleAlpha( contentColor = appearance.colorPalette.text, lightTheme = !appearance.colorPalette.isDark ) } } val shimmerTheme = remember { defaultShimmerTheme.copy( animationSpec = infiniteRepeatable( animation = tween( durationMillis = 800, easing = LinearEasing, delayMillis = 250, ), repeatMode = RepeatMode.Restart ), shaderColors = listOf( Color.Unspecified.copy(alpha = 0.25f), Color.White.copy(alpha = 0.50f), Color.Unspecified.copy(alpha = 0.25f), ), ) } BoxWithConstraints( modifier = Modifier .fillMaxSize() .background(appearance.colorPalette.background0) ) { val paddingValues = WindowInsets.systemBars.asPaddingValues() val playerBottomSheetState = rememberBottomSheetState( dismissedBound = 0.dp, collapsedBound = Dimensions.collapsedPlayer + paddingValues.calculateBottomPadding(), expandedBound = maxHeight, ) val playerAwarePaddingValues = if (playerBottomSheetState.isDismissed) { paddingValues } else { object : PaddingValues by paddingValues { override fun calculateBottomPadding(): Dp = paddingValues.calculateBottomPadding() + Dimensions.collapsedPlayer } } CompositionLocalProvider( LocalAppearance provides appearance, LocalIndication provides rememberRipple(bounded = true), LocalRippleTheme provides rippleTheme, LocalShimmerTheme provides shimmerTheme, LocalPlayerServiceBinder provides binder, LocalPlayerAwarePaddingValues provides playerAwarePaddingValues ) { HomeScreen( onPlaylistUrl = { url -> onNewIntent(Intent.parseUri(url, 0)) } ) Player( layoutState = playerBottomSheetState, modifier = Modifier .align(Alignment.BottomCenter) ) DisposableEffect(binder?.player) { val player = binder?.player ?: return@DisposableEffect onDispose { } if (player.currentMediaItem == null) { if (!playerBottomSheetState.isDismissed) { playerBottomSheetState.dismiss() } } else { if (playerBottomSheetState.isDismissed) { if (launchedFromNotification) { intent.replaceExtras(Bundle()) playerBottomSheetState.expandSoft() } else { playerBottomSheetState.collapseSoft() } } } player.listener(object : Player.Listener { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) { playerBottomSheetState.expand(tween(500)) } } }) } BottomSheetMenu( state = LocalMenuState.current, modifier = Modifier .align(Alignment.BottomCenter) ) } } } onNewIntent(intent) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) val uri = intent?.data ?: return intent.data = null this.intent = null Toast.makeText(this, "Opening url...", Toast.LENGTH_SHORT).show() lifecycleScope.launch(Dispatchers.IO) { uri.getQueryParameter("list")?.let { playlistId -> val browseId = "VL$playlistId" if (playlistId.startsWith("OLAK5uy_")) { Innertube.playlistPage(BrowseBody(browseId = browseId))?.getOrNull()?.let { playlist -> playlist.songsPage?.items?.firstOrNull()?.album?.endpoint?.browseId?.let { browseId -> albumRoute.ensureGlobal(browseId) } } } else { playlistRoute.ensureGlobal(browseId) } } ?: (uri.getQueryParameter("v") ?: uri.takeIf { uri.host == "youtu.be" }?.path?.drop(1))?.let { videoId -> Innertube.song(videoId)?.getOrNull()?.let { song -> val binder = snapshotFlow { binder }.filterNotNull().first() withContext(Dispatchers.Main) { binder.player.forcePlay(song.asMediaItem) } } } } } private fun setSystemBarAppearance(isDark: Boolean) { with(WindowCompat.getInsetsController(window, window.decorView.rootView)) { isAppearanceLightStatusBars = !isDark isAppearanceLightNavigationBars = !isDark } if (Build.VERSION.SDK_INT < 23) { window.statusBarColor = (if (isDark) Color.Transparent else Color.Black.copy(alpha = 0.2f)).toArgb() } if (Build.VERSION.SDK_INT < 26) { window.navigationBarColor = (if (isDark) Color.Transparent else Color.Black.copy(alpha = 0.2f)).toArgb() } } } val LocalPlayerServiceBinder = staticCompositionLocalOf { null } val LocalPlayerAwarePaddingValues = staticCompositionLocalOf { TODO() }