feat!: Added automatic BackupManger.kt
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user