Update
This commit is contained in:
		
					parent
					
						
							
								068d672eee
							
						
					
				
			
			
				commit
				
					
						34af1f025f
					
				
			
		
					 14 changed files with 322 additions and 91 deletions
				
			
		| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
package eu.m724.pojavbackup.core.datastore
 | 
			
		||||
 | 
			
		||||
import androidx.datastore.core.DataStore
 | 
			
		||||
import eu.m724.pojavbackup.proto.BackupDestination
 | 
			
		||||
import eu.m724.pojavbackup.proto.Settings
 | 
			
		||||
import eu.m724.pojavbackup.proto.WorldOrder
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
| 
						 | 
				
			
			@ -28,4 +29,26 @@ class SettingsRepository @Inject constructor(
 | 
			
		|||
                .build()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Destination */
 | 
			
		||||
 | 
			
		||||
    suspend fun getDestinations(): List<BackupDestination> {
 | 
			
		||||
        return getSettings().destinationsList
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun addDestination(destination: BackupDestination) {
 | 
			
		||||
        dataStore.updateData {
 | 
			
		||||
            it.toBuilder()
 | 
			
		||||
                .addDestinations(destination)
 | 
			
		||||
                .build()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun removeDestination(index: Int) {
 | 
			
		||||
        dataStore.updateData {
 | 
			
		||||
            it.toBuilder()
 | 
			
		||||
                .removeDestinations(index)
 | 
			
		||||
                .build()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,15 +5,14 @@ import android.os.Bundle
 | 
			
		|||
import androidx.activity.ComponentActivity
 | 
			
		||||
import androidx.activity.compose.setContent
 | 
			
		||||
import androidx.activity.enableEdgeToEdge
 | 
			
		||||
import androidx.activity.result.ActivityResult
 | 
			
		||||
import androidx.activity.result.contract.ActivityResultContracts
 | 
			
		||||
import androidx.activity.viewModels
 | 
			
		||||
import androidx.compose.animation.core.tween
 | 
			
		||||
import androidx.compose.animation.fadeIn
 | 
			
		||||
import androidx.compose.animation.fadeOut
 | 
			
		||||
import androidx.compose.animation.slideInHorizontally
 | 
			
		||||
import androidx.compose.animation.slideOutHorizontally
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.RowScope
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +28,6 @@ import androidx.compose.runtime.getValue
 | 
			
		|||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.res.painterResource
 | 
			
		||||
import androidx.core.net.toUri
 | 
			
		||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
 | 
			
		||||
import androidx.navigation.NavController
 | 
			
		||||
import androidx.navigation.NavDestination.Companion.hierarchy
 | 
			
		||||
| 
						 | 
				
			
			@ -50,18 +48,20 @@ import eu.m724.pojavbackup.ui.theme.PojavBackupTheme
 | 
			
		|||
class HomeActivity : ComponentActivity() {
 | 
			
		||||
    private val viewModel: HomeViewModel by viewModels()
 | 
			
		||||
 | 
			
		||||
    private val setupResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
 | 
			
		||||
        if (result.resultCode == 0) {
 | 
			
		||||
            // TODO success
 | 
			
		||||
        } else {
 | 
			
		||||
            // TODO failure
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        viewModel.load(
 | 
			
		||||
            onSetupNeeded = {
 | 
			
		||||
                registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
 | 
			
		||||
                    if (result.resultCode == 0) {
 | 
			
		||||
                        // TODO success
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // TODO failure
 | 
			
		||||
                    }
 | 
			
		||||
                }.launch(Intent(applicationContext, SetupActivity::class.java))
 | 
			
		||||
                setupResult.launch(Intent(applicationContext, SetupActivity::class.java))
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -122,10 +122,14 @@ fun HomeScaffold(
 | 
			
		|||
            startDestination = HomeScreen.Dashboard,
 | 
			
		||||
            modifier = Modifier.padding(innerPadding),
 | 
			
		||||
            enterTransition = {
 | 
			
		||||
                fadeIn() + slideInHorizontally(initialOffsetX = { it / 10 })
 | 
			
		||||
                fadeIn(
 | 
			
		||||
                    animationSpec = tween(100)
 | 
			
		||||
                ) + slideInHorizontally(initialOffsetX = { it / 10 })
 | 
			
		||||
            },
 | 
			
		||||
            exitTransition = {
 | 
			
		||||
                fadeOut() + slideOutHorizontally(targetOffsetX = { -it / 10 })
 | 
			
		||||
                fadeOut(
 | 
			
		||||
                    animationSpec = tween(100)
 | 
			
		||||
                ) + slideOutHorizontally(targetOffsetX = { -it / 15 })
 | 
			
		||||
            }
 | 
			
		||||
        ) {
 | 
			
		||||
            composable<HomeScreen.Dashboard> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
package eu.m724.pojavbackup.home.screen.history
 | 
			
		||||
 | 
			
		||||
import android.system.Os.stat
 | 
			
		||||
import androidx.compose.foundation.Image
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
| 
						 | 
				
			
			@ -10,9 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		|||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.material3.Card
 | 
			
		||||
import androidx.compose.material3.CardDefaults
 | 
			
		||||
import androidx.compose.material3.CardElevation
 | 
			
		||||
import androidx.compose.material3.ElevatedCard
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
| 
						 | 
				
			
			@ -23,7 +20,6 @@ import androidx.compose.ui.Modifier
 | 
			
		|||
import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.layout.ContentScale
 | 
			
		||||
import androidx.compose.ui.res.painterResource
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.m724.pojavbackup.R
 | 
			
		||||
import eu.m724.pojavbackup.core.BackupStatus
 | 
			
		||||
| 
						 | 
				
			
			@ -31,7 +27,6 @@ import eu.m724.pojavbackup.home.screen.ScreenColumn
 | 
			
		|||
import java.time.ZonedDateTime
 | 
			
		||||
import java.time.format.DateTimeFormatter
 | 
			
		||||
import java.time.format.FormatStyle
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun HistoryScreen() {
 | 
			
		||||
| 
						 | 
				
			
			@ -101,6 +96,7 @@ fun BackupCard(
 | 
			
		|||
    ) {
 | 
			
		||||
        Row(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .fillMaxWidth()
 | 
			
		||||
                .padding(horizontal = 16.dp, vertical = 8.dp),
 | 
			
		||||
            verticalAlignment = Alignment.CenterVertically
 | 
			
		||||
        ) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,9 @@ import android.os.Bundle
 | 
			
		|||
import androidx.activity.ComponentActivity
 | 
			
		||||
import androidx.activity.compose.setContent
 | 
			
		||||
import androidx.activity.enableEdgeToEdge
 | 
			
		||||
import androidx.activity.result.contract.ActivityResultContracts
 | 
			
		||||
import androidx.activity.viewModels
 | 
			
		||||
import androidx.compose.animation.core.tween
 | 
			
		||||
import androidx.compose.animation.fadeIn
 | 
			
		||||
import androidx.compose.animation.fadeOut
 | 
			
		||||
import androidx.compose.animation.slideInHorizontally
 | 
			
		||||
| 
						 | 
				
			
			@ -11,11 +14,14 @@ import androidx.compose.animation.slideOutHorizontally
 | 
			
		|||
import androidx.compose.foundation.layout.RowScope
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.material3.ExperimentalMaterial3Api
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.IconButton
 | 
			
		||||
import androidx.compose.material3.NavigationBar
 | 
			
		||||
import androidx.compose.material3.NavigationBarItem
 | 
			
		||||
import androidx.compose.material3.Scaffold
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.material3.TopAppBar
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
| 
						 | 
				
			
			@ -30,17 +36,26 @@ import dagger.hilt.android.AndroidEntryPoint
 | 
			
		|||
import eu.m724.pojavbackup.R
 | 
			
		||||
import eu.m724.pojavbackup.settings.screen.SettingsScreen
 | 
			
		||||
import eu.m724.pojavbackup.settings.screen.content.ContentScreen
 | 
			
		||||
import eu.m724.pojavbackup.settings.screen.destination.DestinationScreen
 | 
			
		||||
import eu.m724.pojavbackup.settings.screen.options.OptionsScreen
 | 
			
		||||
import eu.m724.pojavbackup.ui.theme.PojavBackupTheme
 | 
			
		||||
 | 
			
		||||
@AndroidEntryPoint
 | 
			
		||||
class SettingsActivity : ComponentActivity() {
 | 
			
		||||
    private val viewModel: SettingsViewModel by viewModels()
 | 
			
		||||
 | 
			
		||||
    private val openDocumentTree = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
 | 
			
		||||
        viewModel.onOpenDocumentTree(it)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @OptIn(ExperimentalMaterial3Api::class)
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        val startPage = when (intent.getStringExtra("settingsPage")) {
 | 
			
		||||
            "options" -> SettingsScreen.Options
 | 
			
		||||
            "content" -> SettingsScreen.Content
 | 
			
		||||
            "destination" -> SettingsScreen.Destination
 | 
			
		||||
            else -> SettingsScreen.Options
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +66,25 @@ class SettingsActivity : ComponentActivity() {
 | 
			
		|||
            PojavBackupTheme {
 | 
			
		||||
                Scaffold(
 | 
			
		||||
                    modifier = Modifier.fillMaxSize(),
 | 
			
		||||
                    topBar = {
 | 
			
		||||
                        TopAppBar(
 | 
			
		||||
                            title = {
 | 
			
		||||
                                Text("Settings")
 | 
			
		||||
                            },
 | 
			
		||||
                            navigationIcon = {
 | 
			
		||||
                                IconButton(
 | 
			
		||||
                                    onClick = {
 | 
			
		||||
                                        finish()
 | 
			
		||||
                                    }
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    Icon(
 | 
			
		||||
                                        painter = painterResource(R.drawable.baseline_arrow_back_24),
 | 
			
		||||
                                        contentDescription = "Exit Settings"
 | 
			
		||||
                                    )
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        )
 | 
			
		||||
                    },
 | 
			
		||||
                    bottomBar = {
 | 
			
		||||
                        NavigationBar {
 | 
			
		||||
                            ScreenNavigationBarItem(
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +99,12 @@ class SettingsActivity : ComponentActivity() {
 | 
			
		|||
                                route = SettingsScreen.Content,
 | 
			
		||||
                                iconResourceId = R.drawable.baseline_folder_copy_24
 | 
			
		||||
                            )
 | 
			
		||||
                            ScreenNavigationBarItem(
 | 
			
		||||
                                navController = navController,
 | 
			
		||||
                                label = "Destination",
 | 
			
		||||
                                route = SettingsScreen.Destination,
 | 
			
		||||
                                iconResourceId = R.drawable.baseline_cloud_24
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ) { innerPadding ->
 | 
			
		||||
| 
						 | 
				
			
			@ -73,10 +113,14 @@ class SettingsActivity : ComponentActivity() {
 | 
			
		|||
                        startDestination = startPage,
 | 
			
		||||
                        modifier = Modifier.padding(innerPadding),
 | 
			
		||||
                        enterTransition = {
 | 
			
		||||
                            fadeIn() + slideInHorizontally(initialOffsetX = { it / 10 })
 | 
			
		||||
                            fadeIn(
 | 
			
		||||
                                animationSpec = tween(100)
 | 
			
		||||
                            ) + slideInHorizontally(initialOffsetX = { it / 10 })
 | 
			
		||||
                        },
 | 
			
		||||
                        exitTransition = {
 | 
			
		||||
                            fadeOut() + slideOutHorizontally(targetOffsetX = { -it / 10 })
 | 
			
		||||
                            fadeOut(
 | 
			
		||||
                                animationSpec = tween(100)
 | 
			
		||||
                            ) + slideOutHorizontally(targetOffsetX = { -it / 15 })
 | 
			
		||||
                        }
 | 
			
		||||
                    ) {
 | 
			
		||||
                        composable<SettingsScreen.Options> {
 | 
			
		||||
| 
						 | 
				
			
			@ -85,12 +129,19 @@ class SettingsActivity : ComponentActivity() {
 | 
			
		|||
                        composable<SettingsScreen.Content> {
 | 
			
		||||
                            ContentScreen(navController)
 | 
			
		||||
                        }
 | 
			
		||||
                        composable<SettingsScreen.Destination> {
 | 
			
		||||
                            DestinationScreen(navController, onAddDestination = { onAddDestination() })
 | 
			
		||||
                        }
 | 
			
		||||
                        // Add more destinations similarly.
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onAddDestination() {
 | 
			
		||||
        openDocumentTree.launch(null)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO those functions are reused in Home
 | 
			
		||||
| 
						 | 
				
			
			@ -107,6 +158,7 @@ fun RowScope.ScreenNavigationBarItem(
 | 
			
		|||
        selected = selected,
 | 
			
		||||
        onClick = {
 | 
			
		||||
            if (!selected) {
 | 
			
		||||
                navController.popBackStack() // this makes back exit settings
 | 
			
		||||
                navController.navigate(route)
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
package eu.m724.pojavbackup.settings
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.util.Log
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import dagger.hilt.android.lifecycle.HiltViewModel
 | 
			
		||||
import dagger.hilt.android.qualifiers.ApplicationContext
 | 
			
		||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
 | 
			
		||||
import eu.m724.pojavbackup.proto.BackupDestination
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
@HiltViewModel
 | 
			
		||||
class SettingsViewModel @Inject constructor(
 | 
			
		||||
    @ApplicationContext private val appContext: Context,
 | 
			
		||||
    private val settingsRepository: SettingsRepository
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
    private val TAG = javaClass.name
 | 
			
		||||
 | 
			
		||||
    fun onOpenDocumentTree(uri: Uri?) {
 | 
			
		||||
        if (uri != null) {
 | 
			
		||||
            Log.i(TAG, "Got URI: $uri")
 | 
			
		||||
 | 
			
		||||
            appContext.contentResolver.takePersistableUriPermission(
 | 
			
		||||
                uri,
 | 
			
		||||
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            val destination = BackupDestination.newBuilder()
 | 
			
		||||
                .setLabel("label")
 | 
			
		||||
                .setUri(uri.toString())
 | 
			
		||||
                .build()
 | 
			
		||||
 | 
			
		||||
            viewModelScope.launch {
 | 
			
		||||
                settingsRepository.addDestination(destination)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable
 | 
			
		|||
@Serializable sealed interface SettingsScreen {
 | 
			
		||||
    @Serializable data object Options : SettingsScreen
 | 
			
		||||
    @Serializable data object Content : SettingsScreen
 | 
			
		||||
    @Serializable data object Destination : SettingsScreen
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,13 +12,11 @@ import androidx.compose.foundation.layout.size
 | 
			
		|||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyColumn
 | 
			
		||||
import androidx.compose.foundation.lazy.items
 | 
			
		||||
import androidx.compose.material3.Card
 | 
			
		||||
import androidx.compose.material3.CardDefaults
 | 
			
		||||
import androidx.compose.material3.CardElevation
 | 
			
		||||
import androidx.compose.material3.ElevatedCard
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
| 
						 | 
				
			
			@ -55,41 +53,42 @@ fun ContentScreen(
 | 
			
		|||
 | 
			
		||||
    val worlds by viewModel.worlds.collectAsStateWithLifecycle()
 | 
			
		||||
 | 
			
		||||
    LaunchedEffect(Unit) {
 | 
			
		||||
        viewModel.listWorlds()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ScreenColumn {
 | 
			
		||||
        val state = rememberReorderableLazyListState(onMove = { from, to ->
 | 
			
		||||
            viewModel.moveWorld(from.index, to.index)
 | 
			
		||||
        })
 | 
			
		||||
        if (worlds.size <= 1) { // separator
 | 
			
		||||
            Text(
 | 
			
		||||
                text = "No worlds available!"
 | 
			
		||||
            )
 | 
			
		||||
        } else {
 | 
			
		||||
            val state = rememberReorderableLazyListState(onMove = { from, to ->
 | 
			
		||||
                viewModel.moveWorld(from.index, to.index)
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        LazyColumn(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .reorderable(state)
 | 
			
		||||
                .detectReorderAfterLongPress(state),
 | 
			
		||||
            state = state.listState,
 | 
			
		||||
            horizontalAlignment = Alignment.CenterHorizontally
 | 
			
		||||
        ) {
 | 
			
		||||
            items(
 | 
			
		||||
                items = worlds,
 | 
			
		||||
                key = { it.id }
 | 
			
		||||
            ) { world ->
 | 
			
		||||
                ReorderableItem(state, key = world.id) { isDragging ->
 | 
			
		||||
                    if (!world.id.isEmpty()) {
 | 
			
		||||
                        WorldInfoCard(
 | 
			
		||||
                            bitmap = world.icon,
 | 
			
		||||
                            id = world.id,
 | 
			
		||||
                            displayName = world.displayName,
 | 
			
		||||
                            lastPlayed = world.lastPlayed,
 | 
			
		||||
                            elevation = if (isDragging) CardDefaults.elevatedCardElevation() else CardDefaults.cardElevation()
 | 
			
		||||
                        )
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Text(
 | 
			
		||||
                            text = "↑ Worlds above this line will be backed up ↑",
 | 
			
		||||
                            modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
 | 
			
		||||
                            textAlign = TextAlign.Center
 | 
			
		||||
                        )
 | 
			
		||||
            LazyColumn(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .reorderable(state)
 | 
			
		||||
                    .detectReorderAfterLongPress(state),
 | 
			
		||||
                state = state.listState,
 | 
			
		||||
                horizontalAlignment = Alignment.CenterHorizontally
 | 
			
		||||
            ) {
 | 
			
		||||
                items(
 | 
			
		||||
                    items = worlds,
 | 
			
		||||
                    key = { it.id }
 | 
			
		||||
                ) { world ->
 | 
			
		||||
                    ReorderableItem(state, key = world.id) { isDragging ->
 | 
			
		||||
                        if (!world.id.isEmpty()) {
 | 
			
		||||
                            WorldInfoCard(
 | 
			
		||||
                                bitmap = world.icon,
 | 
			
		||||
                                id = world.id,
 | 
			
		||||
                                displayName = world.displayName,
 | 
			
		||||
                                lastPlayed = world.lastPlayed
 | 
			
		||||
                            )
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Text(
 | 
			
		||||
                                text = "↑ Worlds above this line will be backed up ↑",
 | 
			
		||||
                                modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
 | 
			
		||||
                                textAlign = TextAlign.Center
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -118,10 +117,7 @@ fun WorldInfoCard(
 | 
			
		|||
    iconSize: Dp = 64.dp, // Control icon size here
 | 
			
		||||
    id: String,
 | 
			
		||||
    displayName: String,
 | 
			
		||||
    lastPlayed: ZonedDateTime,
 | 
			
		||||
    elevation: CardElevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
 | 
			
		||||
    internalPadding: Dp = 16.dp,
 | 
			
		||||
    spacingBetweenIconAndText: Dp = 16.dp
 | 
			
		||||
    lastPlayed: ZonedDateTime
 | 
			
		||||
) {
 | 
			
		||||
    // Formatter for the timestamp - remember caches the formatter across recompositions
 | 
			
		||||
    val formatter = remember {
 | 
			
		||||
| 
						 | 
				
			
			@ -131,15 +127,14 @@ fun WorldInfoCard(
 | 
			
		|||
        lastPlayed.format(formatter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Card(
 | 
			
		||||
    ElevatedCard(
 | 
			
		||||
        modifier = modifier
 | 
			
		||||
            .padding(horizontal = 8.dp, vertical = 4.dp)
 | 
			
		||||
            .width(300.dp),
 | 
			
		||||
        elevation = elevation
 | 
			
		||||
            .width(300.dp)
 | 
			
		||||
    ) {
 | 
			
		||||
        Row(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .padding(internalPadding)
 | 
			
		||||
                .padding(horizontal = 16.dp, vertical = 8.dp)
 | 
			
		||||
                .fillMaxWidth(),
 | 
			
		||||
            verticalAlignment = Alignment.CenterVertically
 | 
			
		||||
        ) {
 | 
			
		||||
| 
						 | 
				
			
			@ -163,20 +158,17 @@ fun WorldInfoCard(
 | 
			
		|||
                contentScale = ContentScale.Crop // Crop is usually best for fixed aspect ratio
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            Spacer(modifier = Modifier.width(spacingBetweenIconAndText))
 | 
			
		||||
            Spacer(modifier = Modifier.width(16.dp))
 | 
			
		||||
 | 
			
		||||
            // --- Text Column ---
 | 
			
		||||
            Column(
 | 
			
		||||
                modifier = Modifier.weight(1f),
 | 
			
		||||
                verticalArrangement = Arrangement.spacedBy(4.dp)
 | 
			
		||||
            ) {
 | 
			
		||||
                Text(
 | 
			
		||||
                    text = displayName,
 | 
			
		||||
                    style = MaterialTheme.typography.titleLarge
 | 
			
		||||
                )
 | 
			
		||||
                Spacer(
 | 
			
		||||
                    modifier = Modifier.width(5.dp)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                Text(
 | 
			
		||||
                    text = "Last played $formattedTimestamp", // Use formatted timestamp
 | 
			
		||||
                    style = MaterialTheme.typography.bodySmall,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,13 +10,11 @@ import eu.m724.pojavbackup.core.World
 | 
			
		|||
import eu.m724.pojavbackup.core.WorldRepository
 | 
			
		||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
 | 
			
		||||
import eu.m724.pojavbackup.proto.WorldOrder
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.asStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.update
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import java.time.Instant
 | 
			
		||||
import java.time.ZoneOffset
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
| 
						 | 
				
			
			@ -30,32 +28,16 @@ class ContentScreenViewModel @Inject constructor(
 | 
			
		|||
    private val _worlds = MutableStateFlow<List<World>>(emptyList())
 | 
			
		||||
    val worlds: StateFlow<List<World>> = _worlds.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    fun listWorlds() {
 | 
			
		||||
    init {
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            withContext(Dispatchers.IO) {
 | 
			
		||||
                // TODO load order
 | 
			
		||||
                val worldOrder = settingsRepository.getSettings().worldOrder
 | 
			
		||||
                val worlds = worldOrder.worldIdsList.map {
 | 
			
		||||
            settingsRepository.getSettingsFlow().collect { settings ->
 | 
			
		||||
                val worlds = settings.worldOrder.worldIdsList.map {
 | 
			
		||||
                    // TODO mark deleted worlds better
 | 
			
		||||
                    worldRepository.getWorld(it) ?: World(it, "Deleted world", Instant.EPOCH.atZone(ZoneOffset.UTC), null)
 | 
			
		||||
                }.toMutableList()
 | 
			
		||||
                worlds.add(worldOrder.separatorIndex, World.SEPARATOR)
 | 
			
		||||
                worlds.add(settings.worldOrder.separatorIndex, World.SEPARATOR)
 | 
			
		||||
 | 
			
		||||
                _worlds.update { worlds }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun listExtras() {
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            withContext(Dispatchers.IO) {
 | 
			
		||||
                // TODO load order
 | 
			
		||||
                val worldOrder = settingsRepository.getSettings().worldOrder
 | 
			
		||||
                val worlds = worldOrder.worldIdsList.map {
 | 
			
		||||
                    // TODO mark deleted worlds better
 | 
			
		||||
                    worldRepository.getWorld(it) ?: World(it, "Deleted world", Instant.EPOCH.atZone(ZoneOffset.UTC), null)
 | 
			
		||||
                }.toMutableList()
 | 
			
		||||
                worlds.add(worldOrder.separatorIndex, World.SEPARATOR)
 | 
			
		||||
                // TODO same for extras
 | 
			
		||||
 | 
			
		||||
                _worlds.update { worlds }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,83 @@
 | 
			
		|||
package eu.m724.pojavbackup.settings.screen.destination
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.Spacer
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxHeight
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyColumn
 | 
			
		||||
import androidx.compose.foundation.lazy.items
 | 
			
		||||
import androidx.compose.material3.Button
 | 
			
		||||
import androidx.compose.material3.Card
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.res.painterResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.hilt.navigation.compose.hiltViewModel
 | 
			
		||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
 | 
			
		||||
import androidx.navigation.NavController
 | 
			
		||||
import eu.m724.pojavbackup.R
 | 
			
		||||
import eu.m724.pojavbackup.settings.screen.ScreenColumn
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun DestinationScreen(
 | 
			
		||||
    navController: NavController,
 | 
			
		||||
    onAddDestination: () -> Unit
 | 
			
		||||
) {
 | 
			
		||||
    val viewModel: DestinationScreenViewModel = hiltViewModel()
 | 
			
		||||
    val destinations by viewModel.destinations.collectAsStateWithLifecycle()
 | 
			
		||||
 | 
			
		||||
    ScreenColumn {
 | 
			
		||||
        Column(
 | 
			
		||||
            modifier = Modifier.fillMaxWidth().fillMaxHeight(0.5f), // TODO make room for more destinations
 | 
			
		||||
            horizontalAlignment = Alignment.CenterHorizontally
 | 
			
		||||
        ) {
 | 
			
		||||
            if (destinations.isEmpty()) {
 | 
			
		||||
                Text("There are no destinations.")
 | 
			
		||||
            } else {
 | 
			
		||||
                LazyColumn(
 | 
			
		||||
                    horizontalAlignment = Alignment.CenterHorizontally
 | 
			
		||||
                ) {
 | 
			
		||||
                    items(
 | 
			
		||||
                        items = destinations
 | 
			
		||||
                    ) { destination ->
 | 
			
		||||
                        Card { // this is TODO
 | 
			
		||||
                            Text(destination.label)
 | 
			
		||||
                            Text(destination.uri)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        Column(
 | 
			
		||||
            modifier = Modifier.fillMaxSize(),
 | 
			
		||||
            verticalArrangement = Arrangement.Center,
 | 
			
		||||
            horizontalAlignment = Alignment.CenterHorizontally
 | 
			
		||||
        ) {
 | 
			
		||||
            Button(
 | 
			
		||||
                onClick =  onAddDestination
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    painter = painterResource(R.drawable.baseline_add_24),
 | 
			
		||||
                    contentDescription = null
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                Spacer(
 | 
			
		||||
                    modifier = Modifier.width(10.dp)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                Text(
 | 
			
		||||
                    text = "Add destination"
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
package eu.m724.pojavbackup.settings.screen.destination
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import dagger.hilt.android.lifecycle.HiltViewModel
 | 
			
		||||
import dagger.hilt.android.qualifiers.ApplicationContext
 | 
			
		||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
 | 
			
		||||
import eu.m724.pojavbackup.proto.BackupDestination
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.asStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.update
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
@HiltViewModel
 | 
			
		||||
class DestinationScreenViewModel @Inject constructor(
 | 
			
		||||
    @ApplicationContext private val appContext: Context,
 | 
			
		||||
    private val settingsRepository: SettingsRepository
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
    private val _destinations = MutableStateFlow<List<BackupDestination>>(emptyList())
 | 
			
		||||
    val destinations: StateFlow<List<BackupDestination>> = _destinations.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            settingsRepository.getSettingsFlow().collect { settings ->
 | 
			
		||||
                _destinations.update {
 | 
			
		||||
                    settings.destinationsList
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,14 @@ message WorldOrder {
 | 
			
		|||
  int32 separatorIndex = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message BackupDestination {
 | 
			
		||||
  string label = 1;
 | 
			
		||||
  string uri = 2; // TODO
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Settings {
 | 
			
		||||
  WorldOrder worldOrder = 1;
 | 
			
		||||
  repeated string extraPaths = 2;
 | 
			
		||||
 | 
			
		||||
  repeated BackupDestination destinations = 3;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/baseline_add_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/baseline_add_24.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
 | 
			
		||||
      
 | 
			
		||||
    <path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
 | 
			
		||||
    
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/baseline_arrow_back_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/baseline_arrow_back_24.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
 | 
			
		||||
      
 | 
			
		||||
    <path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
 | 
			
		||||
    
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/baseline_cloud_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/baseline_cloud_24.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
 | 
			
		||||
      
 | 
			
		||||
    <path android:fillColor="@android:color/white" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96z"/>
 | 
			
		||||
    
 | 
			
		||||
</vector>
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue