mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-18 16:25:27 +02:00
parent
7e2d4d8f3f
commit
f7a89594fa
@ -19,6 +19,7 @@
|
||||
package com.sadellie.unitto.core.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
@ -39,6 +40,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
fun UnittoScreenWithLargeTopBar(
|
||||
title: String,
|
||||
navigationIcon: @Composable () -> Unit,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||
@ -53,7 +55,8 @@ fun UnittoScreenWithLargeTopBar(
|
||||
Text(text = title)
|
||||
},
|
||||
navigationIcon = navigationIcon,
|
||||
scrollBehavior = scrollBehavior
|
||||
scrollBehavior = scrollBehavior,
|
||||
actions = actions
|
||||
)
|
||||
},
|
||||
content = content
|
||||
|
1
data/backup/.gitignore
vendored
Normal file
1
data/backup/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
36
data/backup/build.gradle.kts
Normal file
36
data/backup/build.gradle.kts
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("unitto.library")
|
||||
id("unitto.android.library.jacoco")
|
||||
id("unitto.android.hilt")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.sadellie.unitto.data.backup"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.datastore.datastore.preferences)
|
||||
implementation(libs.com.squareup.moshi.moshi.kotlin)
|
||||
|
||||
implementation(project(":data:database"))
|
||||
implementation(project(":data:model"))
|
||||
implementation(project(":data:userprefs"))
|
||||
}
|
0
data/backup/consumer-rules.pro
Normal file
0
data/backup/consumer-rules.pro
Normal file
@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data.backup
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.sadellie.unitto.data.userprefs.PrefsKeys
|
||||
import com.sadellie.unitto.data.userprefs.getThemingMode
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BackupManagerTest {
|
||||
|
||||
private lateinit var dataStore: DataStore<Preferences>
|
||||
private lateinit var backupManager: BackupManager
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
dataStore = PreferenceDataStoreFactory.create(
|
||||
corruptionHandler = null,
|
||||
produceFile = { appContext.preferencesDataStoreFile("test") }
|
||||
)
|
||||
|
||||
backupManager = BackupManager(
|
||||
mContext = appContext,
|
||||
dataStore = dataStore,
|
||||
unitsDao = FakeUnitsDao,
|
||||
timeZoneDao = FakeTimeZoneDao
|
||||
)
|
||||
|
||||
runBlocking {
|
||||
dataStore.edit {
|
||||
it[PrefsKeys.THEMING_MODE] = FakeUsrPreferenceValues.themingMode
|
||||
it[PrefsKeys.ENABLE_DYNAMIC_THEME] = FakeUsrPreferenceValues.enableDynamicTheme
|
||||
it[PrefsKeys.ENABLE_AMOLED_THEME] = FakeUsrPreferenceValues.enableAmoledTheme
|
||||
it[PrefsKeys.CUSTOM_COLOR] = FakeUsrPreferenceValues.customColor
|
||||
it[PrefsKeys.MONET_MODE] = FakeUsrPreferenceValues.monetMode
|
||||
it[PrefsKeys.STARTING_SCREEN] = FakeUsrPreferenceValues.startingScreen
|
||||
it[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] = FakeUsrPreferenceValues.enableToolsExperiment
|
||||
it[PrefsKeys.ENABLE_VIBRATIONS] = FakeUsrPreferenceValues.enableVibrations
|
||||
it[PrefsKeys.MIDDLE_ZERO] = FakeUsrPreferenceValues.middleZero
|
||||
it[PrefsKeys.AC_BUTTON] = FakeUsrPreferenceValues.acButton
|
||||
it[PrefsKeys.RPN_MODE] = FakeUsrPreferenceValues.rpnMode
|
||||
|
||||
// FORMATTER
|
||||
it[PrefsKeys.DIGITS_PRECISION] = FakeUsrPreferenceValues.precision
|
||||
it[PrefsKeys.SEPARATOR] = FakeUsrPreferenceValues.separator
|
||||
it[PrefsKeys.OUTPUT_FORMAT] = FakeUsrPreferenceValues.outputFormat
|
||||
|
||||
// CALCULATOR
|
||||
it[PrefsKeys.RADIAN_MODE] = FakeUsrPreferenceValues.radianMode
|
||||
it[PrefsKeys.PARTIAL_HISTORY_VIEW] = FakeUsrPreferenceValues.partialHistoryView
|
||||
it[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] = FakeUsrPreferenceValues.clearInputAfterEquals
|
||||
|
||||
// UNIT CONVERTER
|
||||
it[PrefsKeys.LATEST_LEFT_SIDE] = FakeUsrPreferenceValues.latestLeftSide
|
||||
it[PrefsKeys.LATEST_RIGHT_SIDE] = FakeUsrPreferenceValues.latestRightSide
|
||||
it[PrefsKeys.SHOWN_UNIT_GROUPS] = FakeUsrPreferenceValues.shownUnitGroups.joinToString(",")
|
||||
it[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY] = FakeUsrPreferenceValues.unitConverterFavoritesOnly
|
||||
it[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] = FakeUsrPreferenceValues.unitConverterFormatTime
|
||||
it[PrefsKeys.UNIT_CONVERTER_SORTING] = FakeUsrPreferenceValues.unitConverterSorting.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getUserDataTest() = runBlocking{
|
||||
val actualUserData = backupManager.userDataFromApp()
|
||||
val expectedUserData = UserData(
|
||||
themingMode = FakeUsrPreferenceValues.themingMode,
|
||||
enableDynamicTheme = FakeUsrPreferenceValues.enableDynamicTheme,
|
||||
enableAmoledTheme = FakeUsrPreferenceValues.enableAmoledTheme,
|
||||
customColor = FakeUsrPreferenceValues.customColor,
|
||||
monetMode = FakeUsrPreferenceValues.monetMode,
|
||||
startingScreen = FakeUsrPreferenceValues.startingScreen,
|
||||
enableToolsExperiment = FakeUsrPreferenceValues.enableToolsExperiment,
|
||||
enableVibrations = FakeUsrPreferenceValues.enableVibrations,
|
||||
middleZero = FakeUsrPreferenceValues.middleZero,
|
||||
acButton = FakeUsrPreferenceValues.acButton,
|
||||
rpnMode = FakeUsrPreferenceValues.rpnMode,
|
||||
precision = FakeUsrPreferenceValues.precision,
|
||||
separator = FakeUsrPreferenceValues.separator,
|
||||
outputFormat = FakeUsrPreferenceValues.outputFormat,
|
||||
radianMode = FakeUsrPreferenceValues.radianMode,
|
||||
partialHistoryView = FakeUsrPreferenceValues.partialHistoryView,
|
||||
clearInputAfterEquals = FakeUsrPreferenceValues.clearInputAfterEquals,
|
||||
latestLeftSide = FakeUsrPreferenceValues.latestLeftSide,
|
||||
latestRightSide = FakeUsrPreferenceValues.latestRightSide,
|
||||
shownUnitGroups = FakeUsrPreferenceValues.shownUnitGroups,
|
||||
unitConverterFavoritesOnly = FakeUsrPreferenceValues.unitConverterFavoritesOnly,
|
||||
unitConverterFormatTime = FakeUsrPreferenceValues.unitConverterFormatTime,
|
||||
unitConverterSorting = FakeUsrPreferenceValues.unitConverterSorting,
|
||||
unitsTable = units,
|
||||
timeZoneTable = timeZones
|
||||
)
|
||||
|
||||
assertEquals(expectedUserData, actualUserData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateDatastoreTest() = runBlocking{
|
||||
backupManager.updateDatastore(
|
||||
UserData(
|
||||
themingMode = FakeUsrPreferenceValues.themingMode,
|
||||
enableDynamicTheme = FakeUsrPreferenceValues.enableDynamicTheme,
|
||||
enableAmoledTheme = FakeUsrPreferenceValues.enableAmoledTheme,
|
||||
customColor = FakeUsrPreferenceValues.customColor,
|
||||
monetMode = FakeUsrPreferenceValues.monetMode,
|
||||
startingScreen = FakeUsrPreferenceValues.startingScreen,
|
||||
enableToolsExperiment = FakeUsrPreferenceValues.enableToolsExperiment,
|
||||
enableVibrations = FakeUsrPreferenceValues.enableVibrations,
|
||||
middleZero = FakeUsrPreferenceValues.middleZero,
|
||||
acButton = FakeUsrPreferenceValues.acButton,
|
||||
rpnMode = FakeUsrPreferenceValues.rpnMode,
|
||||
precision = FakeUsrPreferenceValues.precision,
|
||||
separator = FakeUsrPreferenceValues.separator,
|
||||
outputFormat = FakeUsrPreferenceValues.outputFormat,
|
||||
radianMode = FakeUsrPreferenceValues.radianMode,
|
||||
partialHistoryView = FakeUsrPreferenceValues.partialHistoryView,
|
||||
clearInputAfterEquals = FakeUsrPreferenceValues.clearInputAfterEquals,
|
||||
latestLeftSide = FakeUsrPreferenceValues.latestLeftSide,
|
||||
latestRightSide = FakeUsrPreferenceValues.latestRightSide,
|
||||
shownUnitGroups = FakeUsrPreferenceValues.shownUnitGroups,
|
||||
unitConverterFavoritesOnly = FakeUsrPreferenceValues.unitConverterFavoritesOnly,
|
||||
unitConverterFormatTime = FakeUsrPreferenceValues.unitConverterFormatTime,
|
||||
unitConverterSorting = FakeUsrPreferenceValues.unitConverterSorting,
|
||||
unitsTable = units,
|
||||
timeZoneTable = timeZones
|
||||
)
|
||||
)
|
||||
|
||||
val data = dataStore.data.first()
|
||||
// TODO Wrong implementation, should test all
|
||||
assertEquals(FakeUsrPreferenceValues.themingMode, data.getThemingMode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateUnitsTableTest() = runBlocking {
|
||||
backupManager.updateUnitsTable(
|
||||
UserData(
|
||||
themingMode = FakeUsrPreferenceValues.themingMode,
|
||||
enableDynamicTheme = FakeUsrPreferenceValues.enableDynamicTheme,
|
||||
enableAmoledTheme = FakeUsrPreferenceValues.enableAmoledTheme,
|
||||
customColor = FakeUsrPreferenceValues.customColor,
|
||||
monetMode = FakeUsrPreferenceValues.monetMode,
|
||||
startingScreen = FakeUsrPreferenceValues.startingScreen,
|
||||
enableToolsExperiment = FakeUsrPreferenceValues.enableToolsExperiment,
|
||||
enableVibrations = FakeUsrPreferenceValues.enableVibrations,
|
||||
middleZero = FakeUsrPreferenceValues.middleZero,
|
||||
acButton = FakeUsrPreferenceValues.acButton,
|
||||
rpnMode = FakeUsrPreferenceValues.rpnMode,
|
||||
precision = FakeUsrPreferenceValues.precision,
|
||||
separator = FakeUsrPreferenceValues.separator,
|
||||
outputFormat = FakeUsrPreferenceValues.outputFormat,
|
||||
radianMode = FakeUsrPreferenceValues.radianMode,
|
||||
partialHistoryView = FakeUsrPreferenceValues.partialHistoryView,
|
||||
clearInputAfterEquals = FakeUsrPreferenceValues.clearInputAfterEquals,
|
||||
latestLeftSide = FakeUsrPreferenceValues.latestLeftSide,
|
||||
latestRightSide = FakeUsrPreferenceValues.latestRightSide,
|
||||
shownUnitGroups = FakeUsrPreferenceValues.shownUnitGroups,
|
||||
unitConverterFavoritesOnly = FakeUsrPreferenceValues.unitConverterFavoritesOnly,
|
||||
unitConverterFormatTime = FakeUsrPreferenceValues.unitConverterFormatTime,
|
||||
unitConverterSorting = FakeUsrPreferenceValues.unitConverterSorting,
|
||||
unitsTable = emptyList(),
|
||||
timeZoneTable = timeZones
|
||||
)
|
||||
)
|
||||
|
||||
val data = FakeUnitsDao.getAllFlow().first()
|
||||
// TODO Wrong implementation
|
||||
assertEquals("$units | $data", units, data)
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data.backup
|
||||
|
||||
import com.sadellie.unitto.data.database.TimeZoneDao
|
||||
import com.sadellie.unitto.data.database.TimeZoneEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
val timeZones = listOf(
|
||||
TimeZoneEntity(
|
||||
id = "id1",
|
||||
position = 9,
|
||||
label = "label"
|
||||
),
|
||||
TimeZoneEntity(
|
||||
id = "id2",
|
||||
position = 9,
|
||||
label = "label"
|
||||
)
|
||||
)
|
||||
|
||||
object FakeTimeZoneDao: TimeZoneDao {
|
||||
override fun getFavorites(): Flow<List<TimeZoneEntity>> {
|
||||
return flow {
|
||||
emit(timeZones)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMaxPosition(): Int = 0
|
||||
override suspend fun insert(vararg timeZoneEntity: TimeZoneEntity) {}
|
||||
override suspend fun removeFromFavorites(id: String) {}
|
||||
override suspend fun updateLabel(id: String, label: String) {}
|
||||
override suspend fun updateDragged(id: String, oldPosition: Int, newPosition: Int) {}
|
||||
override suspend fun moveDown(currentPosition: Int, targetPosition: Int) {}
|
||||
override suspend fun moveUp(currentPosition: Int, targetPosition: Int) {}
|
||||
override suspend fun clear() {}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data.backup
|
||||
|
||||
import com.sadellie.unitto.data.database.UnitsDao
|
||||
import com.sadellie.unitto.data.database.UnitsEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
val units = listOf(
|
||||
UnitsEntity(
|
||||
unitId = "UnitId1",
|
||||
isFavorite = false,
|
||||
pairedUnitId = "Pair",
|
||||
frequency = 9
|
||||
),
|
||||
UnitsEntity(
|
||||
unitId = "UnitId2",
|
||||
isFavorite = false,
|
||||
pairedUnitId = "Pair",
|
||||
frequency = 9
|
||||
)
|
||||
)
|
||||
|
||||
object FakeUnitsDao : UnitsDao {
|
||||
override fun getAllFlow(): Flow<List<UnitsEntity>> {
|
||||
return flow {
|
||||
emit(units)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun insertUnit(unit: UnitsEntity) {}
|
||||
override suspend fun getById(unitId: String): UnitsEntity? = null
|
||||
override suspend fun clear() {}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data.backup
|
||||
|
||||
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
|
||||
import com.sadellie.unitto.data.model.UnitsListSorting
|
||||
|
||||
object FakeUsrPreferenceValues {
|
||||
const val themingMode = "ThemingMode"
|
||||
const val enableDynamicTheme = false
|
||||
const val enableAmoledTheme = false
|
||||
const val customColor = 777L
|
||||
const val monetMode = "MonetMode"
|
||||
const val startingScreen = "StartingScreen"
|
||||
const val enableToolsExperiment = false
|
||||
const val enableVibrations = false
|
||||
const val middleZero = false
|
||||
const val acButton = false
|
||||
const val rpnMode = false
|
||||
const val precision = 69
|
||||
const val separator = 1
|
||||
const val outputFormat = 1
|
||||
const val radianMode = false
|
||||
const val partialHistoryView = false
|
||||
const val clearInputAfterEquals = false
|
||||
const val latestLeftSide = "LeftSideUnit"
|
||||
const val latestRightSide = "RightSideUnit"
|
||||
val shownUnitGroups = ALL_UNIT_GROUPS
|
||||
const val unitConverterFavoritesOnly = false
|
||||
const val unitConverterFormatTime = false
|
||||
val unitConverterSorting = UnitsListSorting.USAGE
|
||||
}
|
32
data/backup/src/main/AndroidManifest.xml
Normal file
32
data/backup/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Unitto is a unit converter for Android
|
||||
~ Copyright (c) 2023 Elshan Agaev
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="com.sadellie.unitto.BackupManager"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider_path"/>
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import com.sadellie.unitto.data.database.TimeZoneDao
|
||||
import com.sadellie.unitto.data.database.UnitsDao
|
||||
import com.sadellie.unitto.data.userprefs.PrefsKeys
|
||||
import com.sadellie.unitto.data.userprefs.getAcButton
|
||||
import com.sadellie.unitto.data.userprefs.getClearInputAfterEquals
|
||||
import com.sadellie.unitto.data.userprefs.getCustomColor
|
||||
import com.sadellie.unitto.data.userprefs.getDigitsPrecision
|
||||
import com.sadellie.unitto.data.userprefs.getEnableAmoledTheme
|
||||
import com.sadellie.unitto.data.userprefs.getEnableDynamicTheme
|
||||
import com.sadellie.unitto.data.userprefs.getEnableToolsExperiment
|
||||
import com.sadellie.unitto.data.userprefs.getEnableVibrations
|
||||
import com.sadellie.unitto.data.userprefs.getLatestLeftSide
|
||||
import com.sadellie.unitto.data.userprefs.getLatestRightSide
|
||||
import com.sadellie.unitto.data.userprefs.getMiddleZero
|
||||
import com.sadellie.unitto.data.userprefs.getMonetMode
|
||||
import com.sadellie.unitto.data.userprefs.getOutputFormat
|
||||
import com.sadellie.unitto.data.userprefs.getPartialHistoryView
|
||||
import com.sadellie.unitto.data.userprefs.getRadianMode
|
||||
import com.sadellie.unitto.data.userprefs.getRpnMode
|
||||
import com.sadellie.unitto.data.userprefs.getSeparator
|
||||
import com.sadellie.unitto.data.userprefs.getShownUnitGroups
|
||||
import com.sadellie.unitto.data.userprefs.getStartingScreen
|
||||
import com.sadellie.unitto.data.userprefs.getThemingMode
|
||||
import com.sadellie.unitto.data.userprefs.getUnitConverterFavoritesOnly
|
||||
import com.sadellie.unitto.data.userprefs.getUnitConverterFormatTime
|
||||
import com.sadellie.unitto.data.userprefs.getUnitConverterSorting
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
import javax.inject.Inject
|
||||
|
||||
class BackupManager @Inject constructor(
|
||||
@ApplicationContext private val mContext: Context,
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
private val unitsDao: UnitsDao,
|
||||
private val timeZoneDao: TimeZoneDao,
|
||||
// Not planned at the moment
|
||||
// private val calculatorHistoryDao: CalculatorHistoryDao,
|
||||
) {
|
||||
private val moshi: Moshi = Moshi.Builder()
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
private val jsonAdapter: JsonAdapter<UserData> = moshi.adapter(UserData::class.java)
|
||||
private val auth = "com.sadellie.unitto.BackupManager"
|
||||
|
||||
suspend fun backup(): Uri = withContext(Dispatchers.IO) {
|
||||
val userData = userDataFromApp()
|
||||
// save to disk
|
||||
val tempFile = File.createTempFile("backup", ".unitto", mContext.cacheDir)
|
||||
tempFile.writeText(jsonAdapter.toJson(userData))
|
||||
|
||||
return@withContext FileProvider.getUriForFile(mContext, auth, tempFile)
|
||||
}
|
||||
|
||||
suspend fun restore(uri: Uri) = withContext(Dispatchers.IO) {
|
||||
val jsonContent = StringBuilder()
|
||||
mContext.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
BufferedReader(InputStreamReader(inputStream)).use { reader ->
|
||||
var line: String? = reader.readLine()
|
||||
while (line != null) {
|
||||
jsonContent.append(line)
|
||||
line = reader.readLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return error
|
||||
val userData: UserData = jsonAdapter.fromJson(jsonContent.toString()) ?: return@withContext IllegalArgumentException("Can't parse: $jsonContent")
|
||||
|
||||
// Apply tables
|
||||
updateUnitsTable(userData)
|
||||
updateTimeZonesTable(userData)
|
||||
|
||||
// Apply datastore prefs
|
||||
// Datastore settings are restored at the end, because it will trigger recomposition of the
|
||||
// entire app composable and all jobs in ViewModels will be canceled
|
||||
updateDatastore(userData)
|
||||
}
|
||||
|
||||
internal suspend fun userDataFromApp(): UserData {
|
||||
val data = dataStore.data.first()
|
||||
val unitsTableData = unitsDao.getAllFlow().first()
|
||||
val timeZoneTableData = timeZoneDao.getFavorites().first()
|
||||
|
||||
return UserData(
|
||||
themingMode = data.getThemingMode(),
|
||||
enableDynamicTheme = data.getEnableDynamicTheme(),
|
||||
enableAmoledTheme = data.getEnableAmoledTheme(),
|
||||
customColor = data.getCustomColor(),
|
||||
monetMode = data.getMonetMode(),
|
||||
startingScreen = data.getStartingScreen(),
|
||||
enableToolsExperiment = data.getEnableToolsExperiment(),
|
||||
enableVibrations = data.getEnableVibrations(),
|
||||
middleZero = data.getMiddleZero(),
|
||||
acButton = data.getAcButton(),
|
||||
rpnMode = data.getRpnMode(),
|
||||
precision = data.getDigitsPrecision(),
|
||||
separator = data.getSeparator(),
|
||||
outputFormat = data.getOutputFormat(),
|
||||
radianMode = data.getRadianMode(),
|
||||
partialHistoryView = data.getPartialHistoryView(),
|
||||
clearInputAfterEquals = data.getClearInputAfterEquals(),
|
||||
latestLeftSide = data.getLatestLeftSide(),
|
||||
latestRightSide = data.getLatestRightSide(),
|
||||
shownUnitGroups = data.getShownUnitGroups(),
|
||||
unitConverterFavoritesOnly = data.getUnitConverterFavoritesOnly(),
|
||||
unitConverterFormatTime = data.getUnitConverterFormatTime(),
|
||||
unitConverterSorting = data.getUnitConverterSorting(),
|
||||
unitsTable = unitsTableData,
|
||||
timeZoneTable = timeZoneTableData,
|
||||
)
|
||||
}
|
||||
|
||||
internal suspend fun updateDatastore(userData: UserData) {
|
||||
dataStore.edit { it.clear() }
|
||||
dataStore.edit {
|
||||
it[PrefsKeys.THEMING_MODE] = userData.themingMode
|
||||
it[PrefsKeys.ENABLE_DYNAMIC_THEME] = userData.enableDynamicTheme
|
||||
it[PrefsKeys.ENABLE_AMOLED_THEME] = userData.enableAmoledTheme
|
||||
it[PrefsKeys.CUSTOM_COLOR] = userData.customColor
|
||||
it[PrefsKeys.MONET_MODE] = userData.monetMode
|
||||
it[PrefsKeys.STARTING_SCREEN] = userData.startingScreen
|
||||
it[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] = userData.enableToolsExperiment
|
||||
it[PrefsKeys.ENABLE_VIBRATIONS] = userData.enableVibrations
|
||||
it[PrefsKeys.MIDDLE_ZERO] = userData.middleZero
|
||||
it[PrefsKeys.AC_BUTTON] = userData.acButton
|
||||
it[PrefsKeys.RPN_MODE] = userData.rpnMode
|
||||
|
||||
// FORMATTER
|
||||
it[PrefsKeys.DIGITS_PRECISION] = userData.precision
|
||||
it[PrefsKeys.SEPARATOR] = userData.separator
|
||||
it[PrefsKeys.OUTPUT_FORMAT] = userData.outputFormat
|
||||
|
||||
// CALCULATOR
|
||||
it[PrefsKeys.RADIAN_MODE] = userData.radianMode
|
||||
it[PrefsKeys.PARTIAL_HISTORY_VIEW] = userData.partialHistoryView
|
||||
it[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] = userData.clearInputAfterEquals
|
||||
|
||||
// UNIT CONVERTER
|
||||
it[PrefsKeys.LATEST_LEFT_SIDE] = userData.latestLeftSide
|
||||
it[PrefsKeys.LATEST_RIGHT_SIDE] = userData.latestRightSide
|
||||
it[PrefsKeys.SHOWN_UNIT_GROUPS] = userData.shownUnitGroups.joinToString(",")
|
||||
it[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY] = userData.unitConverterFavoritesOnly
|
||||
it[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] = userData.unitConverterFormatTime
|
||||
it[PrefsKeys.UNIT_CONVERTER_SORTING] = userData.unitConverterSorting.name
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun updateUnitsTable(userData: UserData) {
|
||||
unitsDao.clear()
|
||||
userData.unitsTable.forEach { unitsDao.insertUnit(it) }
|
||||
}
|
||||
|
||||
internal suspend fun updateTimeZonesTable(userData: UserData) {
|
||||
timeZoneDao.clear()
|
||||
userData.timeZoneTable.forEach { timeZoneDao.insert(it) }
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data.backup
|
||||
|
||||
import com.sadellie.unitto.data.database.TimeZoneEntity
|
||||
import com.sadellie.unitto.data.database.UnitsEntity
|
||||
import com.sadellie.unitto.data.model.UnitGroup
|
||||
import com.sadellie.unitto.data.model.UnitsListSorting
|
||||
|
||||
// Don't move to model module. This uses entity classes from database module
|
||||
data class UserData(
|
||||
val themingMode: String,
|
||||
val enableDynamicTheme: Boolean,
|
||||
val enableAmoledTheme: Boolean,
|
||||
val customColor: Long,
|
||||
val monetMode: String,
|
||||
val startingScreen: String,
|
||||
val enableToolsExperiment: Boolean,
|
||||
val enableVibrations: Boolean,
|
||||
val middleZero: Boolean,
|
||||
val acButton: Boolean,
|
||||
val rpnMode: Boolean,
|
||||
|
||||
val precision: Int,
|
||||
val separator: Int,
|
||||
val outputFormat: Int,
|
||||
|
||||
val radianMode: Boolean,
|
||||
val partialHistoryView: Boolean,
|
||||
val clearInputAfterEquals: Boolean,
|
||||
|
||||
val latestLeftSide: String,
|
||||
val latestRightSide: String,
|
||||
val shownUnitGroups: List<UnitGroup>,
|
||||
val unitConverterFavoritesOnly: Boolean,
|
||||
val unitConverterFormatTime: Boolean,
|
||||
val unitConverterSorting: UnitsListSorting,
|
||||
|
||||
val unitsTable: List<UnitsEntity>,
|
||||
val timeZoneTable: List<TimeZoneEntity>,
|
||||
)
|
21
data/backup/src/main/res/xml/file_provider_path.xml
Normal file
21
data/backup/src/main/res/xml/file_provider_path.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Unitto is a unit converter for Android
|
||||
~ Copyright (c) 2023 Elshan Agaev
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<paths>
|
||||
<cache-path name="cache" path="."/>
|
||||
</paths>
|
@ -74,4 +74,7 @@ interface TimeZoneDao {
|
||||
}
|
||||
updateDragged(id, 0, targetPosition)
|
||||
}
|
||||
|
||||
@Query("DELETE FROM time_zones")
|
||||
suspend fun clear()
|
||||
}
|
||||
|
@ -35,4 +35,7 @@ interface UnitsDao {
|
||||
|
||||
@Query("SELECT * FROM units WHERE unitId == :unitId LIMIT 1")
|
||||
suspend fun getById(unitId: String): UnitsEntity?
|
||||
|
||||
@Query("DELETE FROM units")
|
||||
suspend fun clear()
|
||||
}
|
||||
|
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data.userprefs
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.sadellie.unitto.core.base.OutputFormat
|
||||
import com.sadellie.unitto.core.base.Separator
|
||||
import com.sadellie.unitto.core.base.TopLevelDestinations
|
||||
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
|
||||
import com.sadellie.unitto.data.model.UnitGroup
|
||||
import com.sadellie.unitto.data.model.UnitsListSorting
|
||||
import com.sadellie.unitto.data.units.MyUnitIDS
|
||||
|
||||
fun Preferences.getEnableDynamicTheme(): Boolean {
|
||||
return this[PrefsKeys.ENABLE_DYNAMIC_THEME] ?: true
|
||||
}
|
||||
|
||||
fun Preferences.getThemingMode(): String {
|
||||
return this[PrefsKeys.THEMING_MODE] ?: ""
|
||||
}
|
||||
|
||||
fun Preferences.getEnableAmoledTheme(): Boolean {
|
||||
return this[PrefsKeys.ENABLE_AMOLED_THEME] ?: false
|
||||
}
|
||||
|
||||
fun Preferences.getCustomColor(): Long {
|
||||
return this[PrefsKeys.CUSTOM_COLOR] ?: 16L // From Color.Unspecified
|
||||
}
|
||||
|
||||
fun Preferences.getMonetMode(): String {
|
||||
return this[PrefsKeys.MONET_MODE] ?: ""
|
||||
}
|
||||
|
||||
fun Preferences.getStartingScreen(): String {
|
||||
return this[PrefsKeys.STARTING_SCREEN] ?: TopLevelDestinations.Calculator.graph
|
||||
}
|
||||
|
||||
fun Preferences.getEnableToolsExperiment(): Boolean {
|
||||
return this[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] ?: false
|
||||
}
|
||||
|
||||
fun Preferences.getEnableVibrations(): Boolean {
|
||||
return this[PrefsKeys.ENABLE_VIBRATIONS] ?: true
|
||||
}
|
||||
|
||||
fun Preferences.getRadianMode(): Boolean {
|
||||
return this[PrefsKeys.RADIAN_MODE] ?: true
|
||||
}
|
||||
|
||||
fun Preferences.getSeparator(): Int {
|
||||
return this[PrefsKeys.SEPARATOR] ?: Separator.SPACE
|
||||
}
|
||||
|
||||
fun Preferences.getMiddleZero(): Boolean {
|
||||
return this[PrefsKeys.MIDDLE_ZERO] ?: true
|
||||
}
|
||||
|
||||
fun Preferences.getPartialHistoryView(): Boolean {
|
||||
return this[PrefsKeys.PARTIAL_HISTORY_VIEW] ?: true
|
||||
}
|
||||
|
||||
fun Preferences.getDigitsPrecision(): Int {
|
||||
return this[PrefsKeys.DIGITS_PRECISION] ?: 3
|
||||
}
|
||||
|
||||
fun Preferences.getOutputFormat(): Int {
|
||||
return this[PrefsKeys.OUTPUT_FORMAT] ?: OutputFormat.PLAIN
|
||||
}
|
||||
|
||||
fun Preferences.getUnitConverterFormatTime(): Boolean {
|
||||
return this[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] ?: false
|
||||
}
|
||||
|
||||
fun Preferences.getUnitConverterSorting(): UnitsListSorting {
|
||||
return this[PrefsKeys.UNIT_CONVERTER_SORTING]
|
||||
?.let { UnitsListSorting.valueOf(it) } ?: UnitsListSorting.USAGE
|
||||
}
|
||||
|
||||
fun Preferences.getShownUnitGroups(): List<UnitGroup> {
|
||||
return this[PrefsKeys.SHOWN_UNIT_GROUPS]
|
||||
?.letTryOrNull { list ->
|
||||
list
|
||||
.ifEmpty { return@letTryOrNull listOf() }
|
||||
.split(",")
|
||||
.map { UnitGroup.valueOf(it) }
|
||||
}
|
||||
?: ALL_UNIT_GROUPS
|
||||
}
|
||||
|
||||
fun Preferences.getUnitConverterFavoritesOnly(): Boolean {
|
||||
return this[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY]
|
||||
?: false
|
||||
}
|
||||
|
||||
fun Preferences.getLatestLeftSide(): String {
|
||||
return this[PrefsKeys.LATEST_LEFT_SIDE] ?: MyUnitIDS.kilometer
|
||||
}
|
||||
|
||||
fun Preferences.getLatestRightSide(): String {
|
||||
return this[PrefsKeys.LATEST_RIGHT_SIDE] ?: MyUnitIDS.mile
|
||||
}
|
||||
|
||||
fun Preferences.getAcButton(): Boolean {
|
||||
return this[PrefsKeys.AC_BUTTON] ?: true
|
||||
}
|
||||
|
||||
fun Preferences.getClearInputAfterEquals(): Boolean {
|
||||
return this[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] ?: true
|
||||
}
|
||||
|
||||
fun Preferences.getRpnMode(): Boolean {
|
||||
return this[PrefsKeys.RPN_MODE] ?: false
|
||||
}
|
||||
|
||||
private inline fun <T, R> T.letTryOrNull(block: (T) -> R): R? = try {
|
||||
this?.let(block)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
@ -23,7 +23,7 @@ import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
|
||||
internal object PrefsKeys {
|
||||
object PrefsKeys {
|
||||
// COMMON
|
||||
val THEMING_MODE = stringPreferencesKey("THEMING_MODE_PREF_KEY")
|
||||
val ENABLE_DYNAMIC_THEME = booleanPreferencesKey("ENABLE_DYNAMIC_THEME_PREF_KEY")
|
||||
|
@ -22,10 +22,6 @@ import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import com.sadellie.unitto.core.base.OutputFormat
|
||||
import com.sadellie.unitto.core.base.Separator
|
||||
import com.sadellie.unitto.core.base.TopLevelDestinations
|
||||
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
|
||||
import com.sadellie.unitto.data.model.UnitGroup
|
||||
import com.sadellie.unitto.data.model.UnitsListSorting
|
||||
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
|
||||
@ -40,7 +36,6 @@ import com.sadellie.unitto.data.model.userprefs.FormattingPreferences
|
||||
import com.sadellie.unitto.data.model.userprefs.GeneralPreferences
|
||||
import com.sadellie.unitto.data.model.userprefs.StartingScreenPreferences
|
||||
import com.sadellie.unitto.data.model.userprefs.UnitGroupsPreferences
|
||||
import com.sadellie.unitto.data.units.MyUnitIDS
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
@ -287,107 +282,3 @@ class UserPreferencesRepositoryImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Preferences.getEnableDynamicTheme(): Boolean {
|
||||
return this[PrefsKeys.ENABLE_DYNAMIC_THEME] ?: true
|
||||
}
|
||||
|
||||
private fun Preferences.getThemingMode(): String {
|
||||
return this[PrefsKeys.THEMING_MODE] ?: ""
|
||||
}
|
||||
|
||||
private fun Preferences.getEnableAmoledTheme(): Boolean {
|
||||
return this[PrefsKeys.ENABLE_AMOLED_THEME] ?: false
|
||||
}
|
||||
|
||||
private fun Preferences.getCustomColor(): Long {
|
||||
return this[PrefsKeys.CUSTOM_COLOR] ?: 16L // From Color.Unspecified
|
||||
}
|
||||
|
||||
private fun Preferences.getMonetMode(): String {
|
||||
return this[PrefsKeys.MONET_MODE] ?: ""
|
||||
}
|
||||
|
||||
private fun Preferences.getStartingScreen(): String {
|
||||
return this[PrefsKeys.STARTING_SCREEN]
|
||||
?: TopLevelDestinations.Calculator.graph
|
||||
}
|
||||
|
||||
private fun Preferences.getEnableToolsExperiment(): Boolean {
|
||||
return this[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] ?: false
|
||||
}
|
||||
|
||||
private fun Preferences.getEnableVibrations(): Boolean {
|
||||
return this[PrefsKeys.ENABLE_VIBRATIONS] ?: true
|
||||
}
|
||||
|
||||
private fun Preferences.getRadianMode(): Boolean {
|
||||
return this[PrefsKeys.RADIAN_MODE] ?: true
|
||||
}
|
||||
|
||||
private fun Preferences.getSeparator(): Int {
|
||||
return this[PrefsKeys.SEPARATOR] ?: Separator.SPACE
|
||||
}
|
||||
|
||||
private fun Preferences.getMiddleZero(): Boolean {
|
||||
return this[PrefsKeys.MIDDLE_ZERO] ?: true
|
||||
}
|
||||
|
||||
private fun Preferences.getPartialHistoryView(): Boolean {
|
||||
return this[PrefsKeys.PARTIAL_HISTORY_VIEW] ?: true
|
||||
}
|
||||
|
||||
private fun Preferences.getDigitsPrecision(): Int {
|
||||
return this[PrefsKeys.DIGITS_PRECISION] ?: 3
|
||||
}
|
||||
|
||||
private fun Preferences.getOutputFormat(): Int {
|
||||
return this[PrefsKeys.OUTPUT_FORMAT] ?: OutputFormat.PLAIN
|
||||
}
|
||||
|
||||
private fun Preferences.getUnitConverterFormatTime(): Boolean {
|
||||
return this[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] ?: false
|
||||
}
|
||||
|
||||
private fun Preferences.getUnitConverterSorting(): UnitsListSorting {
|
||||
return this[PrefsKeys.UNIT_CONVERTER_SORTING]
|
||||
?.let { UnitsListSorting.valueOf(it) } ?: UnitsListSorting.USAGE
|
||||
}
|
||||
|
||||
private fun Preferences.getShownUnitGroups(): List<UnitGroup> {
|
||||
return this[PrefsKeys.SHOWN_UNIT_GROUPS]?.letTryOrNull { list ->
|
||||
list.ifEmpty { return@letTryOrNull listOf() }.split(",")
|
||||
.map { UnitGroup.valueOf(it) }
|
||||
} ?: ALL_UNIT_GROUPS
|
||||
}
|
||||
|
||||
private fun Preferences.getUnitConverterFavoritesOnly(): Boolean {
|
||||
return this[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY]
|
||||
?: false
|
||||
}
|
||||
|
||||
private fun Preferences.getLatestLeftSide(): String {
|
||||
return this[PrefsKeys.LATEST_LEFT_SIDE] ?: MyUnitIDS.kilometer
|
||||
}
|
||||
|
||||
private fun Preferences.getLatestRightSide(): String {
|
||||
return this[PrefsKeys.LATEST_RIGHT_SIDE] ?: MyUnitIDS.mile
|
||||
}
|
||||
|
||||
private fun Preferences.getAcButton(): Boolean {
|
||||
return this[PrefsKeys.AC_BUTTON] ?: true
|
||||
}
|
||||
|
||||
private fun Preferences.getClearInputAfterEquals(): Boolean {
|
||||
return this[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] ?: true
|
||||
}
|
||||
|
||||
private fun Preferences.getRpnMode(): Boolean {
|
||||
return this[PrefsKeys.RPN_MODE] ?: false
|
||||
}
|
||||
|
||||
private inline fun <T, R> T.letTryOrNull(block: (T) -> R): R? = try {
|
||||
this?.let(block)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ dependencies {
|
||||
implementation(libs.org.burnoutcrew.composereorderable.reorderable)
|
||||
implementation(libs.androidx.appcompat.appcompat)
|
||||
|
||||
implementation(project(":data:backup"))
|
||||
implementation(project(":data:common"))
|
||||
implementation(project(":data:database"))
|
||||
implementation(project(":data:model"))
|
||||
|
@ -18,12 +18,21 @@
|
||||
|
||||
package com.sadellie.unitto.feature.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -34,16 +43,27 @@ import androidx.compose.material.icons.filled.Cached
|
||||
import androidx.compose.material.icons.filled.Calculate
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Palette
|
||||
import androidx.compose.material.icons.filled.RateReview
|
||||
import androidx.compose.material.icons.filled.SwapHoriz
|
||||
import androidx.compose.material.icons.filled.Vibration
|
||||
import androidx.compose.material.icons.filled._123
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -59,8 +79,6 @@ import com.sadellie.unitto.core.ui.common.UnittoListItem
|
||||
import com.sadellie.unitto.core.ui.common.UnittoScreenWithLargeTopBar
|
||||
import com.sadellie.unitto.core.ui.openLink
|
||||
import com.sadellie.unitto.core.ui.showToast
|
||||
import com.sadellie.unitto.data.model.userprefs.GeneralPreferences
|
||||
import com.sadellie.unitto.data.userprefs.GeneralPreferencesImpl
|
||||
import com.sadellie.unitto.feature.settings.navigation.aboutRoute
|
||||
import com.sadellie.unitto.feature.settings.navigation.calculatorSettingsRoute
|
||||
import com.sadellie.unitto.feature.settings.navigation.converterSettingsRoute
|
||||
@ -74,38 +92,99 @@ internal fun SettingsRoute(
|
||||
navigateUp: () -> Unit,
|
||||
navControllerAction: (String) -> Unit,
|
||||
) {
|
||||
val userPrefs = viewModel.userPrefs.collectAsStateWithLifecycle().value
|
||||
val cachePercentage = viewModel.cachePercentage.collectAsStateWithLifecycle()
|
||||
val mContext = LocalContext.current
|
||||
|
||||
when (userPrefs) {
|
||||
null -> UnittoEmptyScreen()
|
||||
else -> {
|
||||
SettingsScreen(
|
||||
userPrefs = userPrefs,
|
||||
val errorLabel = stringResource(R.string.error_label)
|
||||
|
||||
val uiState: SettingsUIState = viewModel.uiState.collectAsStateWithLifecycle().value
|
||||
val backupFileUri: Uri? = viewModel.backupFileUri.collectAsStateWithLifecycle(initialValue = null).value
|
||||
val showErrorToast: Boolean = viewModel.showErrorToast.collectAsStateWithLifecycle(initialValue = false).value
|
||||
|
||||
// Share backup file when it's emitted
|
||||
LaunchedEffect(backupFileUri) {
|
||||
if (backupFileUri == null) return@LaunchedEffect
|
||||
mContext.share(backupFileUri)
|
||||
}
|
||||
|
||||
LaunchedEffect(showErrorToast) {
|
||||
if (showErrorToast) Toast.makeText(mContext, errorLabel, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
Crossfade(targetState = uiState) { state ->
|
||||
when (state) {
|
||||
SettingsUIState.Loading -> UnittoEmptyScreen()
|
||||
|
||||
SettingsUIState.BackupInProgress -> BackingUpScreen()
|
||||
|
||||
is SettingsUIState.Ready -> SettingsScreen(
|
||||
uiState = state,
|
||||
navigateUp = navigateUp,
|
||||
navControllerAction = navControllerAction,
|
||||
updateVibrations = viewModel::updateVibrations,
|
||||
cachePercentage = cachePercentage.value,
|
||||
clearCache = viewModel::clearCache,
|
||||
backup = viewModel::backup,
|
||||
restore = viewModel::restore
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackingUpScreen() {
|
||||
Scaffold { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsScreen(
|
||||
userPrefs: GeneralPreferences,
|
||||
uiState: SettingsUIState.Ready,
|
||||
navigateUp: () -> Unit,
|
||||
navControllerAction: (String) -> Unit,
|
||||
updateVibrations: (Boolean) -> Unit,
|
||||
cachePercentage: Float,
|
||||
clearCache: () -> Unit,
|
||||
backup: () -> Unit,
|
||||
restore: (Uri) -> Unit = {},
|
||||
) {
|
||||
val mContext = LocalContext.current
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
// Pass picked file uri to BackupManager
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { pickedUri ->
|
||||
if (pickedUri != null) restore(pickedUri)
|
||||
}
|
||||
|
||||
UnittoScreenWithLargeTopBar(
|
||||
title = stringResource(R.string.settings_title),
|
||||
navigationIcon = { NavigateUpButton(navigateUp) }
|
||||
navigationIcon = { NavigateUpButton(navigateUp) },
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu },
|
||||
content = { Icon(Icons.Default.MoreVert, null) }
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = backup,
|
||||
text = { Text("Backup") }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = { launcher.launch(arrayOf(backupMimeType)) },
|
||||
text = { Text("Restore") }
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@ -155,12 +234,12 @@ private fun SettingsScreen(
|
||||
headlineText = stringResource(R.string.settings_vibrations),
|
||||
supportingText = stringResource(R.string.settings_vibrations_support),
|
||||
modifier = Modifier.clickable { navControllerAction(converterSettingsRoute) },
|
||||
switchState = userPrefs.enableVibrations,
|
||||
switchState = uiState.enableVibrations,
|
||||
onSwitchChange = updateVibrations
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = cachePercentage > 0,
|
||||
visible = uiState.cacheSize > 0,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
@ -189,19 +268,38 @@ private fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.share(uri: Uri) {
|
||||
val shareIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
type = backupMimeType // This is a fucking war crime, it should be text/json
|
||||
}
|
||||
|
||||
startActivity(shareIntent)
|
||||
}
|
||||
|
||||
private const val backupMimeType = "application/octet-stream"
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewSettingsScreen() {
|
||||
var cacheSize by remember { mutableFloatStateOf(0.9f) }
|
||||
|
||||
SettingsScreen(
|
||||
userPrefs = GeneralPreferencesImpl(
|
||||
enableVibrations = true
|
||||
uiState = SettingsUIState.Ready(
|
||||
enableVibrations = false,
|
||||
cacheSize = 2,
|
||||
),
|
||||
navigateUp = {},
|
||||
navControllerAction = {},
|
||||
updateVibrations = {},
|
||||
cachePercentage = cacheSize,
|
||||
clearCache = { cacheSize = 0f }
|
||||
clearCache = { cacheSize = 0f },
|
||||
backup = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewBackingUpScreen() {
|
||||
BackingUpScreen()
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.feature.settings
|
||||
|
||||
internal sealed class SettingsUIState {
|
||||
data object Loading : SettingsUIState()
|
||||
|
||||
data object BackupInProgress : SettingsUIState()
|
||||
|
||||
data class Ready(
|
||||
val enableVibrations: Boolean,
|
||||
val cacheSize: Int,
|
||||
) : SettingsUIState()
|
||||
}
|
@ -18,14 +18,22 @@
|
||||
|
||||
package com.sadellie.unitto.feature.settings
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.sadellie.unitto.data.backup.BackupManager
|
||||
import com.sadellie.unitto.data.common.stateIn
|
||||
import com.sadellie.unitto.data.database.CurrencyRatesDao
|
||||
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -33,15 +41,61 @@ import javax.inject.Inject
|
||||
internal class SettingsViewModel @Inject constructor(
|
||||
private val userPrefsRepository: UserPreferencesRepository,
|
||||
private val currencyRatesDao: CurrencyRatesDao,
|
||||
private val backupManager: BackupManager
|
||||
) : ViewModel() {
|
||||
val userPrefs = userPrefsRepository.generalPrefs
|
||||
.stateIn(viewModelScope, null)
|
||||
private val _backupFileUri = MutableSharedFlow<Uri?>()
|
||||
val backupFileUri = _backupFileUri.asSharedFlow()
|
||||
|
||||
val cachePercentage = currencyRatesDao.size()
|
||||
.map {
|
||||
(it / 100_000f).coerceIn(0f, 1f)
|
||||
private val _showErrorToast = MutableSharedFlow<Boolean>()
|
||||
val showErrorToast = _showErrorToast.asSharedFlow()
|
||||
|
||||
private val _operation = MutableStateFlow(false)
|
||||
private var backupJob: Job? = null
|
||||
|
||||
val uiState = combine(
|
||||
userPrefsRepository.generalPrefs,
|
||||
currencyRatesDao.size(),
|
||||
_operation,
|
||||
) { prefs, cacheSize, operation ->
|
||||
if (operation) return@combine SettingsUIState.BackupInProgress
|
||||
|
||||
SettingsUIState.Ready(
|
||||
enableVibrations = prefs.enableVibrations,
|
||||
cacheSize = cacheSize,
|
||||
)
|
||||
}
|
||||
.stateIn(viewModelScope, SettingsUIState.Loading)
|
||||
|
||||
fun backup() {
|
||||
backupJob?.cancel()
|
||||
backupJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
_operation.update { true }
|
||||
try {
|
||||
val backupFileUri = backupManager.backup()
|
||||
_backupFileUri.emit(backupFileUri) // Emit to trigger file share intent
|
||||
_showErrorToast.emit(false)
|
||||
} catch (e: Exception) {
|
||||
_showErrorToast.emit(true)
|
||||
Log.e(TAG, "$e")
|
||||
}
|
||||
_operation.update { false }
|
||||
}
|
||||
}
|
||||
|
||||
fun restore(uri: Uri) {
|
||||
backupJob?.cancel()
|
||||
backupJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
_operation.update { true }
|
||||
try {
|
||||
backupManager.restore(uri)
|
||||
_showErrorToast.emit(false)
|
||||
} catch (e: Exception) {
|
||||
_showErrorToast.emit(true)
|
||||
Log.e(TAG, "$e")
|
||||
}
|
||||
_operation.update { false }
|
||||
}
|
||||
}
|
||||
.stateIn(viewModelScope, 0f)
|
||||
|
||||
/**
|
||||
* @see UserPreferencesRepository.updateVibrations
|
||||
@ -54,3 +108,5 @@ internal class SettingsViewModel @Inject constructor(
|
||||
currencyRatesDao.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "SettingsViewModel"
|
@ -34,3 +34,4 @@ include(":data:model")
|
||||
include(":data:common")
|
||||
include(":data:evaluatto")
|
||||
include(":data:timezone")
|
||||
include(":data:backup")
|
||||
|
Loading…
x
Reference in New Issue
Block a user