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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
Expand All @@ -15,6 +17,7 @@ import dev.xitee.sleeptimer.feature.timer.about.AboutScreen
import dev.xitee.sleeptimer.feature.timer.settings.SettingsScreen
import dev.xitee.sleeptimer.feature.timer.settings.ThemePickerScreen
import dev.xitee.sleeptimer.feature.timer.timer.AppOrientationController
import dev.xitee.sleeptimer.feature.timer.timer.AppOrientationViewModel
import dev.xitee.sleeptimer.feature.timer.timer.DeviceOrientation
import dev.xitee.sleeptimer.feature.timer.timer.TimerScreen
import dev.xitee.sleeptimer.feature.timer.timer.rememberDeviceOrientation
Expand All @@ -30,7 +33,9 @@ fun SleepTimerNavHost() {
// briefly shows Timer's counter-rotated content being rotated by the
// window manager, appearing upside-down. In natural portrait there is no
// orientation flip, so the fade is kept.
val deviceOrientation by rememberDeviceOrientation()
val autoRotateMode by hiltViewModel<AppOrientationViewModel>()
.autoRotateMode.collectAsStateWithLifecycle()
val deviceOrientation by rememberDeviceOrientation(mode = autoRotateMode)
val animate = deviceOrientation == DeviceOrientation.PORTRAIT

AppOrientationController(orientation = deviceOrientation, lockPortrait = isTimer)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.xitee.sleeptimer.core.data.model

enum class AutoRotateMode {
System,
Always,
Portrait,
;

companion object {
val Default: AutoRotateMode = System

fun fromStorage(value: String?): AutoRotateMode =
entries.firstOrNull { it.name == value } ?: Default
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class UserSettings(
val hapticFeedbackEnabled: Boolean = true,
val theme: ThemeId = ThemeId.Default,
val starsEnabled: Boolean = true,
val autoRotateMode: AutoRotateMode = AutoRotateMode.Default,
val stepMinutes: Int = 5,
val presetMinutes: Int = 15,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.xitee.sleeptimer.core.data.repository

import dev.xitee.sleeptimer.core.data.model.AutoRotateMode
import dev.xitee.sleeptimer.core.data.model.ThemeId
import dev.xitee.sleeptimer.core.data.model.UserSettings
import kotlinx.coroutines.flow.Flow
Expand All @@ -15,6 +16,7 @@ interface SettingsRepository {
suspend fun updateHapticFeedback(enabled: Boolean)
suspend fun updateTheme(theme: ThemeId)
suspend fun updateStarsEnabled(enabled: Boolean)
suspend fun updateAutoRotateMode(mode: AutoRotateMode)
suspend fun updateStepMinutes(minutes: Int)
suspend fun updatePresetMinutes(minutes: Int)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import dev.xitee.sleeptimer.core.data.model.AutoRotateMode
import dev.xitee.sleeptimer.core.data.model.ThemeId
import dev.xitee.sleeptimer.core.data.model.UserSettings
import kotlinx.coroutines.flow.Flow
Expand All @@ -28,6 +29,7 @@ class SettingsRepositoryImpl @Inject constructor(
val HAPTIC_FEEDBACK = booleanPreferencesKey("haptic_feedback")
val THEME = stringPreferencesKey("theme")
val STARS_ENABLED = booleanPreferencesKey("stars_enabled")
val AUTO_ROTATE_MODE = stringPreferencesKey("auto_rotate_mode")
val STEP_MINUTES = intPreferencesKey("step_minutes")
val PRESET_MINUTES = intPreferencesKey("preset_minutes")
}
Expand All @@ -46,6 +48,7 @@ class SettingsRepositoryImpl @Inject constructor(
hapticFeedbackEnabled = prefs[HAPTIC_FEEDBACK] ?: d.hapticFeedbackEnabled,
theme = ThemeId.fromStorage(prefs[THEME]),
starsEnabled = prefs[STARS_ENABLED] ?: d.starsEnabled,
autoRotateMode = AutoRotateMode.fromStorage(prefs[AUTO_ROTATE_MODE]),
stepMinutes = prefs[STEP_MINUTES] ?: d.stepMinutes,
presetMinutes = prefs[PRESET_MINUTES] ?: d.presetMinutes,
)
Expand Down Expand Up @@ -87,6 +90,10 @@ class SettingsRepositoryImpl @Inject constructor(
dataStore.edit { it[STARS_ENABLED] = enabled }
}

override suspend fun updateAutoRotateMode(mode: AutoRotateMode) {
dataStore.edit { it[AUTO_ROTATE_MODE] = mode.name }
}

override suspend fun updateStepMinutes(minutes: Int) {
dataStore.edit { it[STEP_MINUTES] = minutes.coerceIn(1, 30) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.MusicOff
import androidx.compose.material.icons.filled.PhoneAndroid
import androidx.compose.material.icons.filled.ScreenRotation
import androidx.compose.material.icons.filled.Vibration
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material3.Icon
Expand All @@ -53,9 +54,12 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.xitee.sleeptimer.core.service.shizuku.ShizukuManager
import dev.xitee.sleeptimer.core.data.model.AutoRotateMode
import dev.xitee.sleeptimer.feature.timer.R
import dev.xitee.sleeptimer.feature.timer.settings.components.AutoRotateModeDialog
import dev.xitee.sleeptimer.feature.timer.settings.components.FadeOutSlider
import dev.xitee.sleeptimer.feature.timer.settings.components.ScreenLockMethodDialog
import dev.xitee.sleeptimer.feature.timer.settings.components.labelRes
import dev.xitee.sleeptimer.feature.timer.settings.components.SettingsToggleRow
import dev.xitee.sleeptimer.feature.timer.settings.components.SettingsTopBar
import dev.xitee.sleeptimer.feature.timer.settings.components.ShizukuRequiredDialog
Expand Down Expand Up @@ -115,6 +119,7 @@ private fun SettingsContent(
var shizukuDialogExplanation by remember { mutableStateOf<String?>(null) }
var pendingShizukuToggle by remember { mutableStateOf<(() -> Unit)?>(null) }
var showMethodDialog by remember { mutableStateOf(false) }
var showAutoRotateDialog by remember { mutableStateOf(false) }

fun requestWithShizuku(explanation: String, enableAction: () -> Unit) {
if (viewModel.isShizukuReady()) {
Expand Down Expand Up @@ -167,6 +172,14 @@ private fun SettingsContent(
)
}

if (showAutoRotateDialog) {
AutoRotateModeDialog(
selected = uiState.settings.autoRotateMode,
onSelect = { viewModel.updateAutoRotateMode(it) },
onDismiss = { showAutoRotateDialog = false },
)
}

// Auto-complete the pending toggle if Shizuku transitions to Ready while dialog is open.
LaunchedEffect(uiState.shizukuState, pendingShizukuToggle) {
if (pendingShizukuToggle != null && uiState.shizukuState == ShizukuManager.State.Ready) {
Expand Down Expand Up @@ -224,6 +237,12 @@ private fun SettingsContent(
onCheckedChange = { viewModel.updateStarsEnabled(it) },
enabled = AppThemes.byId(uiState.settings.theme).allowStars,
)
SettingsNavigationRow(
icon = Icons.Default.ScreenRotation,
title = stringResource(R.string.auto_rotate_title),
description = stringResource(uiState.settings.autoRotateMode.labelRes),
onClick = { showAutoRotateDialog = true },
)

SectionHeader(stringResource(R.string.category_sleep_timer))
SettingsToggleRow(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.xitee.sleeptimer.core.data.model.AutoRotateMode
import dev.xitee.sleeptimer.core.data.model.ThemeId
import dev.xitee.sleeptimer.core.data.repository.SettingsRepository
import dev.xitee.sleeptimer.core.service.shizuku.ShizukuManager
Expand Down Expand Up @@ -100,6 +101,10 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch { settingsRepository.updateStarsEnabled(enabled) }
}

fun updateAutoRotateMode(mode: AutoRotateMode) {
viewModelScope.launch { settingsRepository.updateAutoRotateMode(mode) }
}

fun updateStepMinutes(minutes: Int) {
viewModelScope.launch { settingsRepository.updateStepMinutes(minutes) }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package dev.xitee.sleeptimer.feature.timer.settings.components

import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PhoneAndroid
import androidx.compose.material.icons.filled.ScreenLockPortrait
import androidx.compose.material.icons.filled.ScreenRotation
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import dev.xitee.sleeptimer.core.data.model.AutoRotateMode
import dev.xitee.sleeptimer.feature.timer.R
import dev.xitee.sleeptimer.feature.timer.theme.appTheme

/** Title string shown in the settings row's description slot for the selected mode. */
@get:StringRes
val AutoRotateMode.labelRes: Int
get() = when (this) {
AutoRotateMode.System -> R.string.auto_rotate_system_title
AutoRotateMode.Always -> R.string.auto_rotate_always_title
AutoRotateMode.Portrait -> R.string.auto_rotate_portrait_title
}

@Composable
fun AutoRotateModeDialog(
selected: AutoRotateMode,
onSelect: (AutoRotateMode) -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = { Icon(Icons.Default.ScreenRotation, contentDescription = null) },
title = { Text(stringResource(R.string.auto_rotate_dialog_title)) },
text = {
Column {
ModeOption(
icon = Icons.Default.PhoneAndroid,
title = stringResource(R.string.auto_rotate_system_title),
description = stringResource(R.string.auto_rotate_system_description),
isSelected = selected == AutoRotateMode.System,
onClick = { onSelect(AutoRotateMode.System); onDismiss() },
)
Spacer(Modifier.height(8.dp))
ModeOption(
icon = Icons.Default.ScreenRotation,
title = stringResource(R.string.auto_rotate_always_title),
description = stringResource(R.string.auto_rotate_always_description),
isSelected = selected == AutoRotateMode.Always,
onClick = { onSelect(AutoRotateMode.Always); onDismiss() },
)
Spacer(Modifier.height(8.dp))
ModeOption(
icon = Icons.Default.ScreenLockPortrait,
title = stringResource(R.string.auto_rotate_portrait_title),
description = stringResource(R.string.auto_rotate_portrait_description),
isSelected = selected == AutoRotateMode.Portrait,
onClick = { onSelect(AutoRotateMode.Portrait); onDismiss() },
)
}
},
confirmButton = {},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.shizuku_action_cancel))
}
},
)
}

@Composable
private fun ModeOption(
icon: ImageVector,
title: String,
description: String,
isSelected: Boolean,
onClick: () -> Unit,
) {
val accent = appTheme().accent
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
icon,
contentDescription = null,
tint = if (isSelected) accent else LocalContentColor.current,
modifier = Modifier.size(28.dp),
)
Spacer(Modifier.width(16.dp))
Column {
Text(
title,
style = MaterialTheme.typography.titleMedium,
color = if (isSelected) accent else LocalContentColor.current,
fontWeight = if (isSelected) FontWeight.SemiBold else null,
)
Text(description, style = MaterialTheme.typography.bodyMedium)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dev.xitee.sleeptimer.feature.timer.timer

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.xitee.sleeptimer.core.data.model.AutoRotateMode
import dev.xitee.sleeptimer.core.data.model.UserSettings
import dev.xitee.sleeptimer.core.data.repository.SettingsRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

// Exposes the auto-rotate setting to SleepTimerNavHost, which sits outside any
// NavBackStackEntry and so has no destination-scoped ViewModel of its own.
@HiltViewModel
class AppOrientationViewModel @Inject constructor(
settingsRepository: SettingsRepository,
) : ViewModel() {

val autoRotateMode: StateFlow<AutoRotateMode> =
settingsRepository.settings
.map { it.autoRotateMode }
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
UserSettings().autoRotateMode,
)
}
Loading
Loading