Files
muza/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt

377 lines
16 KiB
Kotlin

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<PlayerService.Binder?>(null)
private var uri by mutableStateOf<Uri?>(null, neverEqualPolicy())
override fun onStart() {
super.onStart()
bindService(intent<PlayerService>(), 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<PlayerService.Binder?> { null }
val LocalPlayerAwarePaddingValues = staticCompositionLocalOf<PaddingValues> { TODO() }