feat!: Added automatic BackupManger.kt

This commit is contained in:
chindaronit
2026-02-18 22:48:04 +05:30
parent 011dfbaced
commit 56d8f37ab7
18 changed files with 495 additions and 197 deletions
+1
View File
@@ -92,6 +92,7 @@ dependencies {
implementation(libs.androidx.biometric)
implementation(libs.androidx.browser)
implementation(libs.androidx.documentfile)
implementation(libs.androidx.work.runtime.ktx)
// Hilt
ksp(libs.hilt.android.compiler)
+37 -12
View File
@@ -10,7 +10,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -22,6 +24,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flux.navigation.AppNavHost
import com.flux.navigation.Loader
import com.flux.other.Constants.Other
import com.flux.other.createNotificationChannel
import com.flux.ui.effects.ScreenEffect
import com.flux.ui.theme.FluxTheme
@@ -34,6 +37,9 @@ import com.flux.ui.viewModel.SettingsViewModel
import com.flux.ui.viewModel.TodoViewModel
import com.flux.ui.viewModel.WorkspaceViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -73,18 +79,11 @@ class MainActivity : AppCompatActivity() {
// Stop splash screen when settings are loaded
LaunchedEffect(settings.isLoading) { keepSplashScreen.value = settings.isLoading }
// Snackbar effect
LaunchedEffect(Unit) {
workspaceViewModel.effect.collect { effect ->
if (effect is ScreenEffect.ShowSnackBarMessage) {
snackBarHostState.currentSnackbarData?.dismiss()
snackBarHostState.showSnackbar(
message = effect.message,
duration = SnackbarDuration.Short
)
}
}
}
HandleSideEffects(
snackBarHostState,
workspaceViewModel.effect,
settingsViewModel.effect
)
if (!settings.isLoading) {
FluxTheme(settings) {
@@ -117,4 +116,30 @@ class MainActivity : AppCompatActivity() {
}
}
}
}
@Composable
fun HandleSideEffects(
snackbarHostState: SnackbarHostState,
vararg effectFlows: Flow<ScreenEffect>
) {
LaunchedEffect(Other.SIDE_EFFECT_KEY) {
effectFlows.forEach { effectFlow ->
effectFlow
.onEach { effect ->
if (effect is ScreenEffect.ShowSnackBarMessage) {
val result = snackbarHostState.showSnackbar(
message = effect.message,
actionLabel = "Dismiss",
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.ActionPerformed) {
snackbarHostState.currentSnackbarData?.dismiss()
}
}
}
.launchIn(this)
}
}
}
@@ -322,3 +322,9 @@ val MIGRATION_3_4 = object : Migration(3, 4) {
}
}
}
val Migration_4_5 = object : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE SettingsModel ADD COLUMN backupFrequency INTEGER NOT NULL DEFAULT 0")
}
}
@@ -26,5 +26,6 @@ data class SettingsModel(
val isLintValid: Boolean = false,
val isLineNumbersVisible: Boolean = false,
val startWithReadView: Boolean = false,
val storageRootUri: String? = null
val storageRootUri: String? = null,
val backupFrequency: Int = 0
)
+2 -1
View File
@@ -16,6 +16,7 @@ import com.flux.data.database.FluxDatabase
import com.flux.data.database.MIGRATION_1_2
import com.flux.data.database.MIGRATION_2_3
import com.flux.data.database.MIGRATION_3_4
import com.flux.data.database.Migration_4_5
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -35,7 +36,7 @@ object DataModule {
FluxDatabase::class.java,
"FluxDatabase"
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, Migration_4_5)
.build()
@Singleton
@@ -19,9 +19,9 @@ import com.flux.ui.screens.journal.EditJournal
import com.flux.ui.screens.notes.EditLabels
import com.flux.ui.screens.notes.NoteDetails
import com.flux.ui.screens.settings.About
import com.flux.ui.screens.settings.Backup
import com.flux.ui.screens.settings.Contact
import com.flux.ui.screens.settings.Customize
import com.flux.ui.screens.settings.Data
import com.flux.ui.screens.settings.Editor
import com.flux.ui.screens.settings.Languages
import com.flux.ui.screens.settings.Privacy
@@ -171,8 +171,8 @@ val SettingsScreens =
NavRoutes.Contact.route to { navController, _, states, _ ->
Contact(navController, states.settings.data.cornerRadius)
},
NavRoutes.Backup.route to { navController, _, states, viewModels ->
Backup(navController, states.settings.data.cornerRadius, viewModels.backupViewModel)
NavRoutes.Backup.route to { navController, snackbarHostState, states, viewModels ->
Data(navController, states.settings.data.cornerRadius, states.settings, snackbarHostState, viewModels.backupViewModel, viewModels.settingsViewModel::onEvent)
},
NavRoutes.Editor.route to { navController, _, states, viewModels ->
Editor(navController, states.settings, viewModels.settingsViewModel::onEvent)
@@ -0,0 +1,40 @@
package com.flux.other
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit
class BackupManager(
private val workManager: WorkManager
) {
fun scheduleBackup(frequency: BackupFrequency) {
if (frequency == BackupFrequency.NEVER) {
workManager.cancelUniqueWork(BACKUP_WORK_NAME)
return
}
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresStorageNotLow(true)
.build()
val backupRequest = PeriodicWorkRequestBuilder<BackupWorker>(
frequency.days.toLong(), TimeUnit.DAYS
)
.setConstraints(constraints)
.build()
workManager.enqueueUniquePeriodicWork(
BACKUP_WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
backupRequest
)
}
companion object {
const val BACKUP_WORK_NAME = "database_backup_work"
const val BACKUP_FREQUENCY_KEY = "backup_frequency"
}
}
@@ -0,0 +1,82 @@
package com.flux.other
import android.content.Context
import androidx.core.net.toUri
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.flux.R
import com.flux.data.database.FluxBackup
import com.flux.data.model.SettingsModel
import com.flux.di.DataModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.OutputStreamWriter
enum class BackupFrequency(val days: Int, val textRes: Int) {
NEVER(0, R.string.never),
DAILY(1, R.string.daily),
WEEKLY(7, R.string.weekly),
MONTHLY(30, R.string.monthly);
}
class BackupWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
val context = applicationContext
val fluxDatabase = DataModule.provideFluxDatabase(context)
val settingsDao = DataModule.provideSettingsDao(fluxDatabase)
val notesDao = DataModule.provideNotesDao(fluxDatabase)
val workspaceDao = DataModule.provideWorkspaceDao(fluxDatabase)
val todoDao = DataModule.provideTodoDao(fluxDatabase)
val habitDao = DataModule.provideHabitDao(fluxDatabase)
val habitInstanceDao = DataModule.provideHabitInstanceDao(fluxDatabase)
val journalDao = DataModule.provideJournalDao(fluxDatabase)
val labelDao = DataModule.provideLabelDao(fluxDatabase)
val eventDao = DataModule.provideEventDao(fluxDatabase)
val eventInstanceDao = DataModule.provideEventInstanceDao(fluxDatabase)
val settings = settingsDao.loadSetting()
val rootUri = settings?.storageRootUri?.toUri()?: "".toUri()
val openNoteDir = getOrCreateDirectory(context, rootUri, Constants.File.FLUX)
val backupDir = openNoteDir?.let { dir ->
getOrCreateDirectory(context, dir.uri, Constants.File.FLUX_BACKUP)
}
backupDir?.let { dir ->
val backup = FluxBackup(
workspaces = workspaceDao.getAll(),
notes = notesDao.loadAllNotes(),
todos = todoDao.loadAllLists(),
habits = habitDao.loadAllHabits(),
habitInstances = habitInstanceDao.loadAllInstances(),
journals = journalDao.loadAllEntries(),
labels = labelDao.getAll(),
events = eventDao.loadAllEvents(),
eventInstances = eventInstanceDao.getAll(),
settings = settingsDao.loadSetting()?: SettingsModel()
)
val json = Json.encodeToString(FluxBackup.serializer(), backup)
val fileName = "${System.currentTimeMillis()}.json"
val file = dir.createFile("application/json", fileName)
file?.let { docFile ->
context.contentResolver.openOutputStream(docFile.uri)
?.use { outputStream ->
OutputStreamWriter(outputStream).use { writer ->
writer.write(json)
}
}
}
}
Result.success()
} catch (_: Exception) {
Result.failure()
}
}
}
@@ -15,7 +15,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
@@ -49,7 +48,6 @@ class BootReceiver : BroadcastReceiver() {
val events = eventRepository.loadAllEvents()
return habits.filter { it.isLive() } + events
}
}
@@ -19,6 +19,11 @@ object Constants {
const val FLUX_IMAGES = "Images"
const val FLUX_AUDIO = "Audio"
const val FLUX_VIDEOS = "Videos"
const val FLUX_BACKUP = "Backup"
}
object Other {
const val SIDE_EFFECT_KEY = "side_effect"
}
object Editor {
@@ -3,5 +3,6 @@ package com.flux.ui.events
import com.flux.data.model.SettingsModel
sealed class SettingEvents {
data class UpdateSettings(val data: SettingsModel) : SettingEvents()
data class UpdateSettings(val data: SettingsModel): SettingEvents()
data object ResetDatabase: SettingEvents()
}
@@ -1,123 +0,0 @@
package com.flux.ui.screens.settings
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Backup
import androidx.compose.material.icons.rounded.Restore
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.flux.R
import com.flux.other.canScheduleReminder
import com.flux.other.isNotificationPermissionGranted
import com.flux.other.openAppNotificationSettings
import com.flux.other.requestExactAlarmPermission
import com.flux.ui.components.ActionType
import com.flux.ui.components.BasicScaffold
import com.flux.ui.components.SettingOption
import com.flux.ui.components.shapeManager
import com.flux.ui.viewModel.BackupViewModel
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Backup(
navController: NavController,
radius: Int,
backupViewModel: BackupViewModel
) {
val context = LocalContext.current
// EXPORT launcher
val exportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let { backupViewModel.exportBackup(context, it) }
}
// IMPORT launcher
val importLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let { backupViewModel.importBackup(context, it) }
}
// Observe result - Collect SharedFlow properly
LaunchedEffect(Unit) {
backupViewModel.backupResult.collect { result ->
if (result.isSuccess) {
Toast.makeText(context, "Operation successful!", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(context, "Operation failed.", Toast.LENGTH_LONG).show()
}
}
}
// ... rest of your UI
BasicScaffold(
title = stringResource(R.string.Backup),
onBackClicked = {
navController.navigateUp()
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
item {
SettingOption(
title = stringResource(R.string.Backup),
description = stringResource(R.string.Backup_Description),
icon = Icons.Rounded.Backup,
radius = shapeManager(radius = radius, isFirst = true),
actionType = ActionType.CUSTOM,
onCustomClick = { exportLauncher.launch("flux-backup.json") }
)
}
item {
SettingOption(
title = stringResource(R.string.Restore),
description = stringResource(R.string.Restore_Description),
icon = Icons.Rounded.Restore,
radius = shapeManager(radius = radius, isLast = true),
actionType = ActionType.CUSTOM,
onCustomClick = {
if (!canScheduleReminder(context)) {
Toast.makeText(
context,
context.getText(R.string.Reminder_Permission),
Toast.LENGTH_SHORT
).show()
requestExactAlarmPermission(context)
}
if (!isNotificationPermissionGranted(context)) {
Toast.makeText(
context,
context.getText(R.string.Notification_Permission),
Toast.LENGTH_SHORT
).show()
openAppNotificationSettings(context)
}
if (canScheduleReminder(context) && isNotificationPermissionGranted(context)) {
importLauncher.launch(arrayOf("application/json"))
}
}
)
}
}
}
}
@@ -19,10 +19,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.Colorize
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.FormatListNumbered
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Spellcheck
import androidx.compose.material.icons.rounded.FontDownload
import androidx.compose.material.icons.rounded.RoundedCorner
import androidx.compose.material.icons.rounded.ViewCompact
@@ -316,46 +314,6 @@ fun Customize(
)
}
item {
SettingOption(
title = stringResource(R.string.show_line_numbers),
description = stringResource(R.string.show_line_numbers_desc),
icon = Icons.Filled.FormatListNumbered,
radius = shapeManager(radius = settings.data.cornerRadius),
actionType = ActionType.SWITCH,
variable = settings.data.isLineNumbersVisible,
switchEnabled = {
onSettingsEvents(
SettingEvents.UpdateSettings(
settings.data.copy(
isLineNumbersVisible = it
)
)
)
}
)
}
item {
SettingOption(
title = stringResource(R.string.markdown_lint),
description = stringResource(R.string.markdown_lint_desc),
icon = Icons.Filled.Spellcheck,
radius = shapeManager(radius = settings.data.cornerRadius),
actionType = ActionType.SWITCH,
variable = settings.data.isLintValid,
switchEnabled = {
onSettingsEvents(
SettingEvents.UpdateSettings(
settings.data.copy(
isLintValid = it
)
)
)
}
)
}
item {
SettingOption(
title = stringResource(R.string.Hour_Format_24),
@@ -0,0 +1,270 @@
package com.flux.ui.screens.settings
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.CleaningServices
import androidx.compose.material.icons.outlined.EditCalendar
import androidx.compose.material.icons.rounded.Backup
import androidx.compose.material.icons.rounded.Restore
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.work.WorkManager
import com.flux.R
import com.flux.other.BackupFrequency
import com.flux.other.BackupManager
import com.flux.other.canScheduleReminder
import com.flux.other.isNotificationPermissionGranted
import com.flux.other.openAppNotificationSettings
import com.flux.other.requestExactAlarmPermission
import com.flux.ui.components.ActionType
import com.flux.ui.components.DeleteAlert
import com.flux.ui.components.SettingOption
import com.flux.ui.components.shapeManager
import com.flux.ui.events.SettingEvents
import com.flux.ui.state.Settings
import com.flux.ui.viewModel.BackupViewModel
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Data(
navController: NavController,
radius: Int,
settings: Settings,
snackbarHostState: SnackbarHostState,
backupViewModel: BackupViewModel,
onSettingsEvents: (SettingEvents) -> Unit
) {
val context = LocalContext.current
val hapticFeedback = LocalHapticFeedback.current
var showWarningDialog by remember { mutableStateOf(false) }
// EXPORT launcher
val exportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let { backupViewModel.exportBackup(context, it) }
}
// IMPORT launcher
val importLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let { backupViewModel.importBackup(context, it) }
}
// Observe result - Collect SharedFlow properly
LaunchedEffect(Unit) {
backupViewModel.backupResult.collect { result ->
if (result.isSuccess) {
Toast.makeText(context, "Operation successful!", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(context, "Operation failed.", Toast.LENGTH_LONG).show()
}
}
}
// ... rest of your UI
Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
title = { Text("Data") },
navigationIcon = {
IconButton({navController.navigateUp()}) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
)
},
snackbarHost = { SnackbarHost(snackbarHostState) }
){ innerPadding ->
LazyColumn(
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
item {
SettingOption(
title = stringResource(R.string.Backup),
description = stringResource(R.string.Backup_Description),
icon = Icons.Rounded.Backup,
radius = shapeManager(radius = radius, isFirst = true),
actionType = ActionType.CUSTOM,
onCustomClick = { exportLauncher.launch("flux-backup.json") }
)
}
item {
SettingOption(
title = stringResource(R.string.Restore),
description = stringResource(R.string.Restore_Description),
icon = Icons.Rounded.Restore,
radius = shapeManager(radius = radius, isLast = true),
actionType = ActionType.CUSTOM,
onCustomClick = {
if (!canScheduleReminder(context)) {
Toast.makeText(
context,
context.getText(R.string.Reminder_Permission),
Toast.LENGTH_SHORT
).show()
requestExactAlarmPermission(context)
}
if (!isNotificationPermissionGranted(context)) {
Toast.makeText(
context,
context.getText(R.string.Notification_Permission),
Toast.LENGTH_SHORT
).show()
openAppNotificationSettings(context)
}
if (canScheduleReminder(context) && isNotificationPermissionGranted(context)) {
importLauncher.launch(arrayOf("application/json"))
}
}
)
}
item {
val workManager = remember { WorkManager.getInstance(context.applicationContext) }
val backupManager = remember(workManager) { BackupManager(workManager) }
fun mapDaysToSliderPosition(days: Int): Float = when (days) {
0 -> 0f
1 -> 1f
7 -> 2f
30 -> 3f
else -> 0f
}
fun mapSliderPositionToFrequency(position: Float): BackupFrequency = when (position) {
0f -> BackupFrequency.NEVER
1f -> BackupFrequency.DAILY
2f -> BackupFrequency.WEEKLY
3f -> BackupFrequency.MONTHLY
else -> BackupFrequency.NEVER
}
val sliderPosition = remember(settings.data.backupFrequency) {
mapDaysToSliderPosition(settings.data.backupFrequency)
}
val selectedFrequency = remember(sliderPosition) {
mapSliderPositionToFrequency(sliderPosition)
}
ListItem(
modifier = Modifier.padding(top = 8.dp),
leadingContent = {
Icon(
imageVector = Icons.Outlined.EditCalendar,
contentDescription = "Auto backup"
)
},
colors = ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
),
headlineContent = { Text(text = "Frequency of automatic backup") },
supportingContent = { Text(text = stringResource(selectedFrequency.textRes)) }
)
Slider(
modifier = Modifier.padding(horizontal = 16.dp),
value = sliderPosition,
onValueChange = { newPosition ->
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
val newFrequency = mapSliderPositionToFrequency(newPosition)
onSettingsEvents(SettingEvents.UpdateSettings(settings.data.copy(backupFrequency = newFrequency.days)))
},
onValueChangeFinished = { backupManager.scheduleBackup(selectedFrequency) },
valueRange = 0f..3f,
steps = 2
)
}
item {
ListItem(
colors = ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
),
leadingContent = {
Icon(
imageVector = Icons.Outlined.CleaningServices,
contentDescription = "Reset"
)
},
headlineContent = { Text(text = "Reset Database") },
trailingContent = {
TextButton(
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick)
showWarningDialog = true
},
colors = ButtonDefaults.textButtonColors().copy(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer
)
) {
Text(text = "Reset")
}
},
supportingContent = {
Text(
text = "Clear all app data"
)
}
)
}
}
if(showWarningDialog){
DeleteAlert(
onConfirmation = {
showWarningDialog=false
onSettingsEvents(SettingEvents.ResetDatabase) },
onDismissRequest = { showWarningDialog = false },
dialogTitle = stringResource(R.string.deleteDialogTitle),
dialogText = stringResource(R.string.deleteDialogText),
icon = Icons.Default.Delete
)
}
}
}
@@ -119,8 +119,8 @@ fun Settings(
item {
SettingCategory(
title = stringResource(R.string.Backup),
subTitle = stringResource(R.string.Backup_Description),
title = "Data",
subTitle = "Backup * Recovery",
icon = Icons.Rounded.Backup,
shape = shapeManager(radius = settings.data.cornerRadius),
action = {
@@ -3,28 +3,39 @@ package com.flux.ui.viewModel
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.flux.data.database.FluxDatabase
import com.flux.data.model.SettingsModel
import com.flux.data.repository.SettingsRepository
import com.flux.ui.effects.ScreenEffect
import com.flux.ui.events.SettingEvents
import com.flux.ui.state.Settings
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
val repository: SettingsRepository
val repository: SettingsRepository,
val database: FluxDatabase
) : ViewModel() {
private val _state: MutableStateFlow<Settings> = MutableStateFlow(Settings())
val state: StateFlow<Settings> = _state.asStateFlow()
private val _effect: Channel<ScreenEffect> = Channel()
val effect = _effect.receiveAsFlow()
init {
loadSettings()
init { loadSettings() }
private fun setEffect(builder: () -> ScreenEffect) {
val effectValue = builder()
viewModelScope.launch { _effect.send(effectValue) }
}
fun onEvent(event: SettingEvents) {
@@ -37,9 +48,8 @@ class SettingsViewModel @Inject constructor(
private fun reduce(event: SettingEvents) {
when (event) {
is SettingEvents.UpdateSettings -> {
updateSettings(event.data)
}
is SettingEvents.UpdateSettings -> { updateSettings(event.data) }
is SettingEvents.ResetDatabase -> { resetDatabase() }
}
}
@@ -47,10 +57,7 @@ class SettingsViewModel @Inject constructor(
updateState { it.copy(isLoading = true) }
viewModelScope.launch {
repository.loadSettings().collect { data ->
if (data != null){
println(data)
updateState { it.copy(isLoading = false, data = data) }
}
if (data != null){ updateState { it.copy(isLoading = false, data = data) } }
else updateState { it.copy(isLoading = false) }
}
}
@@ -69,4 +76,24 @@ class SettingsViewModel @Inject constructor(
repository.saveStorageRoot(uri)
}
}
fun resetDatabase() {
viewModelScope.launch {
runCatching {
withContext(Dispatchers.IO) {
database.clearAllTables()
}
}.onSuccess {
setEffect {
ScreenEffect.ShowSnackBarMessage("Reset completed")
}
}.onFailure { throwable ->
setEffect {
ScreenEffect.ShowSnackBarMessage(
"Failed to reset database: ${throwable.localizedMessage ?: "error"}"
)
}
}
}
}
}
+4
View File
@@ -328,5 +328,9 @@
<string name="reading_view">Reading View</string>
<string name="editing_view">Editing View</string>
<string name="never">Never</string>
<string name="daily">Daily</string>
<string name="weekly">Weekly</string>
<string name="monthly">Monthly</string>
</resources>
+2
View File
@@ -24,6 +24,7 @@ hilt = "2.57.2"
hiltNavigationCompose = "1.3.0"
browser = "1.9.0"
documentfile = "1.1.0"
workRuntimeKtx = "2.11.1"
[libraries]
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
@@ -66,6 +67,7 @@ commonmark-ext-heading-anchor = { group = "org.commonmark", name = "commonmark-e
commonmark-ext-yaml-front-matter = { group = "org.commonmark", name = "commonmark-ext-yaml-front-matter", version.ref = "commonmark" }
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }