Reorder settings
This commit is contained in:
parent
392c0efc83
commit
115c3e2b6e
17 changed files with 136 additions and 163 deletions
|
|
@ -1,11 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Required to check which pojavlauncher is installed -->
|
|
||||||
<queries>
|
|
||||||
<package android:name="net.kdt.pojavlaunch" />
|
|
||||||
<package android:name="net.kdt.pojavlaunch.debug" />
|
|
||||||
</queries>
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import eu.m724.pojavbackup.core.data.GameDataRepository
|
||||||
import eu.m724.pojavbackup.core.data.World
|
import eu.m724.pojavbackup.core.data.World
|
||||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
||||||
import eu.m724.pojavbackup.notification.NotificationChannels
|
import eu.m724.pojavbackup.notification.NotificationChannels
|
||||||
|
import eu.m724.pojavbackup.proto.BackupSource
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -55,8 +57,13 @@ class BackupWorker @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doBackup(backup: Backup) {
|
private suspend fun doBackup(backup: Backup) {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.settingsFlow.first()
|
||||||
val worldIds = settingsRepository.getIncludedWorldIds(settings.worldOrder)
|
val sources = settings.sourcesList
|
||||||
|
|
||||||
|
// TODO other source types support
|
||||||
|
val worldIds = sources
|
||||||
|
.filter { it.type == BackupSource.SourceType.WORLD }
|
||||||
|
.map { it.id }
|
||||||
|
|
||||||
// TODO multiple destinations support
|
// TODO multiple destinations support
|
||||||
val destinationUri = settings.destinationsList.first().uri.toUri()
|
val destinationUri = settings.destinationsList.first().uri.toUri()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package eu.m724.pojavbackup.core.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -12,6 +13,7 @@ import eu.m724.pojavbackup.core.data.World.Companion.getWorldFromDirectory
|
||||||
import eu.m724.pojavbackup.core.data.World.InvalidWorldException
|
import eu.m724.pojavbackup.core.data.World.InvalidWorldException
|
||||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -27,8 +29,9 @@ object GameDataModule {
|
||||||
settingsRepository: SettingsRepository
|
settingsRepository: SettingsRepository
|
||||||
): GameDataRepository {
|
): GameDataRepository {
|
||||||
return object : GameDataRepository {
|
return object : GameDataRepository {
|
||||||
|
|
||||||
override suspend fun listAllWorlds(): Flow<World> {
|
override suspend fun listAllWorlds(): Flow<World> {
|
||||||
val sourceUri = settingsRepository.getSource()!!
|
val sourceUri = settingsRepository.launcherDocumentUriFlow.first().toUri()
|
||||||
val documentFile = DocumentFile.fromTreeUri(context, sourceUri)!!.findFile(".minecraft")!!.findFile("saves")!!
|
val documentFile = DocumentFile.fromTreeUri(context, sourceUri)!!.findFile(".minecraft")!!.findFile("saves")!!
|
||||||
|
|
||||||
return flow {
|
return flow {
|
||||||
|
|
@ -43,7 +46,7 @@ object GameDataModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getWorld(id: String): World {
|
override suspend fun getWorld(id: String): World {
|
||||||
val sourceUri = settingsRepository.getSource()!!
|
val sourceUri = settingsRepository.launcherDocumentUriFlow.first().toUri()
|
||||||
val documentFile = DocumentFile.fromTreeUri(context, sourceUri)!!.findFile(".minecraft")!!.findFile("saves")!! // TODO maybe we could cache this
|
val documentFile = DocumentFile.fromTreeUri(context, sourceUri)!!.findFile(".minecraft")!!.findFile("saves")!! // TODO maybe we could cache this
|
||||||
|
|
||||||
val worldFile = documentFile.findFile(id) ?: throw NoSuchFileException(File(id))
|
val worldFile = documentFile.findFile(id) ?: throw NoSuchFileException(File(id))
|
||||||
|
|
|
||||||
|
|
@ -24,20 +24,12 @@ data class World(
|
||||||
|
|
||||||
val documentFile: DocumentFile?
|
val documentFile: DocumentFile?
|
||||||
) {
|
) {
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other !is World) return false
|
|
||||||
|
|
||||||
// TODO compare only id or other stuff too?
|
|
||||||
return this.id == other.id
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val SEPARATOR = dummy("")
|
|
||||||
|
|
||||||
fun dummy(id: String, label: String = ""): World {
|
fun dummy(id: String, label: String = ""): World {
|
||||||
return World(id, label, Instant.EPOCH.atZone(ZoneOffset.UTC), null, null)
|
return World(id, label, Instant.EPOCH.atZone(ZoneOffset.UTC), null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO do below belong here?
|
// TODO do below belong here?
|
||||||
|
|
||||||
private val NBT = Nbt {
|
private val NBT = Nbt {
|
||||||
|
|
@ -82,4 +74,20 @@ data class World(
|
||||||
class InvalidWorldException(
|
class InvalidWorldException(
|
||||||
override val message: String?
|
override val message: String?
|
||||||
) : Exception()
|
) : Exception()
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is World) return false
|
||||||
|
|
||||||
|
// TODO compare only id or other stuff too?
|
||||||
|
return this.id == other.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + displayName.hashCode()
|
||||||
|
result = 31 * result + lastPlayed.hashCode()
|
||||||
|
result = 31 * result + (icon?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (documentFile?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
package eu.m724.pojavbackup.core.datastore
|
package eu.m724.pojavbackup.core.datastore
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import eu.m724.pojavbackup.proto.BackupDestination
|
import eu.m724.pojavbackup.proto.BackupDestination
|
||||||
|
import eu.m724.pojavbackup.proto.BackupSource
|
||||||
import eu.m724.pojavbackup.proto.Settings
|
import eu.m724.pojavbackup.proto.Settings
|
||||||
import eu.m724.pojavbackup.proto.WorldOrder
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -15,68 +13,43 @@ import javax.inject.Singleton
|
||||||
class SettingsRepository @Inject constructor(
|
class SettingsRepository @Inject constructor(
|
||||||
private val dataStore: DataStore<Settings>
|
private val dataStore: DataStore<Settings>
|
||||||
) {
|
) {
|
||||||
fun getSettingsFlow(): Flow<Settings> {
|
val settingsFlow = dataStore.data
|
||||||
return dataStore.data
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getSettings(): Settings {
|
/* Getters */
|
||||||
return dataStore.data.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Worlds */
|
val backupSourcesFlow: Flow<List<BackupSource>> =
|
||||||
|
settingsFlow.map { it.sourcesList }
|
||||||
|
|
||||||
suspend fun getIncludedWorldIds(): List<String> {
|
val backupDestinationsFlow: Flow<List<BackupDestination>> =
|
||||||
return getIncludedWorldIds(getSettings().worldOrder)
|
settingsFlow.map { it.destinationsList }
|
||||||
}
|
|
||||||
|
|
||||||
fun getIncludedWorldIds(worldOrder: WorldOrder): List<String> {
|
val launcherDocumentUriFlow: Flow<String> =
|
||||||
return worldOrder.worldIdsList.subList(0, worldOrder.separatorIndex)
|
settingsFlow.map { it.launcherDocumentUri }
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateWorldOrder(worldOrder: WorldOrder) {
|
/* Setters */
|
||||||
|
|
||||||
|
suspend fun updateBackupDestinations(backupDestinations: (List<BackupDestination>) -> List<BackupDestination>) {
|
||||||
dataStore.updateData {
|
dataStore.updateData {
|
||||||
it.toBuilder()
|
it.toBuilder()
|
||||||
.clearWorldOrder()
|
.clearDestinations()
|
||||||
.setWorldOrder(worldOrder)
|
.addAllDestinations(backupDestinations(it.destinationsList))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Destination */
|
suspend fun updateBackupSources(backupSources: (List<BackupSource>) -> List<BackupSource>) {
|
||||||
|
|
||||||
suspend fun getDestinations(): List<BackupDestination> {
|
|
||||||
return getSettings().destinationsList
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun addDestination(destination: BackupDestination) {
|
|
||||||
dataStore.updateData {
|
dataStore.updateData {
|
||||||
it.toBuilder()
|
it.toBuilder()
|
||||||
.addDestinations(destination)
|
.clearSources()
|
||||||
|
.addAllSources(backupSources(it.sourcesList))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeDestination(index: Int) {
|
suspend fun updateLauncherDocumentUri(launcherDocumentUri: (String) -> String) {
|
||||||
dataStore.updateData {
|
dataStore.updateData {
|
||||||
it.toBuilder()
|
it.toBuilder()
|
||||||
.removeDestinations(index)
|
.setLauncherDocumentUri(launcherDocumentUri(it.launcherDocumentUri))
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Source / launcher */
|
|
||||||
|
|
||||||
suspend fun getSource(): Uri? {
|
|
||||||
return getSettings().sourceUri.let {
|
|
||||||
if (it.isEmpty()) null
|
|
||||||
else it.toUri()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setSource(source: Uri) {
|
|
||||||
dataStore.updateData {
|
|
||||||
it.toBuilder()
|
|
||||||
.setSourceUri(source.toString())
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,19 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.m724.pojavbackup.core.data.GameDataRepository
|
|
||||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
||||||
import eu.m724.pojavbackup.notification.NotificationChannels
|
import eu.m724.pojavbackup.notification.NotificationChannels
|
||||||
import eu.m724.pojavbackup.setup.SetupActivity
|
import eu.m724.pojavbackup.setup.SetupActivity
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -24,8 +25,7 @@ import javax.inject.Inject
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject constructor(
|
class HomeViewModel @Inject constructor(
|
||||||
@ApplicationContext private val appContext: Context,
|
@ApplicationContext private val appContext: Context,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository
|
||||||
private val gameDataRepository: GameDataRepository
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val TAG = javaClass.name
|
private val TAG = javaClass.name
|
||||||
|
|
||||||
|
|
@ -45,9 +45,11 @@ class HomeViewModel @Inject constructor(
|
||||||
onSetupRequired: (Intent) -> Unit
|
onSetupRequired: (Intent) -> Unit
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val uri = settingsRepository.getSource()
|
val uri = settingsRepository.launcherDocumentUriFlow.first()
|
||||||
|
|
||||||
val storagePermission = uri?.let { checkForStoragePermission(uri) } == true
|
val storagePermission = if (uri.isNotEmpty()) {
|
||||||
|
checkForStoragePermission(uri.toUri())
|
||||||
|
} else false
|
||||||
|
|
||||||
if (!storagePermission || (!notificationPermissionGranted && !notificationPermissionRejected)) {
|
if (!storagePermission || (!notificationPermissionGranted && !notificationPermissionRejected)) {
|
||||||
val intent = Intent(appContext, SetupActivity::class.java)
|
val intent = Intent(appContext, SetupActivity::class.java)
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ fun DashboardScreen(
|
||||||
) {
|
) {
|
||||||
DashboardCard(
|
DashboardCard(
|
||||||
title = stringResource(R.string.dashboard_card_worlds),
|
title = stringResource(R.string.dashboard_card_worlds),
|
||||||
value = settings.worldOrder.separatorIndex,
|
value = settings.sourcesList.size,
|
||||||
iconResourceId = R.drawable.baseline_mosque_24,
|
iconResourceId = R.drawable.baseline_mosque_24,
|
||||||
onClick = onWorldsIncludedClick
|
onClick = onWorldsIncludedClick
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class DashboardScreenViewModel @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepository.getSettingsFlow().collect { newSettings ->
|
settingsRepository.settingsFlow.collect { newSettings ->
|
||||||
_settings.update { newSettings }
|
_settings.update { newSettings }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class SettingsViewModel @Inject constructor(
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepository.addDestination(destination)
|
settingsRepository.updateBackupDestinations { it + destination }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
|
||||||
import eu.m724.pojavbackup.R
|
import eu.m724.pojavbackup.R
|
||||||
import org.burnoutcrew.reorderable.ReorderableItem
|
import org.burnoutcrew.reorderable.ReorderableItem
|
||||||
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
|
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
|
||||||
|
|
@ -45,9 +44,7 @@ import java.time.format.FormatStyle
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContentScreen(
|
fun ContentScreen() {
|
||||||
navController: NavController,
|
|
||||||
) {
|
|
||||||
val viewModel: ContentScreenViewModel = hiltViewModel()
|
val viewModel: ContentScreenViewModel = hiltViewModel()
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
@ -59,7 +56,7 @@ fun ContentScreen(
|
||||||
if (uiState.loading) {
|
if (uiState.loading) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
} else {
|
} else {
|
||||||
if (uiState.worlds.size <= 1) { // separator
|
if (uiState.worldList.size <= 1) { // separator
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.worlds_none),
|
text = stringResource(R.string.worlds_none),
|
||||||
modifier = Modifier.padding(top = 50.dp)
|
modifier = Modifier.padding(top = 50.dp)
|
||||||
|
|
@ -77,11 +74,11 @@ fun ContentScreen(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = uiState.worlds,
|
items = uiState.worldList,
|
||||||
key = { it.id }
|
key = { it?.id ?: "" }
|
||||||
) { world ->
|
) { world ->
|
||||||
ReorderableItem(state, key = world.id) { isDragging ->
|
ReorderableItem(state, key = world?.id ?: "" ) { isDragging ->
|
||||||
if (!world.id.isEmpty()) {
|
if (world != null) {
|
||||||
WorldInfoCard(
|
WorldInfoCard(
|
||||||
bitmap = world.icon,
|
bitmap = world.icon,
|
||||||
id = world.id,
|
id = world.id,
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,5 @@ import eu.m724.pojavbackup.core.data.World
|
||||||
|
|
||||||
data class ContentScreenUiState(
|
data class ContentScreenUiState(
|
||||||
val loading: Boolean = true,
|
val loading: Boolean = true,
|
||||||
val worlds: List<World> = emptyList()
|
val worldList: List<World?> = emptyList() // null is separator
|
||||||
)
|
)
|
||||||
|
|
@ -1,25 +1,23 @@
|
||||||
package eu.m724.pojavbackup.settings.screen.content
|
package eu.m724.pojavbackup.settings.screen.content
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import eu.m724.pojavbackup.core.data.GameDataRepository
|
import eu.m724.pojavbackup.core.data.GameDataRepository
|
||||||
import eu.m724.pojavbackup.core.data.World
|
import eu.m724.pojavbackup.core.data.World
|
||||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
||||||
import eu.m724.pojavbackup.proto.WorldOrder
|
import eu.m724.pojavbackup.proto.BackupSource
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ContentScreenViewModel @Inject constructor(
|
class ContentScreenViewModel @Inject constructor(
|
||||||
@ApplicationContext private val appContext: Context,
|
|
||||||
private val gameDataRepository: GameDataRepository,
|
private val gameDataRepository: GameDataRepository,
|
||||||
private val settingsRepository: SettingsRepository
|
private val settingsRepository: SettingsRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
@ -28,26 +26,32 @@ class ContentScreenViewModel @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepository.getSettingsFlow().collect { settings ->
|
val savedWorlds = gameDataRepository.listAllWorlds().toList().associate { it.id to it }
|
||||||
val worlds = settings.worldOrder.worldIdsList.map {
|
|
||||||
try {
|
|
||||||
gameDataRepository.getWorld(it)
|
|
||||||
} catch (e: NoSuchFileException) {
|
|
||||||
World.dummy(it, "Deleted world") // TODO more clues that it's deleted
|
|
||||||
} catch (e: World.InvalidWorldException) {
|
|
||||||
World.dummy(it, "Corrupted world") // TODO do we want that
|
|
||||||
}
|
|
||||||
}.toMutableList()
|
|
||||||
worlds.add(settings.worldOrder.separatorIndex, World.SEPARATOR)
|
|
||||||
|
|
||||||
gameDataRepository.listAllWorlds().collect {
|
settingsRepository.backupSourcesFlow.collect { sources ->
|
||||||
if (!worlds.contains(it)) {
|
val worldStack = savedWorlds.toMutableMap()
|
||||||
worlds.add(it)
|
|
||||||
|
val backedUpWorlds = mutableListOf<World>()
|
||||||
|
|
||||||
|
// TODO support non-world sources
|
||||||
|
|
||||||
|
sources.forEach { source ->
|
||||||
|
if (source.type != BackupSource.SourceType.WORLD) return@forEach
|
||||||
|
|
||||||
|
val world = worldStack.remove(source.id)
|
||||||
|
if (world == null) {
|
||||||
|
backedUpWorlds.add(World.dummy(source.id, "Deleted world"))
|
||||||
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backedUpWorlds.add(world)
|
||||||
}
|
}
|
||||||
|
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(loading = false, worlds = worlds)
|
it.copy(
|
||||||
|
loading = false,
|
||||||
|
worldList = backedUpWorlds + null + worldStack.values
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +59,7 @@ class ContentScreenViewModel @Inject constructor(
|
||||||
|
|
||||||
fun moveWorld(fromIndex: Int, toIndex: Int) {
|
fun moveWorld(fromIndex: Int, toIndex: Int) {
|
||||||
// Similar to mutableStateOf, create a NEW list
|
// Similar to mutableStateOf, create a NEW list
|
||||||
val currentList = _uiState.value.worlds.toMutableList()
|
val currentList = _uiState.value.worldList.toMutableList()
|
||||||
// Check bounds for safety
|
// Check bounds for safety
|
||||||
if (fromIndex in currentList.indices && toIndex >= 0 && toIndex <= currentList.size) {
|
if (fromIndex in currentList.indices && toIndex >= 0 && toIndex <= currentList.size) {
|
||||||
val item = currentList.removeAt(fromIndex)
|
val item = currentList.removeAt(fromIndex)
|
||||||
|
|
@ -67,18 +71,31 @@ class ContentScreenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateWorldOrder(newList: List<World>) {
|
private fun updateWorldOrder(newList: List<World?>) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(worlds = newList)
|
it.copy(
|
||||||
|
worldList = newList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupSources = mutableListOf<BackupSource>()
|
||||||
|
|
||||||
|
val iterator = newList.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val world = iterator.next() ?: break // remember the separator is null
|
||||||
|
|
||||||
|
val backupSource = BackupSource.newBuilder()
|
||||||
|
.setId(world.id)
|
||||||
|
.setType(BackupSource.SourceType.WORLD)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
backupSources.add(backupSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepository.updateWorldOrder(
|
settingsRepository.updateBackupSources(
|
||||||
WorldOrder.newBuilder()
|
backupSources = { backupSources }
|
||||||
.addAllWorldIds(newList.filter { it != World.SEPARATOR }.map { it.id })
|
|
||||||
.setSeparatorIndex(newList.indexOf(World.SEPARATOR))
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,14 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
|
||||||
import eu.m724.pojavbackup.R
|
import eu.m724.pojavbackup.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DestinationScreen(
|
fun DestinationScreen(
|
||||||
navController: NavController,
|
|
||||||
onAddDestination: () -> Unit
|
onAddDestination: () -> Unit
|
||||||
) {
|
) {
|
||||||
val viewModel: DestinationScreenViewModel = hiltViewModel()
|
val viewModel: DestinationScreenViewModel = hiltViewModel()
|
||||||
val destinations by viewModel.destinations.collectAsStateWithLifecycle()
|
val destinations by viewModel.destinationsFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Column(
|
Column(
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
package eu.m724.pojavbackup.settings.screen.destination
|
package eu.m724.pojavbackup.settings.screen.destination
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
||||||
import eu.m724.pojavbackup.proto.BackupDestination
|
import eu.m724.pojavbackup.proto.BackupDestination
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -16,18 +14,15 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class DestinationScreenViewModel @Inject constructor(
|
class DestinationScreenViewModel @Inject constructor(
|
||||||
@ApplicationContext private val appContext: Context,
|
|
||||||
private val settingsRepository: SettingsRepository
|
private val settingsRepository: SettingsRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _destinations = MutableStateFlow<List<BackupDestination>>(emptyList())
|
private val _destinationsFlow = MutableStateFlow<List<BackupDestination>>(emptyList())
|
||||||
val destinations: StateFlow<List<BackupDestination>> = _destinations.asStateFlow()
|
val destinationsFlow: StateFlow<List<BackupDestination>> = _destinationsFlow.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepository.getSettingsFlow().collect { settings ->
|
settingsRepository.backupDestinationsFlow.collect { destinations ->
|
||||||
_destinations.update {
|
_destinationsFlow.update { destinations }
|
||||||
settings.destinationsList
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class SetupActivity : ComponentActivity() {
|
||||||
if (!viewModel.checkIfCanProceed()) return
|
if (!viewModel.checkIfCanProceed()) return
|
||||||
|
|
||||||
startActivity(Intent(applicationContext, HomeActivity::class.java))
|
startActivity(Intent(applicationContext, HomeActivity::class.java))
|
||||||
finishActivity(0)
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package eu.m724.pojavbackup.setup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
|
@ -25,11 +24,6 @@ class SetupViewModel @Inject constructor(
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val TAG: String = javaClass.name
|
private val TAG: String = javaClass.name
|
||||||
|
|
||||||
private val POJAV_PACKAGES = arrayOf(
|
|
||||||
"net.kdt.pojavlaunch",
|
|
||||||
"net.kdt.pojavlaunch.debug"
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(SetupUiState())
|
private val _uiState = MutableStateFlow(SetupUiState())
|
||||||
val uiState: StateFlow<SetupUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<SetupUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -67,7 +61,7 @@ class SetupViewModel @Inject constructor(
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
settingsRepository.setSource(uri)
|
settingsRepository.updateLauncherDocumentUri { uri.toString() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,17 +110,4 @@ class SetupViewModel @Inject constructor(
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun detectInstalledLauncherPackage(): List<String> {
|
|
||||||
val packageManager = appContext.packageManager
|
|
||||||
|
|
||||||
return POJAV_PACKAGES.filter {
|
|
||||||
try {
|
|
||||||
packageManager.getPackageInfo(it, 0)
|
|
||||||
true
|
|
||||||
} catch (e: NameNotFoundException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,32 +3,29 @@ syntax = "proto3";
|
||||||
option java_package = "eu.m724.pojavbackup.proto";
|
option java_package = "eu.m724.pojavbackup.proto";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
|
|
||||||
message WorldOrder {
|
message BackupSource {
|
||||||
repeated string worldIds = 1;
|
enum SourceType {
|
||||||
int32 separatorIndex = 2;
|
WORLD = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceType type = 1;
|
||||||
|
string id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BackupDestination {
|
message BackupDestination {
|
||||||
|
enum DestinationType {
|
||||||
|
EXTERNAL = 0; // currently the only one because we use DocumentFile
|
||||||
|
}
|
||||||
|
|
||||||
string label = 1;
|
string label = 1;
|
||||||
string uri = 2; // TODO
|
DestinationType type = 2;
|
||||||
|
string uri = 3; // this should be changed after adding more types
|
||||||
}
|
}
|
||||||
|
|
||||||
message Settings {
|
message Settings {
|
||||||
WorldOrder worldOrder = 1;
|
string launcherDocumentUri = 1;
|
||||||
repeated string extraPaths = 2;
|
|
||||||
|
repeated BackupSource sources = 2;
|
||||||
|
|
||||||
repeated BackupDestination destinations = 3;
|
repeated BackupDestination destinations = 3;
|
||||||
|
|
||||||
string sourceUri = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
message BackupMeta {
|
|
||||||
string id = 1;
|
|
||||||
int64 timestamp = 2;
|
|
||||||
int32 status = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message BackupsMeta {
|
|
||||||
repeated BackupMeta backups = 1;
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue