377 lines
16 KiB
Kotlin
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() }
|