Add color scheme backup / restore

This commit is contained in:
MM20 2023-08-26 18:52:35 +02:00
parent f223e9307d
commit ff7a72f7c4
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
9 changed files with 75 additions and 8 deletions

View File

@ -162,6 +162,7 @@ fun RestoreBackupSheet(
BackupComponent.SearchActions -> Icons.Rounded.ArrowOutward BackupComponent.SearchActions -> Icons.Rounded.ArrowOutward
BackupComponent.Widgets -> Icons.Rounded.Widgets BackupComponent.Widgets -> Icons.Rounded.Widgets
BackupComponent.Customizations -> Icons.Rounded.Edit BackupComponent.Customizations -> Icons.Rounded.Edit
BackupComponent.Themes -> Icons.Rounded.Palette
}, },
contentDescription = null contentDescription = null
) )
@ -173,6 +174,7 @@ fun RestoreBackupSheet(
BackupComponent.SearchActions -> R.string.backup_component_searchactions BackupComponent.SearchActions -> R.string.backup_component_searchactions
BackupComponent.Widgets -> R.string.backup_component_widgets BackupComponent.Widgets -> R.string.backup_component_widgets
BackupComponent.Customizations -> R.string.backup_component_customizations BackupComponent.Customizations -> R.string.backup_component_customizations
BackupComponent.Themes -> R.string.backup_component_themes
} }
), ),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,

View File

@ -133,6 +133,14 @@ fun CreateBackupSheet(
viewModel.toggleComponent(BackupComponent.SearchActions) viewModel.toggleComponent(BackupComponent.SearchActions)
} }
) )
BackupableComponent(
title = stringResource(R.string.backup_component_themes),
icon = Icons.Rounded.Palette,
checked = components.contains(BackupComponent.Themes),
onCheckedChange = {
viewModel.toggleComponent(BackupComponent.Themes)
}
)
SmallMessage( SmallMessage(
modifier = Modifier modifier = Modifier
.padding(top = 8.dp) .padding(top = 8.dp)

View File

@ -24,4 +24,10 @@ interface ThemeDao {
@Query("DELETE FROM Theme WHERE id = :id") @Query("DELETE FROM Theme WHERE id = :id")
suspend fun delete(id: UUID) suspend fun delete(id: UUID)
@Query("DELETE FROM Theme")
suspend fun deleteAll()
@Insert
fun insertAll(themes: List<ThemeEntity>)
} }

View File

@ -694,6 +694,7 @@
<string name="backup_component_settings">Settings</string> <string name="backup_component_settings">Settings</string>
<string name="backup_component_websearches">Web search shortcuts</string> <string name="backup_component_websearches">Web search shortcuts</string>
<string name="backup_component_searchactions">Quick actions</string> <string name="backup_component_searchactions">Quick actions</string>
<string name="backup_component_themes">Color schemes</string>
<string name="backup_component_widgets">Built-in widgets</string> <string name="backup_component_widgets">Built-in widgets</string>
<string name="backup_component_customizations">Customizations</string> <string name="backup_component_customizations">Customizations</string>
<string name="backup_complete">The backup has been completed.</string> <string name="backup_complete">The backup has been completed.</string>

View File

@ -1,18 +1,22 @@
package de.mm20.launcher2.themes package de.mm20.launcher2.themes
import android.content.Context import android.content.Context
import android.util.Log import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import java.io.File
import java.lang.IllegalArgumentException
import java.util.UUID import java.util.UUID
class ThemeRepository( class ThemeRepository(
@ -85,4 +89,38 @@ class ThemeRepository(
} }
} }
suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.themeDao()
val themes = dao.getAll().first().map { Theme(it) }
val data = ThemeJson.encodeToString(themes)
val file = File(toDir, "themes.0000")
file.bufferedWriter().use {
it.write(data)
}
}
suspend fun import(fromDir: File) = withContext(Dispatchers.IO) {
val dao = database.themeDao()
dao.deleteAll()
val files =
fromDir.listFiles { _, name -> name.startsWith("themes.") }
?: return@withContext
for (file in files) {
val data = file.inputStream().reader().readText()
val themes: List<Theme> = try {
ThemeJson.decodeFromString(data)
} catch (e: SerializationException) {
CrashReporter.logException(e)
continue
} catch (e: IllegalArgumentException) {
CrashReporter.logException(e)
continue
}
dao.insertAll(themes.map { it.toEntity() })
}
}
} }

View File

@ -46,5 +46,6 @@ dependencies {
implementation(project(":core:preferences")) implementation(project(":core:preferences"))
implementation(project(":core:ktx")) implementation(project(":core:ktx"))
implementation(project(":data:customattrs")) implementation(project(":data:customattrs"))
implementation(project(":data:themes"))
} }

View File

@ -5,11 +5,12 @@ enum class BackupComponent(val value: String) {
Favorites("favorites"), Favorites("favorites"),
Widgets("widgets2"), Widgets("widgets2"),
Customizations("customizations"), Customizations("customizations"),
SearchActions("searchactions"); SearchActions("searchactions"),
Themes("themes");
companion object { companion object {
fun fromValue(value: String): BackupComponent? { fun fromValue(value: String): BackupComponent? {
return values().firstOrNull { it.value == value } return entries.firstOrNull { it.value == value }
} }
} }
} }

View File

@ -9,6 +9,7 @@ import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.export import de.mm20.launcher2.preferences.export
import de.mm20.launcher2.preferences.import import de.mm20.launcher2.preferences.import
import de.mm20.launcher2.searchactions.SearchActionRepository import de.mm20.launcher2.searchactions.SearchActionRepository
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.widgets.WidgetRepository import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
@ -25,6 +26,7 @@ class BackupManager(
private val widgetRepository: WidgetRepository, private val widgetRepository: WidgetRepository,
private val searchActionRepository: SearchActionRepository, private val searchActionRepository: SearchActionRepository,
private val customAttrsRepository: CustomAttributesRepository, private val customAttrsRepository: CustomAttributesRepository,
private val themesRepository: ThemeRepository,
) { ) {
private val scope = CoroutineScope(Dispatchers.Default + Job()) private val scope = CoroutineScope(Dispatchers.Default + Job())
@ -34,7 +36,7 @@ class BackupManager(
*/ */
suspend fun backup( suspend fun backup(
uri: Uri, uri: Uri,
include: Set<BackupComponent> = BackupComponent.values().toSet() include: Set<BackupComponent> = BackupComponent.entries.toSet()
) { ) {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
@ -78,6 +80,10 @@ class BackupManager(
customAttrsRepository.export(backupDir) customAttrsRepository.export(backupDir)
} }
if (include.contains(BackupComponent.Themes)) {
themesRepository.export(backupDir)
}
createArchive(backupDir, outputStream) createArchive(backupDir, outputStream)
outputStream.close() outputStream.close()
@ -118,6 +124,10 @@ class BackupManager(
if (include.contains(BackupComponent.Customizations)) { if (include.contains(BackupComponent.Customizations)) {
customAttrsRepository.import(restoreDir) customAttrsRepository.import(restoreDir)
} }
if (include.contains(BackupComponent.Themes)) {
themesRepository.import(restoreDir)
}
} }
} }
job.join() job.join()
@ -188,7 +198,7 @@ class BackupManager(
*/ */
private const val BackupFormatMajor = 1 private const val BackupFormatMajor = 1
private const val BackupFormatMinor = 6 private const val BackupFormatMinor = 7
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor" internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
} }
} }

View File

@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val backupModule = module { val backupModule = module {
single { BackupManager(androidContext(), get(), get(), get(), get(), get()) } single { BackupManager(androidContext(), get(), get(), get(), get(), get(), get()) }
} }