Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
760813f
add: import & export buttons in SettingsScreen
mnjxclone Apr 25, 2026
c17da6d
update: add import and export functionality
mnjxclone Apr 25, 2026
daed80b
progress on a migration from raw SQL backup/restore to a JSON arch.
mnjxclone Apr 29, 2026
1d61653
update: JSON data export/import
mnjxclone May 3, 2026
e8bb260
fix: remove temporary logging
mnjxclone May 3, 2026
8e2aed6
fix: remove to-be-deprecated content
mnjxclone May 9, 2026
3bc0c36
fix: proper use of ioDispatcher
mnjxclone May 9, 2026
a6b6f97
fix: remove unused imports
mnjxclone May 9, 2026
eb46859
fix: remove redundant lib in libs.versions.toml
mnjxclone May 9, 2026
f1ee9b5
fix: repair build.gradle.kts
mnjxclone May 9, 2026
70c7dc0
fix: use standard naming conventions
mnjxclone May 9, 2026
cab0cc9
fix: use correct serializer for LocalDateTime
mnjxclone May 9, 2026
aab175e
fix: remove unused variables
mnjxclone May 9, 2026
703cfb7
fix: revert prev commit
mnjxclone May 9, 2026
3d0f7aa
fix: remove unnecessary upsert methods
mnjxclone May 9, 2026
3ecf5e0
fix: correct export/import pipeline (but allows duplicates)
mnjxclone May 9, 2026
e7f57bc
fix: convert toasts to snackbars
mnjxclone May 9, 2026
0d1f0c6
resolve merge conflicts with upstream
mnjxclone May 25, 2026
7d6d1cf
add: missing generic savePreferences function
mnjxclone May 25, 2026
3988156
add: datastore dependency
mnjxclone May 25, 2026
2240b70
fix: correct jvm version
mnjxclone May 25, 2026
67efb57
update: use dialogs instead of snackbars
mnjxclone May 25, 2026
d7000c9
fix: fix linted errors
mnjxclone May 25, 2026
29d6a42
fix: add proper migration logic
mnjxclone May 25, 2026
01a3c42
fix: fix typo and add animations to loading
mnjxclone May 25, 2026
edb8a62
fix: separated concerns regarding SettingsEvents
mnjxclone May 25, 2026
48d9855
fix: remove redundant datastore dependency
mnjxclone Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/src/main/java/org/librefit/db/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

package org.librefit.db

import android.content.Context
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
Expand Down Expand Up @@ -38,6 +40,29 @@ abstract class AppDatabase : RoomDatabase() {
companion object {
const val NAME = "librefit_database"

@Volatile
private var INSTANCE: AppDatabase? = null

fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
NAME
)
.addMigrations(MIGRATION_2_3)
.build()

INSTANCE = instance
instance
}
}

fun closeInstance() {
INSTANCE?.close()
INSTANCE = null
}

val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.librefit.db.repository

import android.content.Context
import android.content.Intent
import android.net.Uri
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.librefit.db.AppDatabase
import java.io.File
import javax.inject.Inject
import kotlin.system.exitProcess

class ImportExportRepository @Inject constructor(
private val db: AppDatabase,
@ApplicationContext private val context: Context
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@ApplicationContext private val context: Context
@param:ApplicationContext private val context: Context

) {
suspend fun exportTo(uri: Uri) = withContext(Dispatchers.IO) {
val sqliteDb = db.openHelper.writableDatabase

val tempFile = File(context.cacheDir, "backup.db")

val path = tempFile.absolutePath.replace("'", "''")
sqliteDb.execSQL("VACUUM INTO '$path'")

context.contentResolver.openOutputStream(uri)?.use { out ->
tempFile.inputStream().use { input ->
input.copyTo(out)
}
}

tempFile.delete()
}

suspend fun importFrom(uri: Uri): Nothing = withContext(Dispatchers.IO) {
val dbFile = context.getDatabasePath(AppDatabase.NAME)

AppDatabase.closeInstance()

val tempFile = File(context.cacheDir, "restore.db")

context.contentResolver.openInputStream(uri)?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}

val wal = File(dbFile.path + "-wal")
val shm = File(dbFile.path + "-shm")

wal.delete()
shm.delete()

tempFile.copyTo(dbFile, overwrite = true)
tempFile.delete()

AppDatabase.getInstance(context) // Reinitialize with the new DB file
// restart app process cleanly
val intent = context.packageManager
.getLaunchIntentForPackage(context.packageName)

intent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)

context.startActivity(intent)
exitProcess(0)
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/org/librefit/ui/models/UiBackupEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.librefit.ui.models

sealed interface BackupEvent {
object BackupSuccess : BackupEvent
object RestoreSuccess : BackupEvent
data class Error(val message: String) : BackupEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@

package org.librefit.ui.screens.settings

import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
Expand All @@ -21,9 +25,12 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
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
Expand All @@ -32,6 +39,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
Expand All @@ -53,6 +61,7 @@ import org.librefit.ui.components.HeadlineText
import org.librefit.ui.components.LibreFitLazyColumn
import org.librefit.ui.components.LibreFitScaffold
import org.librefit.ui.components.dialogs.PreferenceDialog
import org.librefit.ui.models.BackupEvent
import org.librefit.ui.theme.LibreFitTheme
import org.librefit.util.Formatter
import kotlin.random.Random
Expand Down Expand Up @@ -105,7 +114,9 @@ fun SettingsScreen(
isSupporter = isSupporter,
isWorkoutHeaderSticky = isWorkoutHeaderSticky,
updatePreferences = viewModel::updatePreferences,
saveBooleanValue = viewModel::savePreference
saveBooleanValue = viewModel::savePreference,
backupExport = viewModel::backupExport,
backupImport = viewModel::backupImport
)
}

Expand All @@ -121,7 +132,9 @@ private fun SettingsScreenContent(
isSupporter: Boolean,
isWorkoutHeaderSticky: Boolean,
updatePreferences: (List<DialogPreference>) -> Unit,
saveBooleanValue: (Preferences.Key<Boolean>, value: Boolean) -> Unit
saveBooleanValue: (Preferences.Key<Boolean>, value: Boolean) -> Unit,
backupExport: (Uri) -> Unit,
backupImport: (Uri) -> Unit
) {
LibreFitScaffold(
title = AnnotatedString(stringResource(id = R.string.settings)),
Expand Down Expand Up @@ -228,6 +241,43 @@ private fun SettingsScreenContent(
settingName = stringResource(R.string.stick_status_bar)
)
}

item {
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/octet-stream")
) { uri ->
uri?.let { backupExport(it) }
}

SettingItem(
onClick = {
val fileName = "librefit-backup.db"
launcher.launch(fileName)
},
icon = painterResource(R.drawable.ic_backup),
settingName = stringResource(id = R.string.export_data),
settingDesc = stringResource(R.string.export_data_desc)
)
}

item {
val launcher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
uri?.let {
backupImport(it)
}
}
SettingItem(
onClick = {
launcher.launch(arrayOf("*/*"))
},
icon = painterResource(R.drawable.ic_restore),
settingName = stringResource(id = R.string.import_data),
settingDesc = stringResource(R.string.import_data_desc)
)
}
}
}
}
Expand Down Expand Up @@ -331,6 +381,8 @@ fun SettingsScreenPreview() {
}
}
},
backupExport = {},
backupImport = {}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,34 @@

package org.librefit.ui.screens.settings

import android.net.Uri
import androidx.datastore.preferences.core.Preferences
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.librefit.db.repository.ImportExportRepository
import org.librefit.db.repository.UserPreferencesRepository
import org.librefit.enums.userPreferences.DialogPreference
import org.librefit.enums.userPreferences.Language
import org.librefit.enums.userPreferences.ThemeMode
import org.librefit.ui.models.BackupEvent
import javax.inject.Inject

@HiltViewModel
class SettingsScreenViewModel @Inject constructor(
private val userPreferences: UserPreferencesRepository
private val userPreferences: UserPreferencesRepository,
private val importExportRepository: ImportExportRepository
) : ViewModel() {
val themeMode = userPreferences.themeMode
val materialMode = userPreferences.materialMode
Expand Down Expand Up @@ -90,4 +96,33 @@ class SettingsScreenViewModel @Inject constructor(
)
}
}

private val _events = MutableSharedFlow<BackupEvent>()
val events = _events.asSharedFlow()
Comment thread
IamDg marked this conversation as resolved.

fun backupExport(uri: Uri) {
viewModelScope.launch {
try {
importExportRepository.exportTo(uri)
} catch (e: Exception) {
// TODO: catch and show
_events.emit(
BackupEvent.Error(e.message ?: "Data backup export failed")
)
}
}
}

fun backupImport(uri: Uri) {
viewModelScope.launch {
try {
importExportRepository.importFrom(uri)
} catch (e: Exception) {
// TODO: catch and show
_events.emit(
BackupEvent.Error(e.message ?: "Data backup restore failed")
)
}
}
}
}
24 changes: 24 additions & 0 deletions app/src/main/res/drawable/ic_backup.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M260,800q-91,0 -155.5,-63T40,583q0,-78 47,-139t123,-78q25,-92 100,-149t170,-57q117,0 198.5,81.5T760,440q69,8 114.5,59.5T920,620q0,75 -52.5,127.5T740,800L520,800q-33,0 -56.5,-23.5T440,720v-206l-64,62 -56,-56 160,-160 160,160 -56,56 -64,-62v206h220q42,0 71,-29t29,-71q0,-42 -29,-71t-71,-29h-60v-80q0,-83 -58.5,-141.5T480,240q-83,0 -141.5,58.5T280,440h-20q-58,0 -99,41t-41,99q0,58 41,99t99,41h100v80L260,800ZM480,520Z"
android:fillColor="#000000"/>
</vector>
24 changes: 24 additions & 0 deletions app/src/main/res/drawable/ic_restore.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M260,800q-91,0 -155.5,-63T40,583q0,-78 47,-139t123,-78q17,-72 85,-137t145,-65q33,0 56.5,23.5T520,244v242l64,-62 56,56 -160,160 -160,-160 56,-56 64,62v-242q-76,14 -118,73.5T280,440h-20q-58,0 -99,41t-41,99q0,58 41,99t99,41h480q42,0 71,-29t29,-71q0,-42 -29,-71t-71,-29h-60v-80q0,-48 -22,-89.5T600,280v-93q74,35 117,103.5T760,440q69,8 114.5,59.5T920,620q0,75 -52.5,127.5T740,800L260,800ZM480,442Z"
android:fillColor="#000000"/>
</vector>
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@
<string name="stick_status_bar">Stick status bar</string>
<string name="stick_status_bar_desc">The status bar of ongoing workout will be stuck at the top of the screen</string>
<string name="not_stick_status_bar_desc">The status bar of ongoing workout won\'t be stuck at the top of the screen</string>
<string name="export_data">Export data</string>
<string name="export_data_desc">Export the current database</string>
<string name="export_data_success">Data exported successfully</string>
<string name="import_data">Import data</string>
<string name="import_data_desc">Import backed up data</string>
<string name="import_data_success">Data imported successfully</string>


<!-- Statistics screen-->
Expand Down
Loading
Loading