Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions composeApp/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@
<item quantity="other">%1$d lines selected</item>
</plurals>

<string name="action_start">Start</string>
<string name="action_ok">OK</string>
<string name="action_play">Play</string>
<string name="action_next_song">Next song</string>
Expand All @@ -327,7 +328,13 @@
<string name="action_cancel_download">Cancel download</string>
<string name="action_sleep_timer">Sleep timer</string>
<string name="action_sleep_timer_enabled">Sleep timer - %1$s left </string>
<string name="action_sleep_timer_songs_enabled">Sleep timer - %1$d songs left</string>
<string name="action_sleep_timer_queue_enabled">Sleep timer - end of queue</string>
<string name="action_sleep_timer_songs">Stop after %1$d songs</string>
<string name="action_sleep_timer_queue">Stop at end of current queue</string>
<string name="action_disable_sleep_timer">Turn off timer</string>
<string name="title_sleep_timer_time">Time</string>
<string name="title_sleep_timer_songs">Songs</string>
<string name="option_playback_speed">Playback speed</string>
<string name="action_navigate_back">Navigate up</string>
<string name="action_log_out">Logout</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,103 @@ import kotlinx.coroutines.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import paige.navic.shared.MediaPlayerViewModel
import kotlin.time.Clock
import kotlin.time.Duration
import kotlin.time.Instant

sealed interface SleepTimerMode {
data object Disabled : SleepTimerMode
data class Time(val endTime: Instant) : SleepTimerMode
data class Songs(val remaining: Int) : SleepTimerMode
data object EndOfQueue : SleepTimerMode
}

class SleepTimerManager(
private val player: MediaPlayerViewModel
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var job: Job? = null
var endTimeStamp: Instant? = null
private set
private var observerJob: Job? = null
private var remainingSongs = 0

private val _mode = MutableStateFlow<SleepTimerMode>(SleepTimerMode.Disabled)
val mode = _mode.asStateFlow()

val endTimeStamp: Instant?
get() = (mode.value as? SleepTimerMode.Time)?.endTime

val timeLeft: Duration?
get() = endTimeStamp?.let { it - Clock.System.now() }

fun startTimer(duration: Duration) {
job?.cancel()
stopTimer()
val endTime = Clock.System.now() + duration
_mode.value = SleepTimerMode.Time(endTime)

job = scope.launch {
endTimeStamp = Clock.System.now() + duration

delay(duration)
player.pause()
stopTimer()
}
}

fun startSongsTimer(count: Int) {
stopTimer()
remainingSongs = count
_mode.value = SleepTimerMode.Songs(count)
observeSongs()
}

fun startEndOfQueueTimer() {
stopTimer()
val state = player.uiState.value
remainingSongs = if (state.currentIndex != -1) {
state.queue.size - state.currentIndex
} else {
0
}

if (remainingSongs > 0) {
_mode.value = SleepTimerMode.EndOfQueue
observeSongs()
}
}

private fun observeSongs() {
observerJob?.cancel()
observerJob = scope.launch {
player.uiState
.map { it.currentIndex }
.distinctUntilChanged()
.drop(1)
.collect {
remainingSongs--
if (remainingSongs <= 0) {
player.pause()
stopTimer()
} else {
val currentMode = _mode.value
if (currentMode is SleepTimerMode.Songs) {
_mode.value = SleepTimerMode.Songs(remainingSongs)
}
}
}
}
}

fun stopTimer() {
job?.cancel()
job = null
endTimeStamp = null
observerJob?.cancel()
observerJob = null
remainingSongs = 0
_mode.value = SleepTimerMode.Disabled
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package paige.navic.ui.components.common

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp

@Composable
fun OptionCard(
label: String,
icon: ImageVector,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isActive: Boolean = false
) {
val containerColor = if (isActive) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
}
val contentColor = if (isActive) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}

Surface(
modifier = modifier.clip(MaterialTheme.shapes.medium),
color = containerColor,
contentColor = contentColor,
shape = MaterialTheme.shapes.medium,
onClick = onClick
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
maxLines = 1
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,23 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.kyant.capsule.ContinuousCapsule
import com.russhwolf.settings.Settings
import kotlinx.coroutines.launch
import navic.composeapp.generated.resources.Res
import navic.composeapp.generated.resources.action_log_out
import navic.composeapp.generated.resources.action_sleep_timer
import navic.composeapp.generated.resources.action_sleep_timer_enabled
import navic.composeapp.generated.resources.action_sleep_timer_queue_enabled
import navic.composeapp.generated.resources.action_sleep_timer_songs_enabled
import navic.composeapp.generated.resources.action_view_shares
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import paige.navic.LocalNavStack
import paige.navic.domain.manager.LoginManager
import paige.navic.domain.manager.SleepTimerManager
import paige.navic.domain.manager.SleepTimerMode
import paige.navic.icons.Icons
import paige.navic.icons.outlined.Bedtime
import paige.navic.icons.outlined.Logout
Expand All @@ -62,7 +66,7 @@ fun AccountSheet(

var sleepTimerSheetOpen by rememberSaveable { mutableStateOf(false) }
val sleepTimerManager = koinInject<SleepTimerManager>()
val sleepTimerLeft = sleepTimerManager.timeLeft
val sleepTimerMode by sleepTimerManager.mode.collectAsStateWithLifecycle()

val sheetState = rememberBottomSheetState(
initialValue = SheetValue.Hidden,
Expand Down Expand Up @@ -159,17 +163,31 @@ fun AccountSheet(
contentPadding = contentPadding,
color = color
) {
if (sleepTimerLeft != null) {
val isEnabled = sleepTimerMode !is SleepTimerMode.Disabled
if (isEnabled) {
Icon(
imageVector = Icons.Outlined.Bedtime,
contentDescription = null,
tint = MaterialTheme.colorScheme.positive
)
Text(
text = stringResource(
resource = Res.string.action_sleep_timer_enabled,
sleepTimerLeft.label()
),
text = when (val mode = sleepTimerMode) {
is SleepTimerMode.Time -> stringResource(
Res.string.action_sleep_timer_enabled,
sleepTimerManager.timeLeft?.label() ?: ""
)

is SleepTimerMode.Songs -> stringResource(
Res.string.action_sleep_timer_songs_enabled,
mode.remaining
)

is SleepTimerMode.EndOfQueue -> stringResource(
Res.string.action_sleep_timer_queue_enabled
)

else -> ""
},
color = MaterialTheme.colorScheme.positive,
modifier = Modifier.weight(1f)
)
Expand Down
Loading
Loading