Skip to content

Commit

Permalink
📁 Implement Backup and Restore (#31)
Browse files Browse the repository at this point in the history
* Added backup description to strings.xml.
* Updated export functionality to create the "Grit" directory if it doesn't exist.
* Added a new backup entry in the settings page.
* Updated settings to include backup page.
* Added new icon.
* Updated navigation.

Signed-off-by: shub39 <cptnshubham39@gmail.com>
  • Loading branch information
shub39 committed Feb 10, 2025
1 parent a95a759 commit 26d0cf5
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 7 deletions.
10 changes: 9 additions & 1 deletion app/src/main/java/com/shub39/grit/app/Grit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import com.shub39.grit.R
import com.shub39.grit.core.presentation.settings.AboutPage
import com.shub39.grit.core.presentation.settings.Backup
import com.shub39.grit.core.presentation.settings.LookAndFeelPage
import com.shub39.grit.core.presentation.settings.SettingsPage
import com.shub39.grit.core.presentation.theme.GritTheme
Expand Down Expand Up @@ -129,7 +130,8 @@ fun Grit(
SettingsPage(
onCategoryClick = { navController.navigate(Routes.Categories) },
onAboutClick = { navController.navigate(Routes.About) },
onLookAndFeelClick = { navController.navigate(Routes.LookAndFeel) }
onLookAndFeelClick = { navController.navigate(Routes.LookAndFeel) },
onBackupClick = { navController.navigate(Routes.Backup) }
)
}

Expand All @@ -153,6 +155,12 @@ fun Grit(

LookAndFeelPage()
}

composable<Routes.Backup> {
currentRoute = Routes.SettingsGraph

Backup()
}
}

composable<Routes.HabitsPage> {
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/shub39/grit/app/Routes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ sealed interface Routes {
data object Categories: Routes
@Serializable
data object LookAndFeel: Routes
@Serializable
data object Backup: Routes
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class ExportImpl(
"Grit"
)

if (!exportFolder.exists() || !exportFolder.isDirectory) exportFolder.mkdirs()

val time = LocalDateTime.now().toString().replace(":", "").replace(" ", "")
val file = File(exportFolder, "Grit-Export-$time.json")

Expand Down
163 changes: 163 additions & 0 deletions app/src/main/java/com/shub39/grit/core/presentation/settings/Backup.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,178 @@
package com.shub39.grit.core.presentation.settings

import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.shub39.grit.R
import com.shub39.grit.core.domain.backup.ExportRepo
import com.shub39.grit.core.domain.backup.ExportState
import com.shub39.grit.core.domain.backup.RestoreRepo
import com.shub39.grit.core.domain.backup.RestoreResult
import com.shub39.grit.core.domain.backup.RestoreState
import com.shub39.grit.core.presentation.components.BetterIconButton
import com.shub39.grit.core.presentation.components.PageFill
import kotlinx.coroutines.launch
import org.koin.compose.koinInject

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Backup(
exportRepo: ExportRepo = koinInject(),
restoreRepo: RestoreRepo = koinInject()
) = PageFill {
val coroutineScope = rememberCoroutineScope()

var exportState by remember { mutableStateOf(ExportState.IDLE) }
var restoreState by remember { mutableStateOf(RestoreState.IDLE) }

var uri by remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri = it }

Column(
modifier = Modifier
.widthIn(max = 700.dp)
.fillMaxSize()
) {
TopAppBar(
title = {
Text(
text = stringResource(R.string.backup)
)
}
)

LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
item {
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.export)
)
},
supportingContent = {
Text(
text = stringResource(R.string.export_desc)
)
},
trailingContent = {
BetterIconButton(
onClick = {
if (exportState == ExportState.IDLE) {
coroutineScope.launch {
exportState = ExportState.EXPORTING

exportRepo.exportToJson()

exportState = ExportState.EXPORTED
}
}
}
) {
when (exportState) {
ExportState.IDLE -> Icon(
painter = painterResource(R.drawable.round_play_arrow_24),
contentDescription = "Start"
)
ExportState.EXPORTING -> CircularProgressIndicator(modifier = Modifier.size(24.dp))
ExportState.EXPORTED -> Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = "Done"
)
}
}
}
)
}

item {
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.restore)
)
},
supportingContent = {
Text(
text = stringResource(R.string.restore_desc)
)
},
trailingContent = {
if (uri == null) {
TextButton(
onClick = { launcher.launch(arrayOf("application/json")) }
) {
Text(
text = stringResource(R.string.choose_file)
)
}
} else {
BetterIconButton(
onClick = {
if (restoreState == RestoreState.IDLE) {
coroutineScope.launch {
restoreState = RestoreState.RESTORING

val result = restoreRepo.restoreSongs(uri!!)

restoreState = if (result == RestoreResult.Success) {
RestoreState.RESTORED
} else {
RestoreState.FAILURE
}
}
}
}
) {
when (restoreState) {
RestoreState.IDLE -> Icon(
painter = painterResource(R.drawable.round_play_arrow_24),
contentDescription = "Start"
)
RestoreState.RESTORING -> CircularProgressIndicator(modifier = Modifier.size(24.dp))
RestoreState.RESTORED -> Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = "Done"
)
RestoreState.FAILURE -> Icon(
imageVector = Icons.Default.Warning,
contentDescription = "Fail"
)
}
}
}
}
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import com.shub39.grit.core.presentation.components.BetterIconButton
fun SettingsPage(
onCategoryClick: () -> Unit,
onAboutClick: () -> Unit,
onLookAndFeelClick: () -> Unit
onLookAndFeelClick: () -> Unit,
onBackupClick: () -> Unit
) {
Box(
modifier = Modifier.fillMaxSize(),
Expand Down Expand Up @@ -74,12 +75,17 @@ fun SettingsPage(
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.about)
text = stringResource(R.string.look_and_feel)
)
},
supportingContent = {
Text(
text = stringResource(R.string.look_and_feel_desc)
)
},
trailingContent = {
BetterIconButton (
onClick = onAboutClick
onClick = onLookAndFeelClick
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
Expand All @@ -94,17 +100,37 @@ fun SettingsPage(
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.look_and_feel)
text = stringResource(R.string.backup)
)
},
supportingContent = {
Text(
text = stringResource(R.string.look_and_feel_desc)
text = stringResource(R.string.backup_desc)
)
},
trailingContent = {
BetterIconButton (
onClick = onLookAndFeelClick
onClick = onBackupClick
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null
)
}
}
)
}

item {
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.about)
)
},
trailingContent = {
BetterIconButton (
onClick = onAboutClick
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/round_play_arrow_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z"/>

</vector>
7 changes: 7 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,11 @@
<string name="select_seed_desc">Select color to generate palette from</string>
<string name="palette_style">Palette Style</string>
<string name="look_and_feel_desc">Customise the app in various ways</string>
<string name="backup">Backup</string>
<string name="backup_desc">Export or Restore your tasks and habits</string>
<string name="export">Export data</string>
<string name="export_desc">All data exported to Downloads/Grit</string>
<string name="restore">Restore</string>
<string name="restore_desc">Restore tasks and habits from selected file</string>
<string name="choose_file">Choose file</string>
</resources>

0 comments on commit 26d0cf5

Please sign in to comment.