feat: [#28][#29] écran paramètres (thème, sync, notifs, comptes CalDAV) + thème dynamique Material You piloté par DataStore
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
package com.planify.mobile.data.preferences
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
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 androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore("app_prefs")
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class AppPreferences @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) {
|
||||||
|
private val store = context.dataStore
|
||||||
|
|
||||||
|
// ── Keys ─────────────────────────────────────────────────────────────────
|
||||||
|
private val keyTheme = stringPreferencesKey("theme_mode")
|
||||||
|
private val keySyncEnabled = booleanPreferencesKey("sync_enabled")
|
||||||
|
private val keySyncInterval = intPreferencesKey("sync_interval_minutes")
|
||||||
|
private val keyNotifications = booleanPreferencesKey("notifications_enabled")
|
||||||
|
|
||||||
|
// ── Reads ─────────────────────────────────────────────────────────────────
|
||||||
|
val themeMode: Flow<ThemeMode> = store.data.map { prefs ->
|
||||||
|
ThemeMode.fromKey(prefs[keyTheme] ?: ThemeMode.SYSTEM.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
val syncEnabled: Flow<Boolean> = store.data.map { it[keySyncEnabled] ?: true }
|
||||||
|
|
||||||
|
val syncIntervalMinutes: Flow<Int> = store.data.map { it[keySyncInterval] ?: 30 }
|
||||||
|
|
||||||
|
val notificationsEnabled: Flow<Boolean> = store.data.map { it[keyNotifications] ?: true }
|
||||||
|
|
||||||
|
// ── Writes ────────────────────────────────────────────────────────────────
|
||||||
|
suspend fun setThemeMode(mode: ThemeMode) =
|
||||||
|
store.edit { it[keyTheme] = mode.key }
|
||||||
|
|
||||||
|
suspend fun setSyncEnabled(enabled: Boolean) =
|
||||||
|
store.edit { it[keySyncEnabled] = enabled }
|
||||||
|
|
||||||
|
suspend fun setSyncInterval(minutes: Int) =
|
||||||
|
store.edit { it[keySyncInterval] = minutes }
|
||||||
|
|
||||||
|
suspend fun setNotificationsEnabled(enabled: Boolean) =
|
||||||
|
store.edit { it[keyNotifications] = enabled }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ThemeMode(val key: String, val label: String) {
|
||||||
|
SYSTEM("SYSTEM", "Système"),
|
||||||
|
LIGHT("LIGHT", "Clair"),
|
||||||
|
DARK("DARK", "Sombre");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromKey(key: String) = entries.firstOrNull { it.key == key } ?: SYSTEM
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ fun MainScreen(viewModel: DrawerViewModel = hiltViewModel()) {
|
|||||||
Route.Scheduled.path to "Planifié",
|
Route.Scheduled.path to "Planifié",
|
||||||
Route.Search.path to "Recherche",
|
Route.Search.path to "Recherche",
|
||||||
Route.Filter.path to "Filtres",
|
Route.Filter.path to "Filtres",
|
||||||
|
Route.Settings.path to "Paramètres",
|
||||||
)
|
)
|
||||||
val title = drawerTitles[currentRoute]
|
val title = drawerTitles[currentRoute]
|
||||||
?: projects.find { "project/${it.id}" == currentRoute }?.name
|
?: projects.find { "project/${it.id}" == currentRoute }?.name
|
||||||
@@ -145,8 +146,11 @@ fun MainScreen(viewModel: DrawerViewModel = hiltViewModel()) {
|
|||||||
NavigationDrawerItem(
|
NavigationDrawerItem(
|
||||||
icon = { Icon(Icons.Outlined.Settings, null) },
|
icon = { Icon(Icons.Outlined.Settings, null) },
|
||||||
label = { Text("Paramètres") },
|
label = { Text("Paramètres") },
|
||||||
selected = false,
|
selected = currentRoute == Route.Settings.path,
|
||||||
onClick = { scope.launch { drawerState.close() } },
|
onClick = {
|
||||||
|
navController.navigate(Route.Settings.path)
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
},
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.planify.mobile.ui.label.LabelScreen
|
|||||||
import com.planify.mobile.ui.project.ProjectScreen
|
import com.planify.mobile.ui.project.ProjectScreen
|
||||||
import com.planify.mobile.ui.scheduled.ScheduledScreen
|
import com.planify.mobile.ui.scheduled.ScheduledScreen
|
||||||
import com.planify.mobile.ui.search.SearchScreen
|
import com.planify.mobile.ui.search.SearchScreen
|
||||||
|
import com.planify.mobile.ui.settings.SettingsScreen
|
||||||
import com.planify.mobile.ui.today.TodayScreen
|
import com.planify.mobile.ui.today.TodayScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -71,5 +72,9 @@ fun PlanifyNavHost(
|
|||||||
onTaskClick = { /* TODO: ouvrir édition */ },
|
onTaskClick = { /* TODO: ouvrir édition */ },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(Route.Settings.path) {
|
||||||
|
SettingsScreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
package com.planify.mobile.ui.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.AccountCircle
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.Sync
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.SegmentedButton
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.planify.mobile.data.preferences.ThemeMode
|
||||||
|
import com.planify.mobile.domain.model.Source
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
val discovery by viewModel.discoveryInProgress.collectAsState()
|
||||||
|
var showAddAccount by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
// ── Apparence ───────────────────────────────────────────────────────
|
||||||
|
SectionTitle("Apparence")
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Thème") },
|
||||||
|
supportingContent = {
|
||||||
|
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ThemeMode.entries.forEachIndexed { index, mode ->
|
||||||
|
SegmentedButton(
|
||||||
|
selected = state.themeMode == mode,
|
||||||
|
onClick = { viewModel.setTheme(mode) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(index, ThemeMode.entries.size),
|
||||||
|
label = { Text(mode.label) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(Modifier.padding(horizontal = 16.dp))
|
||||||
|
|
||||||
|
// ── Synchronisation ─────────────────────────────────────────────────
|
||||||
|
SectionTitle("Synchronisation")
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Sync automatique") },
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = state.syncEnabled,
|
||||||
|
onCheckedChange = viewModel::setSyncEnabled,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (state.syncEnabled) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Intervalle") },
|
||||||
|
supportingContent = {
|
||||||
|
val options = listOf(15 to "15 min", 30 to "30 min", 60 to "1 h", 240 to "4 h")
|
||||||
|
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
options.forEachIndexed { index, (mins, label) ->
|
||||||
|
SegmentedButton(
|
||||||
|
selected = state.syncIntervalMinutes == mins,
|
||||||
|
onClick = { viewModel.setSyncInterval(mins) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(index, options.size),
|
||||||
|
label = { Text(label) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Synchroniser maintenant") },
|
||||||
|
trailingContent = {
|
||||||
|
IconButton(onClick = viewModel::syncNow) {
|
||||||
|
Icon(Icons.Outlined.Sync, contentDescription = "Sync")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(Modifier.padding(horizontal = 16.dp))
|
||||||
|
|
||||||
|
// ── Notifications ────────────────────────────────────────────────────
|
||||||
|
SectionTitle("Notifications")
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Rappels activés") },
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = state.notificationsEnabled,
|
||||||
|
onCheckedChange = viewModel::setNotificationsEnabled,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(Modifier.padding(horizontal = 16.dp))
|
||||||
|
|
||||||
|
// ── Comptes CalDAV ────────────────────────────────────────────────────
|
||||||
|
SectionTitle("Comptes CalDAV")
|
||||||
|
state.caldavSources.forEach { source ->
|
||||||
|
CalDavSourceRow(source = source, onDelete = { viewModel.removeCalDavAccount(source) })
|
||||||
|
}
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Ajouter un compte") },
|
||||||
|
leadingContent = { Icon(Icons.Outlined.Add, contentDescription = null) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.let { mod ->
|
||||||
|
mod.then(
|
||||||
|
Modifier.padding(0.dp).run {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = null,
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = { showAddAccount = true },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Outlined.Add, contentDescription = null)
|
||||||
|
Text("Ajouter un compte CalDAV", modifier = Modifier.padding(start = 8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discovery.first) {
|
||||||
|
ListItem(headlineContent = { Text("Connexion en cours…") })
|
||||||
|
}
|
||||||
|
discovery.second?.let { error ->
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAddAccount) {
|
||||||
|
AddCalDavAccountDialog(
|
||||||
|
onDismiss = { showAddAccount = false },
|
||||||
|
onConfirm = { url, user, pwd ->
|
||||||
|
viewModel.addCalDavAccount(url, user, pwd)
|
||||||
|
showAddAccount = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionTitle(text: String) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CalDavSourceRow(source: Source, onDelete: () -> Unit) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = { Icon(Icons.Outlined.AccountCircle, contentDescription = null) },
|
||||||
|
headlineContent = { Text(source.displayName) },
|
||||||
|
supportingContent = { Text(source.caldavData?.serverUrl ?: "") },
|
||||||
|
trailingContent = {
|
||||||
|
IconButton(onClick = onDelete) {
|
||||||
|
Icon(Icons.Outlined.Delete, contentDescription = "Supprimer", tint = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AddCalDavAccountDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (url: String, username: String, password: String) -> Unit,
|
||||||
|
) {
|
||||||
|
var url by remember { mutableStateOf("") }
|
||||||
|
var username by remember { mutableStateOf("") }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text("Ajouter un compte CalDAV") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = url,
|
||||||
|
onValueChange = { url = it },
|
||||||
|
label = { Text("URL du serveur") },
|
||||||
|
placeholder = { Text("https://example.com/caldav") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = username,
|
||||||
|
onValueChange = { username = it },
|
||||||
|
label = { Text("Nom d'utilisateur") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it },
|
||||||
|
label = { Text("Mot de passe") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = { onConfirm(url.trim(), username.trim(), password) },
|
||||||
|
enabled = url.isNotBlank() && username.isNotBlank() && password.isNotBlank(),
|
||||||
|
) { Text("Connecter") }
|
||||||
|
},
|
||||||
|
dismissButton = { TextButton(onClick = onDismiss) { Text("Annuler") } },
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.planify.mobile.ui.settings
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.planify.mobile.data.caldav.CalDavCredentialStore
|
||||||
|
import com.planify.mobile.data.caldav.CalDavDiscovery
|
||||||
|
import com.planify.mobile.data.caldav.DiscoveryResult
|
||||||
|
import com.planify.mobile.data.preferences.AppPreferences
|
||||||
|
import com.planify.mobile.data.preferences.ThemeMode
|
||||||
|
import com.planify.mobile.data.sync.SyncScheduler
|
||||||
|
import com.planify.mobile.domain.model.Source
|
||||||
|
import com.planify.mobile.domain.repository.SourceRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class SettingsUiState(
|
||||||
|
val themeMode: ThemeMode = ThemeMode.SYSTEM,
|
||||||
|
val syncEnabled: Boolean = true,
|
||||||
|
val syncIntervalMinutes: Int = 30,
|
||||||
|
val notificationsEnabled: Boolean = true,
|
||||||
|
val caldavSources: List<Source> = emptyList(),
|
||||||
|
val discoveryInProgress: Boolean = false,
|
||||||
|
val discoveryError: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SettingsViewModel @Inject constructor(
|
||||||
|
private val prefs: AppPreferences,
|
||||||
|
private val sourceRepository: SourceRepository,
|
||||||
|
private val syncScheduler: SyncScheduler,
|
||||||
|
private val discovery: CalDavDiscovery,
|
||||||
|
private val credentialStore: CalDavCredentialStore,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val uiState = combine(
|
||||||
|
prefs.themeMode,
|
||||||
|
prefs.syncEnabled,
|
||||||
|
prefs.syncIntervalMinutes,
|
||||||
|
prefs.notificationsEnabled,
|
||||||
|
sourceRepository.getAllSources(),
|
||||||
|
) { theme, sync, interval, notifs, sources ->
|
||||||
|
SettingsUiState(
|
||||||
|
themeMode = theme,
|
||||||
|
syncEnabled = sync,
|
||||||
|
syncIntervalMinutes = interval,
|
||||||
|
notificationsEnabled = notifs,
|
||||||
|
caldavSources = sources.filter { it.type == com.planify.mobile.domain.model.SourceType.CALDAV },
|
||||||
|
)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), SettingsUiState())
|
||||||
|
|
||||||
|
private val _discoveryState = MutableStateFlow<Pair<Boolean, String?>>(false to null)
|
||||||
|
val discoveryInProgress = _discoveryState.asStateFlow()
|
||||||
|
|
||||||
|
fun setTheme(mode: ThemeMode) = viewModelScope.launch { prefs.setThemeMode(mode) }
|
||||||
|
|
||||||
|
fun setSyncEnabled(enabled: Boolean) = viewModelScope.launch {
|
||||||
|
prefs.setSyncEnabled(enabled)
|
||||||
|
if (enabled) syncScheduler.schedule(uiState.value.syncIntervalMinutes.toLong())
|
||||||
|
else syncScheduler.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSyncInterval(minutes: Int) = viewModelScope.launch {
|
||||||
|
prefs.setSyncInterval(minutes)
|
||||||
|
if (uiState.value.syncEnabled) syncScheduler.schedule(minutes.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNotificationsEnabled(enabled: Boolean) = viewModelScope.launch {
|
||||||
|
prefs.setNotificationsEnabled(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncNow() { syncScheduler.syncNow() }
|
||||||
|
|
||||||
|
fun addCalDavAccount(baseUrl: String, username: String, password: String) {
|
||||||
|
_discoveryState.update { true to null }
|
||||||
|
viewModelScope.launch {
|
||||||
|
when (val result = discovery.discover(baseUrl, username, password)) {
|
||||||
|
is DiscoveryResult.Success -> {
|
||||||
|
result.sources.forEach { source ->
|
||||||
|
credentialStore.savePassword(source.id, password)
|
||||||
|
sourceRepository.insertSource(source)
|
||||||
|
}
|
||||||
|
_discoveryState.update { false to null }
|
||||||
|
if (uiState.value.syncEnabled) syncScheduler.schedule()
|
||||||
|
}
|
||||||
|
is DiscoveryResult.Failure -> {
|
||||||
|
_discoveryState.update { false to result.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCalDavAccount(source: Source) = viewModelScope.launch {
|
||||||
|
credentialStore.deletePassword(source.id)
|
||||||
|
sourceRepository.deleteSource(source.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,29 +8,40 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
|||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.planify.mobile.data.preferences.ThemeMode
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme()
|
private val LightColorScheme = lightColorScheme()
|
||||||
private val DarkColorScheme = darkColorScheme()
|
private val DarkColorScheme = darkColorScheme()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlanifyTheme(
|
fun PlanifyTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
viewModel: ThemeViewModel = hiltViewModel(),
|
||||||
dynamicColor: Boolean = true,
|
content: @Composable () -> Unit,
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
) {
|
||||||
|
val themeMode by viewModel.themeMode.collectAsState()
|
||||||
|
val systemDark = isSystemInDarkTheme()
|
||||||
|
val isDark = when (themeMode) {
|
||||||
|
ThemeMode.DARK -> true
|
||||||
|
ThemeMode.LIGHT -> false
|
||||||
|
ThemeMode.SYSTEM -> systemDark
|
||||||
|
}
|
||||||
|
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
}
|
}
|
||||||
darkTheme -> DarkColorScheme
|
isDark -> DarkColorScheme
|
||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography,
|
typography = Typography,
|
||||||
content = content
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.planify.mobile.ui.theme
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.planify.mobile.data.preferences.AppPreferences
|
||||||
|
import com.planify.mobile.data.preferences.ThemeMode
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ThemeViewModel @Inject constructor(prefs: AppPreferences) : ViewModel() {
|
||||||
|
val themeMode = prefs.themeMode.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.Eagerly,
|
||||||
|
ThemeMode.SYSTEM,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user