@@ -30,7 +30,9 @@ import android.net.Uri
3030import android.os.Bundle
3131import android.widget.Toast
3232import androidx.activity.ComponentActivity
33+ import androidx.activity.compose.rememberLauncherForActivityResult
3334import androidx.activity.compose.setContent
35+ import androidx.activity.result.contract.ActivityResultContracts
3436import androidx.compose.foundation.clickable
3537import androidx.compose.foundation.layout.PaddingValues
3638import androidx.compose.foundation.layout.padding
@@ -45,6 +47,9 @@ import androidx.compose.material3.MaterialTheme
4547import androidx.compose.material3.Scaffold
4648import androidx.compose.material3.Text
4749import androidx.compose.runtime.Composable
50+ import androidx.compose.runtime.mutableStateOf
51+ import androidx.compose.runtime.remember
52+ import androidx.compose.runtime.rememberCoroutineScope
4853import androidx.compose.ui.Modifier
4954import androidx.compose.ui.platform.LocalClipboardManager
5055import androidx.compose.ui.platform.LocalContext
@@ -54,6 +59,10 @@ import androidx.compose.ui.text.SpanStyle
5459import androidx.compose.ui.text.buildAnnotatedString
5560import androidx.compose.ui.text.withStyle
5661import androidx.compose.ui.unit.dp
62+ import kotlinx.coroutines.CoroutineScope
63+ import kotlinx.coroutines.Dispatchers
64+ import kotlinx.coroutines.launch
65+ import kotlinx.coroutines.withContext
5766import me.zhanghai.compose.preference.ProvidePreferenceLocals
5867import me.zhanghai.compose.preference.footerPreference
5968import me.zhanghai.compose.preference.listPreference
@@ -72,6 +81,13 @@ import xyz.jekyllex.utils.Setting.*
7281import xyz.jekyllex.utils.trimQuotes
7382import xyz.jekyllex.BuildConfig
7483import xyz.jekyllex.ui.activities.viewer.WebPageViewer
84+ import xyz.jekyllex.ui.components.GenericDialog
85+ import xyz.jekyllex.ui.components.ProgressDialog
86+ import xyz.jekyllex.utils.Commands.rm
87+ import xyz.jekyllex.utils.Commands.rmDir
88+ import xyz.jekyllex.utils.Commands.shell
89+ import xyz.jekyllex.utils.Commands.unzip
90+ import xyz.jekyllex.utils.Commands.zip
7591import xyz.jekyllex.utils.Constants.DOCS
7692import xyz.jekyllex.utils.Constants.TERMS
7793import xyz.jekyllex.utils.Constants.PRIVACY
@@ -80,8 +96,15 @@ import xyz.jekyllex.utils.Constants.themeMap
8096import xyz.jekyllex.utils.Constants.ISSUES_URL
8197import xyz.jekyllex.utils.Constants.PAT_SETTINGS_URL
8298import xyz.jekyllex.utils.Constants.EDITOR_PREVIEWS_URL
99+ import xyz.jekyllex.utils.Constants.HOME_DIR
100+ import xyz.jekyllex.utils.Constants.TMP_DIR
101+ import xyz.jekyllex.utils.mergeCommands
102+ import java.io.File
103+ import java.io.FileInputStream
104+ import java.io.FileOutputStream
83105
84106class SettingsActivity : ComponentActivity () {
107+
85108 override fun onCreate (savedInstanceState : Bundle ? ) {
86109 super .onCreate(savedInstanceState)
87110
@@ -95,8 +118,107 @@ class SettingsActivity : ComponentActivity() {
95118
96119@Composable
97120fun SettingsView () {
121+ val coroutineScope = rememberCoroutineScope()
98122 val context = LocalContext .current as Activity
99123 val clipboardManager = LocalClipboardManager .current
124+ val processing = remember { mutableStateOf(false ) }
125+ val restoreConfirmation = remember { mutableStateOf<Uri ?>(null ) }
126+
127+ val picker = rememberLauncherForActivityResult(ActivityResultContracts .GetContent ()) { uri ->
128+ uri?.let {
129+ restoreConfirmation.value = uri
130+ } ? : run {
131+ Toast .makeText(context, " No file selected!" , Toast .LENGTH_SHORT )
132+ .show()
133+ }
134+ }
135+
136+ val saver = rememberLauncherForActivityResult(
137+ contract = ActivityResultContracts .StartActivityForResult ()
138+ ) { result ->
139+ val uri = result.data?.data
140+ val file = File (HOME_DIR , " backup.zip" )
141+ uri?.let {
142+ coroutineScope.launch(Dispatchers .IO ) {
143+ try {
144+ context.contentResolver.openOutputStream(uri)?.use { outputStream ->
145+ FileInputStream (file).use { inputStream ->
146+ inputStream.copyTo(outputStream)
147+ }
148+ }
149+ withContext(Dispatchers .Main ) {
150+ Toast .makeText(context, " Backup saved!" , Toast .LENGTH_SHORT ).show()
151+ }
152+ } catch (e: Exception ) {
153+ e.printStackTrace()
154+ withContext(Dispatchers .Main ) {
155+ Toast .makeText(context, " Failed to save backup file!" , Toast .LENGTH_SHORT )
156+ .show()
157+ }
158+ }
159+ file.delete()
160+ }
161+ } ? : run {
162+ file.delete()
163+ Toast .makeText(context, " No location selected!" , Toast .LENGTH_SHORT )
164+ .show()
165+ }
166+ }
167+
168+ if (processing.value) {
169+ ProgressDialog (dialogTitle = " Processing..." )
170+ }
171+
172+ if (restoreConfirmation.value != null ) {
173+ GenericDialog (
174+ isCancellable = false ,
175+ dialogTitle = " Restore data" ,
176+ dialogText = " Are you sure you want to restore data from the selected file?\n\n " +
177+ " This will overwrite all existing data." ,
178+ onDismissRequest = {
179+ restoreConfirmation.value = null
180+ },
181+ onConfirmation = confirmation@{
182+ val uri = restoreConfirmation.value ? : return @confirmation
183+ val file = File (TMP_DIR , " backup-${System .currentTimeMillis()} .zip" )
184+ restoreConfirmation.value = null
185+ coroutineScope.launch(Dispatchers .IO ) {
186+ processing.value = true
187+ try {
188+ context.contentResolver.openInputStream(uri)?.use { inputStream ->
189+ FileOutputStream (file).use { outputStream ->
190+ inputStream.copyTo(outputStream)
191+ }
192+ }
193+ NativeUtils .exec(
194+ shell(
195+ mergeCommands(
196+ rmDir(" *" , " .*" ),
197+ unzip(file.absolutePath, " -d" , " $HOME_DIR /" )
198+ )
199+ )
200+ )
201+ processing.value = false
202+ withContext(Dispatchers .Main ) {
203+ Toast .makeText(
204+ context,
205+ " Backup restored successfully!" ,
206+ Toast .LENGTH_SHORT
207+ ).show()
208+ }
209+ } catch (e: Exception ) {
210+ e.printStackTrace()
211+ withContext(Dispatchers .Main ) {
212+ processing.value = false
213+ Toast .makeText(context, " Failed to fetch backup file!" , Toast .LENGTH_SHORT )
214+ .show()
215+ }
216+ }
217+ file.delete()
218+ }
219+ }
220+ )
221+ }
100222
101223 Scaffold (
102224 topBar = {
@@ -384,6 +506,47 @@ fun SettingsView() {
384506 },
385507 )
386508
509+ preferenceCategory(
510+ key = " advanced" ,
511+ title = { Text (" Advanced" ) },
512+ )
513+
514+ preference(
515+ key = " backup" ,
516+ onClick = {
517+ processing.value = true
518+ NativeUtils .exec(
519+ shell(
520+ mergeCommands(
521+ rm(" backup.zip" ),
522+ zip(" -r" , " backup.zip" , " ." , " -x" , " '.bundle/*'" )
523+ ),
524+ ),
525+ CoroutineScope (Dispatchers .IO )
526+ ) {
527+ processing.value = false
528+ withContext(Dispatchers .Main ) {
529+ saver.launch(
530+ Intent (Intent .ACTION_CREATE_DOCUMENT ).apply {
531+ type = " application/zip"
532+ addCategory(Intent .CATEGORY_OPENABLE )
533+ putExtra(Intent .EXTRA_TITLE , " backup.zip" )
534+ }
535+ )
536+ }
537+ }
538+ },
539+ title = { Text (context.getString(R .string.backup_setting_title)) },
540+ summary = { Text (context.getString(R .string.backup_setting_summary)) },
541+ )
542+
543+ preference(
544+ key = " restore" ,
545+ onClick = { picker.launch(" application/zip" ) },
546+ title = { Text (context.getString(R .string.restore_setting_title)) },
547+ summary = { Text (context.getString(R .string.restore_setting_summary)) },
548+ )
549+
387550 preferenceCategory(
388551 key = " other" ,
389552 title = { Text (" Other" ) }
0 commit comments