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.net.Uri import android.os.Bundle import android.os.IBinder 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.LocalOverscrollConfiguration 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.neverEqualPolicy import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue 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.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat 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.collapsedAnchor import it.vfsfitvnm.vimusic.ui.components.dismissedAnchor import it.vfsfitvnm.vimusic.ui.components.expandedAnchor import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState import it.vfsfitvnm.vimusic.ui.screens.HomeScreen import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen 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.ui.views.PlayerView import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class MainActivity : ComponentActivity() { companion object { private var alreadyRunning = false } 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) private var uri by mutableStateOf(null, neverEqualPolicy()) 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 playerBottomSheetAnchor = when { intent?.extras?.getBoolean("expandPlayerBottomSheet") == true -> expandedAnchor alreadyRunning -> collapsedAnchor else -> dismissedAnchor.also { alreadyRunning = true } } uri = intent?.data setContent { val coroutineScope = rememberCoroutineScope() val isSystemInDarkTheme = isSystemInDarkTheme() var appearance by remember(isSystemInDarkTheme) { 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, initialAnchor = playerBottomSheetAnchor ) val playerAwarePaddingValues = if (playerBottomSheetState.isDismissed) { paddingValues } else { object : PaddingValues by paddingValues { override fun calculateBottomPadding(): Dp = paddingValues.calculateBottomPadding() + Dimensions.collapsedPlayer } } CompositionLocalProvider( LocalAppearance provides appearance, LocalOverscrollConfiguration provides null, LocalIndication provides rememberRipple(bounded = false), LocalRippleTheme provides rippleTheme, LocalShimmerTheme provides shimmerTheme, LocalPlayerServiceBinder provides binder, LocalPlayerAwarePaddingValues provides playerAwarePaddingValues ) { when (val uri = uri) { null -> { HomeScreen() PlayerView( layoutState = playerBottomSheetState, modifier = Modifier .align(Alignment.BottomCenter) ) DisposableEffect(binder?.player) { binder?.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)) } } }) ?: onDispose { } } } else -> IntentUriScreen(uri = uri) } BottomSheetMenu( state = LocalMenuState.current, modifier = Modifier .align(Alignment.BottomCenter) ) } } } } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) uri = intent?.data } private fun setSystemBarAppearance(isDark: Boolean) { with(WindowCompat.getInsetsController(window, window.decorView.rootView)) { isAppearanceLightStatusBars = !isDark isAppearanceLightNavigationBars = !isDark } } } val LocalPlayerServiceBinder = staticCompositionLocalOf { null } val LocalPlayerAwarePaddingValues = staticCompositionLocalOf { TODO() }