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" }