Files
muza/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt
2022-08-10 18:29:19 +02:00

361 lines
15 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.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
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.platform.LocalHapticFeedback
import androidx.compose.ui.unit.coerceAtMost
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.rememberBottomSheetState
import it.vfsfitvnm.vimusic.ui.components.rememberMenuState
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.rememberHapticFeedback
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() {
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 expandPlayerBottomSheet =
intent?.extras?.getBoolean("expandPlayerBottomSheet", false) ?: false
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),
),
)
}
CompositionLocalProvider(
LocalAppearance provides appearance,
LocalOverscrollConfiguration provides null,
LocalIndication provides rememberRipple(bounded = false),
LocalRippleTheme provides rippleTheme,
LocalShimmerTheme provides shimmerTheme,
LocalPlayerServiceBinder provides binder,
LocalMenuState provides rememberMenuState(),
LocalHapticFeedback provides rememberHapticFeedback()
) {
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.background(appearance.colorPalette.background0)
) {
when (val uri = uri) {
null -> {
val paddingValues = WindowInsets.navigationBars.asPaddingValues()
val playerBottomSheetState = rememberBottomSheetState(
dismissedBound = 0.dp,
collapsedBound = Dimensions.collapsedPlayer + paddingValues.calculateBottomPadding(),
expandedBound = maxHeight,
isExpanded = expandPlayerBottomSheet
)
HomeScreen()
PlayerView(
layoutState = playerBottomSheetState,
modifier = Modifier
.align(Alignment.BottomCenter)
)
binder?.player?.let { player ->
ExpandPlayerOnPlaylistChange(
player = player,
expand = {
playerBottomSheetState.expand(tween(500))
}
)
}
}
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 }
@Composable
fun ExpandPlayerOnPlaylistChange(player: Player, expand: () -> Unit) {
DisposableEffect(player, expand) {
player.listener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) {
expand()
}
}
})
}
}