diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index a146f6bf3..8eab00e1b 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -179,6 +179,8 @@ Playback Save Playback State Save shuffle and repeat mode + Double press on image to ±5sec + Double tap on left/right of the image to seek back/forward 5 seconds Skip Silent Skip no music part Time out, check internet connection (%1$s) diff --git a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/component/PlayerControlLayout.kt b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/component/PlayerControlLayout.kt index 98f928fd2..562a4daa7 100644 --- a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/component/PlayerControlLayout.kt +++ b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/component/PlayerControlLayout.kt @@ -13,10 +13,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Forward5 import androidx.compose.material.icons.rounded.PauseCircle import androidx.compose.material.icons.rounded.PlayCircle import androidx.compose.material.icons.rounded.Repeat import androidx.compose.material.icons.rounded.RepeatOne +import androidx.compose.material.icons.rounded.Replay5 import androidx.compose.material.icons.rounded.Shuffle import androidx.compose.material.icons.rounded.SkipNext import androidx.compose.material.icons.rounded.SkipPrevious @@ -111,6 +113,29 @@ fun PlayerControlLayout( ) } } + Box(Modifier.weight(1f), contentAlignment = Alignment.Center) { + Box( + modifier = + Modifier + .background(transparent) + .size(smallIcon.second) + .aspectRatio(1f) + .clip( + CircleShape, + ) + .clickable { + onUIEvent(UIEvent.Backward5) + }, + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Rounded.Replay5, + tint = Color.White, + contentDescription = "", + modifier = Modifier.size(smallIcon.first), + ) + } + } Box(Modifier.weight(1f), contentAlignment = Alignment.Center) { Box( modifier = @@ -145,6 +170,29 @@ fun PlayerControlLayout( } } } + Box(Modifier.weight(1f), contentAlignment = Alignment.Center) { + Box( + modifier = + Modifier + .background(transparent) + .size(mediumIcon.second) + .aspectRatio(1f) + .clip( + CircleShape, + ) + .clickable { + onUIEvent(UIEvent.Forward5) + }, + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Rounded.Forward5, + tint = Color.White, + contentDescription = "", + modifier = Modifier.size(mediumIcon.first), + ) + } + } Box(Modifier.weight(1f), contentAlignment = Alignment.Center) { Box( modifier = @@ -217,4 +265,4 @@ fun PlayerControlLayout( } } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/home/SettingScreen.kt b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/home/SettingScreen.kt index 9f1ac4294..9c4dd88df 100644 --- a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/home/SettingScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/home/SettingScreen.kt @@ -296,6 +296,8 @@ import simpmusic.composeapp.generated.resources.save_all_your_playlist_data import simpmusic.composeapp.generated.resources.save_last_played import simpmusic.composeapp.generated.resources.save_last_played_track_and_queue import simpmusic.composeapp.generated.resources.save_playback_state +import simpmusic.composeapp.generated.resources.double_press_to_seek +import simpmusic.composeapp.generated.resources.double_press_to_seek_description import simpmusic.composeapp.generated.resources.save_shuffle_and_repeat_mode import simpmusic.composeapp.generated.resources.send_back_listening_data_to_google import simpmusic.composeapp.generated.resources.set @@ -413,6 +415,7 @@ fun SettingScreen( val normalizeVolume by viewModel.normalizeVolume.map { it == TRUE }.collectAsStateWithLifecycle(initialValue = false) val skipSilent by viewModel.skipSilent.map { it == TRUE }.collectAsStateWithLifecycle(initialValue = false) val savePlaybackState by viewModel.savedPlaybackState.map { it == TRUE }.collectAsStateWithLifecycle(initialValue = false) + val doublePressToSeek by viewModel.doublePressToSeek.map { it == TRUE }.collectAsStateWithLifecycle(initialValue = false) val saveLastPlayed by viewModel.saveRecentSongAndQueue.map { it == TRUE }.collectAsStateWithLifecycle(initialValue = false) val killServiceOnExit by viewModel.killServiceOnExit.map { it == TRUE }.collectAsStateWithLifecycle(initialValue = true) val mainLyricsProvider by viewModel.mainLyricsProvider.collectAsStateWithLifecycle() @@ -970,6 +973,11 @@ fun SettingScreen( subtitle = stringResource(Res.string.save_shuffle_and_repeat_mode), switch = (savePlaybackState to { viewModel.setSavedPlaybackState(it) }), ) + SettingItem( + title = stringResource(Res.string.double_press_to_seek), + subtitle = stringResource(Res.string.double_press_to_seek_description), + switch = (doublePressToSeek to { viewModel.setDoublePressToSeek(it) }), + ) SettingItem( title = stringResource(Res.string.save_last_played), subtitle = stringResource(Res.string.save_last_played_track_and_queue), diff --git a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/player/FullscreenPlayer.kt b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/player/FullscreenPlayer.kt index ef336cc9e..8873fafdd 100644 --- a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/player/FullscreenPlayer.kt +++ b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/player/FullscreenPlayer.kt @@ -31,8 +31,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Forward5 import androidx.compose.material.icons.filled.FullscreenExit -import androidx.compose.material.icons.filled.KeyboardDoubleArrowLeft -import androidx.compose.material.icons.filled.KeyboardDoubleArrowRight import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Replay5 @@ -91,7 +89,6 @@ import org.koin.compose.koinInject import simpmusic.composeapp.generated.resources.Res import simpmusic.composeapp.generated.resources.baseline_arrow_back_ios_new_24 import simpmusic.composeapp.generated.resources.baseline_more_vert_24 -import simpmusic.composeapp.generated.resources.five_seconds import kotlin.math.roundToLong @OptIn(ExperimentalMaterial3Api::class) @@ -121,6 +118,7 @@ fun FullscreenPlayer( val nowPlayingState by sharedViewModel.nowPlayingScreenData.collectAsStateWithLifecycle() val controllerState by sharedViewModel.controllerState.collectAsStateWithLifecycle() val timelineState by sharedViewModel.timeline.collectAsStateWithLifecycle() + val doublePressToSeek by sharedViewModel.doublePressToSeek.collectAsStateWithLifecycle() var showBottom by rememberSaveable { mutableStateOf(false) } var isSliding by rememberSaveable { @@ -154,10 +152,8 @@ fun FullscreenPlayer( // For double tap effect val coroutineScope = rememberCoroutineScope() - var doubleBackwardTapped by remember { mutableStateOf(false) } val interactionSourceBackward = remember { MutableInteractionSource() } - var doubleForwardTapped by remember { mutableStateOf(false) } val interactionSourceForward = remember { MutableInteractionSource() } var showBackwardText by remember { mutableStateOf(false) } @@ -216,14 +212,14 @@ fun FullscreenPlayer( showHideFullscreenOverlay = !showHideFullscreenOverlay }, onDoubleTap = { offset -> - coroutineScope.launch { - doubleBackwardTapped = true - val press = PressInteraction.Press(offset) - interactionSourceBackward.emit(press) - sharedViewModel.onUIEvent(UIEvent.Backward) - showBackwardText = true - interactionSourceBackward.emit(PressInteraction.Release(press)) - doubleBackwardTapped = false + if (doublePressToSeek) { + coroutineScope.launch { + val press = PressInteraction.Press(offset) + interactionSourceBackward.emit(press) + sharedViewModel.onUIEvent(UIEvent.Backward5) + showBackwardText = true + interactionSourceBackward.emit(PressInteraction.Release(press)) + } } }, ) @@ -234,17 +230,16 @@ fun FullscreenPlayer( if (it) { Row( verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .background(Color.Black.copy(alpha = 0.5f), RoundedCornerShape(16.dp)) + .padding(horizontal = 14.dp, vertical = 8.dp), ) { Icon( - Icons.Filled.KeyboardDoubleArrowLeft, + Icons.Filled.Replay5, "", tint = Color.White, - ) - Spacer(Modifier.width(4.dp)) - Text( - stringResource(Res.string.five_seconds), - color = Color.White, - style = typo().bodyMedium, + modifier = Modifier.size(36.dp), ) } } @@ -268,14 +263,14 @@ fun FullscreenPlayer( showHideFullscreenOverlay = !showHideFullscreenOverlay }, onDoubleTap = { offset -> - coroutineScope.launch { - doubleForwardTapped = true - val press = PressInteraction.Press(offset) - interactionSourceForward.emit(press) - sharedViewModel.onUIEvent(UIEvent.Forward) - showForwardText = true - interactionSourceForward.emit(PressInteraction.Release(press)) - doubleForwardTapped = false + if (doublePressToSeek) { + coroutineScope.launch { + val press = PressInteraction.Press(offset) + interactionSourceForward.emit(press) + sharedViewModel.onUIEvent(UIEvent.Forward5) + showForwardText = true + interactionSourceForward.emit(PressInteraction.Release(press)) + } } }, ) @@ -286,17 +281,16 @@ fun FullscreenPlayer( if (it) { Row( verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .background(Color.Black.copy(alpha = 0.5f), RoundedCornerShape(16.dp)) + .padding(horizontal = 14.dp, vertical = 8.dp), ) { - Text( - stringResource(Res.string.five_seconds), - color = Color.White, - style = typo().bodyMedium, - ) - Spacer(Modifier.width(4.dp)) Icon( - Icons.Filled.KeyboardDoubleArrowRight, + Icons.Filled.Forward5, "", tint = Color.White, + modifier = Modifier.size(36.dp), ) } } @@ -765,4 +759,4 @@ fun FullscreenPlayer( expect fun FullScreenRotationImmersive( onLaunch: () -> Unit = {}, onDispose: () -> Unit = {}, -) \ No newline at end of file +) diff --git a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt index 01d812bd6..ae8ad0d55 100644 --- a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt @@ -27,7 +27,12 @@ import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.material3.ripple import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -276,6 +281,7 @@ fun NowPlayingScreenContent( dismissIcon: ImageVector, onDismiss: () -> Unit = {}, ) { + val coroutineScope = rememberCoroutineScope() val screenInfo = getScreenSizeInfo() val localDensity = LocalDensity.current @@ -392,6 +398,27 @@ fun NowPlayingScreenContent( // State val isInPipMode = rememberIsInPipMode() + val doublePressToSeek by sharedViewModel.doublePressToSeek.collectAsStateWithLifecycle() + + val interactionSourceBackward = remember { MutableInteractionSource() } + val interactionSourceForward = remember { MutableInteractionSource() } + + var showBackwardText by remember { mutableStateOf(false) } + var showForwardText by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = showBackwardText) { + if (showBackwardText) { + delay(1000) + showBackwardText = false + } + } + LaunchedEffect(key1 = showForwardText) { + if (showForwardText) { + delay(1000) + showForwardText = false + } + } + val mainScrollState = rememberScrollState() var showHideMiddleLayout by rememberSaveable { @@ -1447,8 +1474,316 @@ fun NowPlayingScreenContent( .value .toInt() } - }.aspectRatio(1f), - ) + }.alpha( + if (showHideMiddleLayout) 1f else 0f, + ).aspectRatio(1f), + ) { + // IS SONG => Show Artwork + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier + .align(Alignment.Center) + .background(Color.Transparent) + .shadow( + elevation = 3.dp, + shape = RoundedCornerShape(8.dp), + spotColor = + spotShadowColor.copy( + alpha = 0.6f, + ), + ambientColor = Color.Transparent, + ), + ) { + AsyncImage( + model = + ImageRequest + .Builder(LocalPlatformContext.current) + .data(screenDataState.thumbnailURL) + .diskCachePolicy(CachePolicy.ENABLED) + .diskCacheKey(screenDataState.thumbnailURL + "BIGGER") + .crossfade(550) + .build(), + contentDescription = "", + onSuccess = { + sharedViewModel.setBitmap( + it.result.image + .toBitmap() + .asImageBitmap(), + ) + }, + contentScale = ContentScale.Crop, + placeholder = painterResource(Res.drawable.holder), + modifier = + Modifier + .align(Alignment.Center) + .padding(3.dp) + .fillMaxWidth() + .background(Color.Transparent) + .aspectRatio( + if (!screenDataState.isVideo) 1f else 16f / 9, + ).clip( + RoundedCornerShape(8.dp), + ).alpha( + if (!screenDataState.isVideo || !shouldShowVideo) 1f else 0f, + ), + ) + if (doublePressToSeek && (!screenDataState.isVideo || !shouldShowVideo)) { + Row( + modifier = Modifier + .matchParentSize() + .padding(3.dp) + .clip(RoundedCornerShape(8.dp)) + ) { + // Left side (Seek backward) + Box( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .indication( + interactionSource = interactionSourceBackward, + indication = ripple(), + ) + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { offset -> + coroutineScope.launch { + val press = PressInteraction.Press(offset) + interactionSourceBackward.emit(press) + sharedViewModel.onUIEvent(UIEvent.Backward5) + showBackwardText = true + interactionSourceBackward.emit(PressInteraction.Release(press)) + } + } + ) + }, + contentAlignment = Alignment.Center + ) { + Crossfade(showBackwardText) { + if (it) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background(Color.Black.copy(alpha = 0.5f), RoundedCornerShape(16.dp)) + .padding(horizontal = 14.dp, vertical = 8.dp) + ) { + Icon( + Icons.Rounded.Replay5, + "", + tint = Color.White, + modifier = Modifier.size(36.dp) + ) + } + } + } + } + // Right side (Seek forward) + Box( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .indication( + interactionSource = interactionSourceForward, + indication = ripple(), + ) + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { offset -> + coroutineScope.launch { + val press = PressInteraction.Press(offset) + interactionSourceForward.emit(press) + sharedViewModel.onUIEvent(UIEvent.Forward5) + showForwardText = true + interactionSourceForward.emit(PressInteraction.Release(press)) + } + } + ) + }, + contentAlignment = Alignment.Center + ) { + Crossfade(showForwardText) { + if (it) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background(Color.Black.copy(alpha = 0.5f), RoundedCornerShape(16.dp)) + .padding(horizontal = 14.dp, vertical = 8.dp) + ) { + Icon( + Icons.Rounded.Forward5, + "", + tint = Color.White, + modifier = Modifier.size(36.dp) + ) + } + } + } + } + } + } + } + + // IS VIDEO => Show Video + androidx.compose.animation.AnimatedVisibility( + visible = screenDataState.isVideo && shouldShowVideo, + modifier = Modifier.align(Alignment.Center), + ) { + var internalShowSubtitle by rememberSaveable { + mutableStateOf(true) + } + Box( + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(16f / 9) + .clip( + RoundedCornerShape(8.dp), + ).background( + md_theme_dark_background, + ), + ) { + // Player + Box(Modifier.fillMaxSize()) { + MediaPlayerViewWithSubtitle( + playerName = MAIN_PLAYER, + modifier = Modifier.align(Alignment.Center), + shouldShowSubtitle = internalShowSubtitle, + shouldPip = false, + shouldScaleDownSubtitle = true, + timelineState = timelineState, + lyricsData = screenDataState.lyricsData?.lyrics, + translatedLyricsData = screenDataState.lyricsData?.translatedLyrics?.first, + isInPipMode = isInPipMode, + mainTextStyle = typo().bodyLarge, + translatedTextStyle = typo().bodyMedium, + ) + } + Box( + modifier = + Modifier + .fillMaxSize() + .clickable( + onClick = { showHideFullscreenOverlay = !showHideFullscreenOverlay }, + indication = null, + interactionSource = + remember { + MutableInteractionSource() + }, + ), + ) { + Crossfade( + targetState = showHideFullscreenOverlay, + ) { + if (it) { + Box( + modifier = + Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colorStops = + arrayOf( + 0.03f to blackMoreOverlay, + 0.15f to overlay, + 0.8f to Color.Transparent, + ), + ), + ), + ) { + IconButton(onClick = { + onDismiss() + navController.navigate( + FullscreenDestination, + ) + }, Modifier.align(Alignment.TopEnd)) { + Icon( + painter = painterResource(Res.drawable.baseline_fullscreen_24), + contentDescription = "", + tint = Color.White, + ) + } + Row( + Modifier + .align(Alignment.Center) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + FilledTonalIconButton( + colors = + IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent, + ), + modifier = + Modifier + .size(48.dp) + .aspectRatio(1f) + .clip( + CircleShape, + ), + onClick = { + sharedViewModel.onUIEvent(UIEvent.Backward) + }, + ) { + Icon( + imageVector = Icons.Rounded.Replay5, + tint = Color.White, + contentDescription = "", + modifier = + Modifier + .size(36.dp) + .alpha(0.8f), + ) + } + FilledTonalIconButton( + colors = + IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent, + ), + modifier = + Modifier + .size(48.dp) + .aspectRatio(1f) + .clip( + CircleShape, + ), + onClick = { + sharedViewModel.onUIEvent(UIEvent.Forward) + }, + ) { + Icon( + imageVector = Icons.Rounded.Forward5, + tint = Color.White, + contentDescription = "", + modifier = + Modifier + .size(36.dp) + .alpha(0.8f), + ) + } + } + if (screenDataState.lyricsData != null) { + IconButton(onClick = { + internalShowSubtitle = !internalShowSubtitle + }, Modifier.align(Alignment.BottomEnd)) { + Icon( + imageVector = + if (internalShowSubtitle) { + Icons.Filled.SubtitlesOff + } else { + Icons.Filled.Subtitles + }, + contentDescription = "", + tint = Color.White, + ) + } + } + } + } + } + } + } + } + } Spacer( modifier = @@ -2571,4 +2906,4 @@ fun NowPlayingScreenContent( } } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt index ea4b276de..87c805e24 100644 --- a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt @@ -13,6 +13,8 @@ import com.maxrave.domain.data.entities.DownloadState import com.maxrave.domain.data.entities.GoogleAccountEntity import com.maxrave.domain.extension.toNetScapeString import com.maxrave.domain.manager.DataStoreManager +import com.maxrave.domain.manager.DataStoreManager.Values.FALSE +import com.maxrave.domain.manager.DataStoreManager.Values.TRUE import com.maxrave.domain.mediaservice.handler.DownloadHandler import com.maxrave.domain.repository.AccountRepository import com.maxrave.domain.repository.CacheRepository @@ -50,6 +52,8 @@ import simpmusic.composeapp.generated.resources.clear_thumbnail_cache import simpmusic.composeapp.generated.resources.restore_failed import simpmusic.composeapp.generated.resources.restore_in_progress +private const val DOUBLE_PRESS_TO_SEEK_KEY = "double_press_to_seek" + class SettingsViewModel( private val dataStoreManager: DataStoreManager, private val commonRepository: CommonRepository, @@ -70,6 +74,8 @@ class SettingsViewModel( val normalizeVolume: StateFlow = _normalizeVolume private var _skipSilent: MutableStateFlow = MutableStateFlow(null) val skipSilent: StateFlow = _skipSilent + private var _doublePressToSeek: MutableStateFlow = MutableStateFlow(null) + val doublePressToSeek: StateFlow = _doublePressToSeek private var _savedPlaybackState: MutableStateFlow = MutableStateFlow(null) val savedPlaybackState: StateFlow = _savedPlaybackState private var _saveRecentSongAndQueue: MutableStateFlow = MutableStateFlow(null) @@ -235,6 +241,7 @@ class SettingsViewModel( getLoggedIn() getNormalizeVolume() getSkipSilent() + getDoublePressToSeek() getSavedPlaybackState() getSendBackToGoogle() getSaveRecentSongAndQueue() @@ -1192,6 +1199,21 @@ class SettingsViewModel( } } + fun getDoublePressToSeek() { + viewModelScope.launch { + dataStoreManager.getString(DOUBLE_PRESS_TO_SEEK_KEY).collect { doublePressToSeek -> + _doublePressToSeek.emit(doublePressToSeek) + } + } + } + + fun setDoublePressToSeek(enable: Boolean) { + viewModelScope.launch { + dataStoreManager.putString(DOUBLE_PRESS_TO_SEEK_KEY, if (enable) TRUE else FALSE) + getDoublePressToSeek() + } + } + fun getSavedPlaybackState() { viewModelScope.launch { dataStoreManager.saveStateOfPlayback.collect { savedPlaybackState -> @@ -1632,4 +1654,4 @@ expect fun getPackageName(): String expect fun getFileDir(): String -expect fun changeLanguageNative(code: String) \ No newline at end of file +expect fun changeLanguageNative(code: String) diff --git a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/viewModel/SharedViewModel.kt b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/viewModel/SharedViewModel.kt index ab047c27d..ed94247b8 100644 --- a/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/viewModel/SharedViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/viewModel/SharedViewModel.kt @@ -103,6 +103,8 @@ import java.io.FileOutputStream import kotlin.math.abs import kotlin.reflect.KClass +private const val DOUBLE_PRESS_TO_SEEK_KEY = "double_press_to_seek" + @OptIn(ExperimentalCoroutinesApi::class) class SharedViewModel( private val dataStoreManager: DataStoreManager, @@ -207,6 +209,11 @@ class SharedViewModel( val likeStatus: StateFlow = _likeStatus val openAppTime: StateFlow = dataStoreManager.openAppTime.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0) + val doublePressToSeek: StateFlow = + dataStoreManager + .getString(DOUBLE_PRESS_TO_SEEK_KEY) + .map { it == TRUE } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), false) private val _shareSavedLyrics: MutableStateFlow = MutableStateFlow(true) val shareSavedLyrics: StateFlow get() = _shareSavedLyrics @@ -828,6 +835,30 @@ class SharedViewModel( dataStoreManager.setPlayerVolume(newVolume) mediaPlayerHandler.onPlayerEvent(PlayerEvent.UpdateVolume(newVolume)) } + + UIEvent.Forward5 -> { + val current = timeline.value.current + val total = timeline.value.total + if (total > 0) { + val newProgress = (current + 5000).coerceAtMost(total) + val progressPercent = (newProgress.toFloat() / total.toFloat()) * 100f + mediaPlayerHandler.onPlayerEvent( + PlayerEvent.UpdateProgress(progressPercent), + ) + } + } + + UIEvent.Backward5 -> { + val current = timeline.value.current + val total = timeline.value.total + if (total > 0) { + val newProgress = (current - 5000).coerceAtLeast(0) + val progressPercent = (newProgress.toFloat() / total.toFloat()) * 100f + mediaPlayerHandler.onPlayerEvent( + PlayerEvent.UpdateProgress(progressPercent), + ) + } + } } } @@ -1931,6 +1962,10 @@ sealed class UIEvent { ) : UIEvent() data object ToggleLike : UIEvent() + + data object Forward5 : UIEvent() + + data object Backward5 : UIEvent() } enum class LyricsProvider { @@ -1980,23 +2015,3 @@ data class NowPlayingScreenData( ) } } - -data class VoteData( - val id: String, - val vote: Int, - val state: VoteState, -) - -sealed class VoteState { - data object Idle : VoteState() - - data object Loading : VoteState() - - data class Success( - val upvote: Boolean, - ) : VoteState() - - data class Error( - val message: String, - ) : VoteState() -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 430704356..90bb44358 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,17 +46,13 @@ common = "1.22.0" json = "1.11.0" gemini-kotlin = "4.0.2" slf4j = "1.7.36" -sentry-android = "8.43.1" -sentry-gradle-android = "6.10.0" -sentry-jvm = "8.43.1" -newpipe = "v0.26.1" -brave-newpipe = "v0.26.3" -# org.json ships with the Android SDK but is absent on JVM desktop; PipePipeExtractor references it. -org-json = "20231013" -pipepipe = "c3139c584d" -webkit = "1.16.0" -kermit = "2.1.0" -paging-common = "3.5.0" +sentry-android = "8.28.0" +sentry-gradle-android = "5.12.2" +sentry-jvm = "8.28.0" +newpipe = "0.24.8" +webkit = "1.14.0" +kermit = "2.0.8" +paging-common = "3.3.6" glance = "1.1.1" liquid-glass = "2.0.0" liquid-glass-shapes = "1.2.0" @@ -256,5 +252,3 @@ packagedeps = { id = "io.github.kdroidfilter.compose.linux.packagedeps", version # JVM osdetector = { id = "com.google.osdetector", version.ref = "osdetector" } -vlc-setup = { id = "ir.mahozad.vlc-setup", version = "0.1.0" } -conveyor = { id = "dev.hydraulic.conveyor", version.ref = "conveyor" }