diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index c224ad5..131e44d 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index a4f09e2..1a1bf72 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4dd8821..354b030 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -3,6 +3,9 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.hilt)
+ alias(libs.plugins.protobuf)
}
android {
@@ -41,7 +44,6 @@ android {
}
dependencies {
-
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
@@ -56,6 +58,10 @@ dependencies {
implementation(libs.knbt)
implementation(libs.reorderable)
implementation(libs.androidx.navigation.compose)
+ implementation(libs.hilt.android)
+ implementation(libs.androidx.hilt.navigation.compose)
+ implementation(libs.androidx.datastore)
+ implementation(libs.protobuf.javalite)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -63,4 +69,20 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
+ ksp(libs.hilt.compiler)
+}
+
+protobuf {
+ protoc {
+ artifact = "com.google.protobuf:protoc:4.30.2"
+ }
+ generateProtoTasks {
+ all().forEach { task ->
+ task.builtins {
+ create("java") {
+ option("lite")
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 25b3816..a211ec4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+ android:theme="@style/Theme.PojavBackup">
+
@@ -34,17 +39,6 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/MyCloudProvider.kt b/app/src/main/java/eu/m724/pojavbackup/MyCloudProvider.kt
deleted file mode 100644
index 6c89956..0000000
--- a/app/src/main/java/eu/m724/pojavbackup/MyCloudProvider.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-package eu.m724.pojavbackup
-
-import android.R
-import android.database.Cursor
-import android.database.MatrixCursor
-import android.os.CancellationSignal
-import android.os.ParcelFileDescriptor
-import android.provider.DocumentsContract
-import android.provider.DocumentsContract.Root
-import android.provider.DocumentsProvider
-import android.util.Log
-import java.io.FileNotFoundException
-
-
-class MyCloudProvider : DocumentsProvider() {
- val TAG: String = "MinimalDocsProvider"
-
- // Define the authority (must match AndroidManifest.xml)
- // Use your app's package name + ".documents" or similar unique identifier
- val AUTHORITY: String = "com.example.myapp.minimalprovider"
-
- // Define a unique ID for our single root
- val ROOT_ID: String = "minimal_root"
-
- // Define the document ID for the root directory itself
- val ROOT_DOC_ID: String = "minimal_root_directory"
-
- private val DEFAULT_ROOT_PROJECTION: Array = arrayOf(
- DocumentsContract.Root.COLUMN_ROOT_ID,
- DocumentsContract.Root.COLUMN_MIME_TYPES,
- DocumentsContract.Root.COLUMN_FLAGS,
- DocumentsContract.Root.COLUMN_ICON,
- DocumentsContract.Root.COLUMN_TITLE,
- DocumentsContract.Root.COLUMN_SUMMARY,
- DocumentsContract.Root.COLUMN_DOCUMENT_ID,
- DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
- )
- private val DEFAULT_DOCUMENT_PROJECTION: Array = arrayOf(
- DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- DocumentsContract.Document.COLUMN_MIME_TYPE,
- DocumentsContract.Document.COLUMN_DISPLAY_NAME,
- DocumentsContract.Document.COLUMN_LAST_MODIFIED,
- DocumentsContract.Document.COLUMN_FLAGS,
- DocumentsContract.Document.COLUMN_SIZE
- )
-
- override fun onCreate(): Boolean {
- return true
- }
-
- override fun queryRoots(projection: Array?): Cursor {
- Log.d(TAG, "queryRoots called")
-
- // Use the default projection if none is provided
- val resolvedProjection: Array = projection ?: DEFAULT_ROOT_PROJECTION
-
-
- // Create a MatrixCursor to hold the root information
- val cursor = MatrixCursor(resolvedProjection)
-
-
- // Add a single row for our minimal root
- val row = cursor.newRow()
- row.add(Root.COLUMN_ROOT_ID, ROOT_ID)
- row.add(Root.COLUMN_TITLE, "My Minimal Provider") // The name shown in the file manager
- row.add(Root.COLUMN_DOCUMENT_ID, ROOT_DOC_ID) // ID of the root directory document
-
-
- // --- IMPORTANT: Add an icon! ---
- // Replace R.mipmap.ic_launcher with your actual app icon or a dedicated icon
- // If you don't have one, the entry might not show up correctly.
- row.add(Root.COLUMN_ICON, R.mipmap.sym_def_app_icon)
-
-
- // --- Set Flags ---
- // FLAG_SUPPORTS_CREATE: If you plan to allow creating files/folders later
- // FLAG_LOCAL_ONLY: If the data is only on the device
- // FLAG_SUPPORTS_SEARCH: If you plan to implement search
- // For minimal display, LOCAL_ONLY is often sufficient.
- row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY /* | Root.FLAG_SUPPORTS_CREATE */)
-
-
- // You could add other columns here if they are in the projection
- // row.add(Root.COLUMN_SUMMARY, "A basic example provider");
- // row.add(Root.COLUMN_AVAILABLE_BYTES, null); // Or calculate if known
- return cursor
- }
-
- override fun queryDocument(documentId: String?, projection: Array?): Cursor {
- println("queryDocument called for ID: $documentId")
-
- throw FileNotFoundException("Document not found: $documentId")
- }
-
- override fun queryChildDocuments(
- parentDocumentId: String?,
- projection: Array?,
- sortOrder: String?
- ): Cursor {
- println("queryChildDocuments called for parent ID: $parentDocumentId")
-
- val resolvedProjection: Array = projection
- ?: DEFAULT_DOCUMENT_PROJECTION
- val cursor = MatrixCursor(resolvedProjection)
-
-
- // Check if the request is for the children of our root directory
- if (ROOT_DOC_ID.equals(parentDocumentId)) {
- // --- THIS IS WHERE YOU WOULD ADD ACTUAL FILES/FOLDERS ---
- // For this minimal example, we return an empty cursor, meaning the root directory is empty.
-
- // Example of adding a dummy file (if you wanted to test further):
- /*
- MatrixCursor.RowBuilder row = cursor.newRow();
- row.add(Document.COLUMN_DOCUMENT_ID, "dummy_file_1");
- row.add(Document.COLUMN_DISPLAY_NAME, "hello.txt");
- row.add(Document.COLUMN_MIME_TYPE, "text/plain");
- row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE);
- row.add(Document.COLUMN_SIZE, 1234);
- row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
- */
- } else {
- // We only know the root, so any other parent ID is invalid
- println("queryChildDocuments requested for unknown parent ID: $parentDocumentId")
- throw FileNotFoundException("Parent document not found: $parentDocumentId")
- }
-
- return cursor
- }
-
- override fun openDocument(
- documentId: String?,
- mode: String?,
- signal: CancellationSignal?
- ): ParcelFileDescriptor {
- throw FileNotFoundException("Cannot open document in minimal provider: $documentId");
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/PojavBackupApplication.kt b/app/src/main/java/eu/m724/pojavbackup/PojavBackupApplication.kt
new file mode 100644
index 0000000..96650c3
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/PojavBackupApplication.kt
@@ -0,0 +1,9 @@
+package eu.m724.pojavbackup
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class PojavBackupApplication : Application() {
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/DataLoader.kt b/app/src/main/java/eu/m724/pojavbackup/core/DataLoader.kt
deleted file mode 100644
index 1322d45..0000000
--- a/app/src/main/java/eu/m724/pojavbackup/core/DataLoader.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package eu.m724.pojavbackup.core
-
-import android.content.ContentResolver
-import androidx.documentfile.provider.DocumentFile
-
-class DataLoader(
- private val contentResolver: ContentResolver,
- private val dataDirectory: DocumentFile
-) {
-
- fun listWorlds(): List {
- return WorldDetector(contentResolver, getSavesDirectory())
- .listWorlds()
- }
-
- fun listWorlds(consumer: (World) -> Unit) {
- WorldDetector(contentResolver, getSavesDirectory())
- .listWorlds(consumer)
- }
-
- fun getWorld(id: String): World? {
- return WorldDetector(contentResolver, getSavesDirectory())
- .getWorld(id)
- }
-
- fun getSavesDirectory(): DocumentFile {
- return dataDirectory.findFile(".minecraft")!!.findFile("saves")!!
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/WorldDetector.kt b/app/src/main/java/eu/m724/pojavbackup/core/WorldDetector.kt
index 03ced12..3857c82 100644
--- a/app/src/main/java/eu/m724/pojavbackup/core/WorldDetector.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/core/WorldDetector.kt
@@ -4,10 +4,11 @@ import android.content.ContentResolver
import android.graphics.BitmapFactory
import android.util.Log
import androidx.documentfile.provider.DocumentFile
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
import net.benwoodworth.knbt.Nbt
import net.benwoodworth.knbt.NbtCompound
import net.benwoodworth.knbt.NbtCompression
-import net.benwoodworth.knbt.NbtTag
import net.benwoodworth.knbt.NbtVariant
import net.benwoodworth.knbt.decodeFromStream
import net.benwoodworth.knbt.nbtCompound
@@ -27,19 +28,14 @@ class WorldDetector(
compression = NbtCompression.Gzip
}
- // TODO not at once
- /**
- * List worlds in the savesDirectory
- *
- * @return The worlds
- */
- fun listWorlds(): List {
- return savesDirectory.listFiles().mapNotNull {
- try {
- getWorldFromDirectory(it)
- } catch (e: InvalidWorldException) {
- Log.i(TAG, "${it.name} is invalid: ${e.message}")
- null
+ fun listWorlds(): Flow {
+ return flow {
+ savesDirectory.listFiles().mapNotNull {
+ try {
+ emit(getWorldFromDirectory(it))
+ } catch (e: InvalidWorldException) {
+ Log.i(TAG, "${it.name} is invalid: ${e.message}")
+ }
}
}
}
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/WorldRepository.kt b/app/src/main/java/eu/m724/pojavbackup/core/WorldRepository.kt
new file mode 100644
index 0000000..3b22f26
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/core/WorldRepository.kt
@@ -0,0 +1,58 @@
+package eu.m724.pojavbackup.core
+
+import android.content.Context
+import androidx.documentfile.provider.DocumentFile
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class WorldRepository @Inject constructor(
+ @ApplicationContext private val appContext: Context
+) {
+ private lateinit var savesDirectory: DocumentFile
+ private lateinit var worldDetector: WorldDetector
+
+ private var worldsCache: List? = null
+ private val cacheMutex = Mutex() // To ensure thread-safe access to cache
+
+ fun setSavesDirectory(documentFile: DocumentFile) {
+ this.savesDirectory = documentFile
+ this.worldDetector = WorldDetector(appContext.contentResolver, savesDirectory)
+ }
+
+ suspend fun listWorlds(): List {
+ cacheMutex.withLock {
+ if (worldsCache != null) {
+ return worldsCache!! // Return copy or immutable list if needed
+ }
+ }
+
+ // If cache is empty, fetch data on IO dispatcher
+ val freshData = withContext(Dispatchers.IO) {
+ worldDetector.listWorlds().toList() // TODO
+ }
+
+ // Store in cache (thread-safe)
+ cacheMutex.withLock {
+ worldsCache = freshData
+ }
+
+ return freshData
+ }
+
+ suspend fun getWorld(id: String): World? {
+ return listWorlds().firstOrNull { it.id == id }
+ }
+
+ suspend fun clearCache() {
+ cacheMutex.withLock {
+ worldsCache = null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/datastore/DataStoreModule.kt b/app/src/main/java/eu/m724/pojavbackup/core/datastore/DataStoreModule.kt
new file mode 100644
index 0000000..96bc440
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/core/datastore/DataStoreModule.kt
@@ -0,0 +1,42 @@
+package eu.m724.pojavbackup.core.datastore
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.dataStoreFile
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import eu.m724.pojavbackup.proto.Settings
+import javax.inject.Singleton
+
+private const val USER_PREFERENCES_FILE_NAME = "user_prefs.pb"
+
+@Module
+@InstallIn(SingletonComponent::class) // Provides dependencies for the entire app
+object DataStoreModule {
+ @Provides
+ @Singleton // Ensure only one instance is created
+ fun provideUserPreferencesDataStore(
+ @ApplicationContext applicationContext: Context,
+ // Optional: You can inject CoroutineScope/Dispatcher if you want to customize it
+ // userPreferencesSerializer: UserPreferencesSerializer // Inject serializer if it's a class with dependencies
+ ): DataStore {
+ return DataStoreFactory.create(
+ serializer = SettingsSerializer, // Use your serializer instance/object
+ produceFile = { applicationContext.dataStoreFile(USER_PREFERENCES_FILE_NAME) },
+ // Optional: Add corruption handler
+ // corruptionHandler = ReplaceFileCorruptionHandler { ex -> /* Handle corruption */ UserPreferences.getDefaultInstance() },
+ // Optional: Specify a custom scope, though the default (SupervisorJob + Dispatchers.IO) is usually fine
+ // scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ )
+ }
+
+ // If your Serializer was a class and had dependencies, you'd provide it too:
+ // @Provides
+ // fun provideUserPreferencesSerializer(): UserPreferencesSerializer {
+ // return UserPreferencesSerializer(/* dependencies */)
+ // }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/datastore/SettingsRepository.kt b/app/src/main/java/eu/m724/pojavbackup/core/datastore/SettingsRepository.kt
new file mode 100644
index 0000000..7cbb3b7
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/core/datastore/SettingsRepository.kt
@@ -0,0 +1,31 @@
+package eu.m724.pojavbackup.core.datastore
+
+import androidx.datastore.core.DataStore
+import eu.m724.pojavbackup.proto.Settings
+import eu.m724.pojavbackup.proto.WorldOrder
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class SettingsRepository @Inject constructor(
+ private val dataStore: DataStore
+) {
+ fun getSettingsFlow(): Flow {
+ return dataStore.data
+ }
+
+ suspend fun getSettings(): Settings {
+ return dataStore.data.first()
+ }
+
+ suspend fun updateWorldOrder(worldOrder: WorldOrder) {
+ dataStore.updateData {
+ it.toBuilder()
+ .clearWorldOrder()
+ .setWorldOrder(worldOrder)
+ .build()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/datastore/SettingsSerializer.kt b/app/src/main/java/eu/m724/pojavbackup/core/datastore/SettingsSerializer.kt
new file mode 100644
index 0000000..8e66034
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/core/datastore/SettingsSerializer.kt
@@ -0,0 +1,24 @@
+package eu.m724.pojavbackup.core.datastore
+
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.Serializer
+import com.google.protobuf.InvalidProtocolBufferException
+import eu.m724.pojavbackup.proto.Settings
+import java.io.InputStream
+import java.io.OutputStream
+
+object SettingsSerializer : Serializer {
+ override val defaultValue: Settings = Settings.getDefaultInstance()
+
+ override suspend fun readFrom(input: InputStream): Settings {
+ try {
+ return Settings.parseFrom(input)
+ } catch (exception: InvalidProtocolBufferException) {
+ throw CorruptionException("Cannot read proto.", exception)
+ }
+ }
+
+ override suspend fun writeTo(t: Settings, output: OutputStream) {
+ t.writeTo(output)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/home/HomeActivity.kt b/app/src/main/java/eu/m724/pojavbackup/home/HomeActivity.kt
index be3f3c2..ad33fa6 100644
--- a/app/src/main/java/eu/m724/pojavbackup/home/HomeActivity.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/home/HomeActivity.kt
@@ -1,21 +1,24 @@
package eu.m724.pojavbackup.home
+import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.spring
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
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.ColumnScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
@@ -26,76 +29,126 @@ 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
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
+import dagger.hilt.android.AndroidEntryPoint
import eu.m724.pojavbackup.R
-import eu.m724.pojavbackup.home.screen.Screen
+import eu.m724.pojavbackup.home.screen.HomeScreen
import eu.m724.pojavbackup.home.screen.dashboard.DashboardScreen
import eu.m724.pojavbackup.home.screen.history.HistoryScreen
+import eu.m724.pojavbackup.settings.SettingsActivity
+import eu.m724.pojavbackup.setup.SetupActivity
import eu.m724.pojavbackup.ui.theme.PojavBackupTheme
+@AndroidEntryPoint
class HomeActivity : ComponentActivity() {
+ private val viewModel: HomeViewModel by viewModels()
+
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))
+ }
+ )
+
enableEdgeToEdge()
setContent {
- val navController = rememberNavController()
-
PojavBackupTheme {
- Scaffold(
- modifier = Modifier.fillMaxSize(),
- bottomBar = {
- NavigationBar {
- ScreenNavigationBarItem(
- navController = navController,
- label = "Dashboard",
- route = Screen.Dashboard,
- iconResourceId = R.drawable.baseline_home_24
- )
- ScreenNavigationBarItem(
- navController = navController,
- label = "History",
- route = Screen.History,
- iconResourceId = R.drawable.baseline_history_24
- )
- }
- }
- ) { innerPadding ->
- NavHost(
- navController = navController,
- startDestination = Screen.Dashboard,
- modifier = Modifier.padding(innerPadding),
- enterTransition = {
- fadeIn() + slideInHorizontally(initialOffsetX = { it / 10 })
- },
- exitTransition = {
- fadeOut() + slideOutHorizontally(targetOffsetX = { -it / 10 })
- }
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ if (uiState.loading) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- composable {
- DashboardScreen(navController)
- }
- composable {
- HistoryScreen()
- }
- // Add more destinations similarly.
+ CircularProgressIndicator()
}
+ } else {
+ HomeScaffold(
+ onSettingsOpen = { onSettingsOpen(it) }
+ )
}
}
}
}
+
+ fun onSettingsOpen(page: String) {
+ startActivity(Intent(applicationContext, SettingsActivity::class.java).putExtra("settingsPage", page))
+ }
+}
+
+@Composable
+fun HomeScaffold(
+ onSettingsOpen: (String) -> Unit
+) {
+ val navController = rememberNavController()
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ bottomBar = {
+ NavigationBar {
+ ScreenNavigationBarItem(
+ navController = navController,
+ label = "Dashboard",
+ route = HomeScreen.Dashboard,
+ iconResourceId = R.drawable.baseline_home_24
+ )
+ ScreenNavigationBarItem(
+ navController = navController,
+ label = "History",
+ route = HomeScreen.History,
+ iconResourceId = R.drawable.baseline_history_24
+ )
+ }
+ }
+ ) { innerPadding ->
+ NavHost(
+ navController = navController,
+ startDestination = HomeScreen.Dashboard,
+ modifier = Modifier.padding(innerPadding),
+ enterTransition = {
+ fadeIn() + slideInHorizontally(initialOffsetX = { it / 10 })
+ },
+ exitTransition = {
+ fadeOut() + slideOutHorizontally(targetOffsetX = { -it / 10 })
+ }
+ ) {
+ composable {
+ DashboardScreen(
+ navController = navController,
+ onWorldsIncludedClick = {
+ onSettingsOpen("content")
+ }
+ )
+ }
+ composable {
+ HistoryScreen()
+ }
+ // Add more destinations similarly.
+ }
+ }
}
@Composable
fun RowScope.ScreenNavigationBarItem(
navController: NavController,
label: String,
- route: Screen,
+ route: HomeScreen,
iconResourceId: Int
) {
val selected = isSelected(navController, route)
@@ -122,7 +175,7 @@ fun RowScope.ScreenNavigationBarItem(
@Composable
fun isSelected(
navController: NavController,
- route: Screen
+ route: HomeScreen
): Boolean {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
diff --git a/app/src/main/java/eu/m724/pojavbackup/home/HomeUiState.kt b/app/src/main/java/eu/m724/pojavbackup/home/HomeUiState.kt
new file mode 100644
index 0000000..aba434d
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/home/HomeUiState.kt
@@ -0,0 +1,5 @@
+package eu.m724.pojavbackup.home
+
+data class HomeUiState(
+ val loading: Boolean = true
+)
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/home/HomeViewModel.kt b/app/src/main/java/eu/m724/pojavbackup/home/HomeViewModel.kt
index f405120..769ce2d 100644
--- a/app/src/main/java/eu/m724/pojavbackup/home/HomeViewModel.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/home/HomeViewModel.kt
@@ -1,4 +1,87 @@
package eu.m724.pojavbackup.home
-class HomeViewModel {
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import androidx.core.net.toUri
+import androidx.documentfile.provider.DocumentFile
+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.WorldRepository
+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 javax.inject.Inject
+
+@HiltViewModel
+class HomeViewModel @Inject constructor(
+ @ApplicationContext private val appContext: Context,
+ private val worldRepository: WorldRepository
+) : ViewModel() {
+ private val TAG = javaClass.name
+
+ private val _uiState = MutableStateFlow(HomeUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ fun load(
+ onSetupNeeded: () -> Unit
+ ) {
+ // TODO this should be dynamic (selected) or a list (debug/normal version)
+ val uri = "content://net.kdt.pojavlaunch.scoped.gamefolder.debug/tree/%2Fstorage%2Femulated%2F0%2FAndroid%2Fdata%2Fnet.kdt.pojavlaunch.debug%2Ffiles".toUri()
+
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ val hasPermission = checkForStoragePermission(uri)
+
+ if (hasPermission) {
+ // some paths were checked earlier
+ val documentFile = DocumentFile.fromTreeUri(appContext, uri)!!
+ .findFile(".minecraft")!!
+ .findFile("saves")
+
+ if (documentFile != null) {
+ worldRepository.setSavesDirectory(documentFile)
+ worldRepository.listWorlds()
+ } else {
+ // TODO handle if "saves" doesn't exist
+ }
+
+ _uiState.update { it.copy(loading = false) }
+ } else {
+ // TODO there could be that only one or two permissions are missing
+ onSetupNeeded()
+ }
+ }
+ }
+ }
+
+ private fun checkForStoragePermission(uri: Uri): Boolean {
+ Log.i(TAG, "Checking for storage permission...")
+
+ // TODO Is this the right way? This isn't in https://developer.android.com/training/data-storage/shared/documents-files
+ val directory = DocumentFile.fromTreeUri(appContext, uri)
+
+ if (directory == null || !directory.isDirectory) {
+ Log.i(TAG, "No permission or not a directory")
+ return false
+ }
+
+ val dotMinecraftExists = directory.findFile(".minecraft") != null
+ val controlMapExists = directory.findFile("controlmap") != null
+
+ if (!dotMinecraftExists || !controlMapExists) {
+ Log.i(TAG, "Selected directory is missing .minecraft or controlmap")
+ return false
+ }
+
+ Log.i(TAG, "Yes we have permission")
+
+ return true
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/home/screen/Screen.kt b/app/src/main/java/eu/m724/pojavbackup/home/screen/HomeScreen.kt
similarity index 79%
rename from app/src/main/java/eu/m724/pojavbackup/home/screen/Screen.kt
rename to app/src/main/java/eu/m724/pojavbackup/home/screen/HomeScreen.kt
index f72a61e..b8492a8 100644
--- a/app/src/main/java/eu/m724/pojavbackup/home/screen/Screen.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/home/screen/HomeScreen.kt
@@ -1,6 +1,5 @@
package eu.m724.pojavbackup.home.screen
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
@@ -11,9 +10,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.serialization.Serializable
-@Serializable sealed interface Screen {
- @Serializable data object Dashboard : Screen
- @Serializable data object History : Screen
+@Serializable sealed interface HomeScreen {
+ @Serializable data object Dashboard : HomeScreen
+ @Serializable data object History : HomeScreen
}
@Composable
diff --git a/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreen.kt b/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreen.kt
index a91934b..8bea8fd 100644
--- a/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreen.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreen.kt
@@ -9,42 +9,43 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.material3.TextFieldDefaults
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import eu.m724.pojavbackup.R
-import eu.m724.pojavbackup.home.screen.Screen
+import eu.m724.pojavbackup.home.screen.HomeScreen
import eu.m724.pojavbackup.home.screen.ScreenColumn
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DashboardScreen(
- navController: NavController
+ navController: NavController,
+ onWorldsIncludedClick: () -> Unit,
) {
+ val viewModel: DashboardScreenViewModel = hiltViewModel()
+ val settings by viewModel.settings.collectAsStateWithLifecycle()
+
ScreenColumn {
FlowRow(
maxItemsInEachRow = 3
) {
DashboardCard(
title = "Worlds included",
- value = "1",
+ value = settings.worldOrder.separatorIndex,
iconResourceId = R.drawable.baseline_mosque_24,
- onClick = {
-
- }
+ onClick = onWorldsIncludedClick
)
DashboardCard(
@@ -58,7 +59,7 @@ fun DashboardScreen(
value = "1d ago",
iconResourceId = R.drawable.baseline_access_time_filled_24,
onClick = {
- navController.navigate(Screen.History)
+ navController.navigate(HomeScreen.History)
}
)
}
diff --git a/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreenViewModel.kt b/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreenViewModel.kt
new file mode 100644
index 0000000..095afc8
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreenViewModel.kt
@@ -0,0 +1,29 @@
+package eu.m724.pojavbackup.home.screen.dashboard
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import eu.m724.pojavbackup.core.datastore.SettingsRepository
+import eu.m724.pojavbackup.proto.Settings
+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 DashboardScreenViewModel @Inject constructor(
+ private val settingsRepository: SettingsRepository
+) : ViewModel() {
+ private val _settings = MutableStateFlow(Settings.getDefaultInstance())
+ val settings: StateFlow = _settings.asStateFlow()
+
+ init {
+ viewModelScope.launch {
+ settingsRepository.getSettingsFlow().collect { newSettings ->
+ _settings.update { newSettings }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/SettingsActivity.kt b/app/src/main/java/eu/m724/pojavbackup/settings/SettingsActivity.kt
new file mode 100644
index 0000000..5c7383e
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/SettingsActivity.kt
@@ -0,0 +1,153 @@
+package eu.m724.pojavbackup.settings
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+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.RowScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.navigation.NavController
+import androidx.navigation.NavDestination.Companion.hierarchy
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+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.options.OptionsScreen
+import eu.m724.pojavbackup.ui.theme.PojavBackupTheme
+
+@AndroidEntryPoint
+class SettingsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val startPage = when (intent.getStringExtra("settingsPage")) {
+ "options" -> SettingsScreen.Options
+ "content" -> SettingsScreen.Content
+ else -> SettingsScreen.Options
+ }
+
+ enableEdgeToEdge()
+ setContent {
+ val navController = rememberNavController()
+
+ PojavBackupTheme {
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ bottomBar = {
+ NavigationBar {
+ ScreenNavigationBarItem(
+ navController = navController,
+ label = "Options",
+ route = SettingsScreen.Options,
+ iconResourceId = R.drawable.baseline_settings_24
+ )
+ ScreenNavigationBarItem(
+ navController = navController,
+ label = "Content",
+ route = SettingsScreen.Content,
+ iconResourceId = R.drawable.baseline_folder_copy_24
+ )
+ }
+ }
+ ) { innerPadding ->
+ NavHost(
+ navController = navController,
+ startDestination = startPage,
+ modifier = Modifier.padding(innerPadding),
+ enterTransition = {
+ fadeIn() + slideInHorizontally(initialOffsetX = { it / 10 })
+ },
+ exitTransition = {
+ fadeOut() + slideOutHorizontally(targetOffsetX = { -it / 10 })
+ }
+ ) {
+ composable {
+ OptionsScreen(navController)
+ }
+ composable {
+ ContentScreen(navController)
+ }
+ // Add more destinations similarly.
+ }
+ }
+ }
+ }
+ }
+}
+
+// TODO those functions are reused in Home
+@Composable
+fun RowScope.ScreenNavigationBarItem(
+ navController: NavController,
+ label: String,
+ route: SettingsScreen,
+ iconResourceId: Int
+) {
+ val selected = isSelected(navController, route)
+
+ NavigationBarItem(
+ selected = selected,
+ onClick = {
+ if (!selected) {
+ navController.navigate(route)
+ }
+ },
+ icon = {
+ Icon(
+ painter = painterResource(iconResourceId),
+ contentDescription = label
+ )
+ },
+ label = {
+ Text(label)
+ }
+ )
+}
+
+@Composable
+fun isSelected(
+ navController: NavController,
+ route: SettingsScreen
+): Boolean {
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentDestination = navBackStackEntry?.destination
+
+ // Check if the current destination's hierarchy contains a destination
+ // whose route definition matches the target 'route' object.
+
+ // For @Serializable object routes, Navigation Compose typically uses a stable
+ // route string derived from the object's definition. We compare against that.
+ return currentDestination?.hierarchy?.any { destination ->
+ // Check if the destination in the hierarchy corresponds to the
+ // target 'route' object passed into this function.
+ // For @Serializable objects/classes used as routes, comparing
+ // the destination's 'route' property is the standard way.
+ destination.route == navController.graph.findNode(route)?.route
+ // Explanation:
+ // 1. `navController.graph.findNode(route)`: Finds the NavDestination node
+ // within the navigation graph that corresponds to your @Serializable 'route' object.
+ // (Requires NavController knows about this route, typically added via `composable(typeMap = ...)`)
+ // 2. `?.route`: Gets the unique route string pattern associated with that node.
+ // 3. `destination.route`: Gets the route string pattern of the current destination being checked in the hierarchy.
+ // 4. `==`: Compares if they are the same route.
+
+ } == true // If currentDestination is null, it's not selected, return false.
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/screen/SettingsScreen.kt b/app/src/main/java/eu/m724/pojavbackup/settings/screen/SettingsScreen.kt
new file mode 100644
index 0000000..41ef73f
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/screen/SettingsScreen.kt
@@ -0,0 +1,28 @@
+package eu.m724.pojavbackup.settings.screen
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import kotlinx.serialization.Serializable
+
+@Serializable sealed interface SettingsScreen {
+ @Serializable data object Options : SettingsScreen
+ @Serializable data object Content : SettingsScreen
+}
+
+@Composable
+fun ScreenColumn(
+ modifier: Modifier = Modifier,
+ content: @Composable (ColumnScope.() -> Unit)
+) {
+ Column(
+ modifier = modifier.fillMaxSize().padding(top = 50.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/screen/content/ContentScreen.kt b/app/src/main/java/eu/m724/pojavbackup/settings/screen/content/ContentScreen.kt
new file mode 100644
index 0000000..a1c751d
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/screen/content/ContentScreen.kt
@@ -0,0 +1,203 @@
+package eu.m724.pojavbackup.settings.screen.content
+
+import android.graphics.Bitmap
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+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.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.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
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+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.home.screen.ScreenColumn
+import org.burnoutcrew.reorderable.ReorderableItem
+import org.burnoutcrew.reorderable.detectReorderAfterLongPress
+import org.burnoutcrew.reorderable.rememberReorderableLazyListState
+import org.burnoutcrew.reorderable.reorderable
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+
+@Composable
+fun ContentScreen(
+ navController: NavController,
+) {
+ val viewModel: ContentScreenViewModel = hiltViewModel()
+
+ val worlds by viewModel.worlds.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ viewModel.listWorlds()
+ }
+
+ ScreenColumn {
+ 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
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * A Composable Card that displays a square icon (Bitmap or default drawable) on the left
+ * and text information (ID, Display Name, formatted Timestamp) on the right.
+ *
+ * @param modifier Optional Modifier for the Card.
+ * @param bitmap The Bitmap for the icon to display. If null, uses a pack.png-like default icon.
+ * @param iconSize The size for the square icon (width and height).
+ * @param id The ID text to display.
+ * @param displayName The display name text.
+ * @param lastPlayed The ZonedDateTime timestamp to display, formatted by locale.
+ * @param elevation The elevation of the card.
+ * @param internalPadding Padding inside the card, around the content.
+ * @param spacingBetweenIconAndText Space between the icon and the text column.
+ */
+@Composable
+fun WorldInfoCard(
+ modifier: Modifier = Modifier,
+ bitmap: Bitmap?,
+ 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
+) {
+ // Formatter for the timestamp - remember caches the formatter across recompositions
+ val formatter = remember {
+ DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) // Adjust FormatStyle as needed (SHORT, MEDIUM, LONG, FULL)
+ }
+ val formattedTimestamp = remember(lastPlayed, formatter) { // Only reformat when timestamp or formatter changes
+ lastPlayed.format(formatter)
+ }
+
+ Card(
+ modifier = modifier
+ .padding(horizontal = 8.dp, vertical = 4.dp)
+ .width(300.dp),
+ elevation = elevation
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(internalPadding)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // --- Icon ---
+ // Determine the correct painter based on whether bitmap is null
+ val painter: Painter = if (bitmap != null) {
+ // Remember the BitmapPainter based on the bitmap input
+ remember(bitmap) { BitmapPainter(bitmap.asImageBitmap()) }
+ } else {
+ // Use painterResource for the default drawable
+ painterResource(id = R.drawable.default_world_icon)
+ }
+
+ Image(
+ painter = painter, // Use the determined painter
+ contentDescription = "world icon", // Hardcoded content description
+ modifier = Modifier
+ .size(iconSize)
+ .align(Alignment.CenterVertically)
+ .clip(CardDefaults.shape), // TODO match corner radius
+ contentScale = ContentScale.Crop // Crop is usually best for fixed aspect ratio
+ )
+
+ Spacer(modifier = Modifier.width(spacingBetweenIconAndText))
+
+ // --- 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,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true, name = "Card with Default Icon")
+@Composable
+fun WorldInfoCardPreviewDefaultIcon() {
+ MaterialTheme {
+ Column(modifier = Modifier.padding(16.dp)) {
+ WorldInfoCard(
+ bitmap = null, // Test the default icon case
+ id = "world-001",
+ displayName = "Earth",
+ lastPlayed = ZonedDateTime.now().minusDays(1)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/screen/content/ContentScreenViewModel.kt b/app/src/main/java/eu/m724/pojavbackup/settings/screen/content/ContentScreenViewModel.kt
new file mode 100644
index 0000000..f3cb096
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/screen/content/ContentScreenViewModel.kt
@@ -0,0 +1,90 @@
+package eu.m724.pojavbackup.settings.screen.content
+
+import android.content.Context
+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.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
+
+@HiltViewModel
+class ContentScreenViewModel @Inject constructor(
+ @ApplicationContext private val appContext: Context,
+ private val worldRepository: WorldRepository,
+ private val settingsRepository: SettingsRepository
+) : ViewModel() {
+ private val _worlds = MutableStateFlow>(emptyList())
+ val worlds: StateFlow> = _worlds.asStateFlow()
+
+ fun listWorlds() {
+ 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)
+
+ _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)
+
+ _worlds.update { worlds }
+ }
+ }
+ }
+
+ fun moveWorld(fromIndex: Int, toIndex: Int) {
+ // Similar to mutableStateOf, create a NEW list
+ val currentList = _worlds.value.toMutableList()
+ // Check bounds for safety
+ if (fromIndex in currentList.indices && toIndex >= 0 && toIndex <= currentList.size) {
+ val item = currentList.removeAt(fromIndex)
+ currentList.add(toIndex, item)
+ _worlds.value = currentList // Assign the new list to the flow
+
+ updateWorldOrder()
+ } else {
+ Log.e("Reorder", "Invalid indices: from $fromIndex, to $toIndex, size ${currentList.size}")
+ }
+ }
+
+ private fun updateWorldOrder() {
+ viewModelScope.launch {
+ settingsRepository.updateWorldOrder(
+ WorldOrder.newBuilder()
+ .addAllWorldIds(_worlds.value.filter { it != World.SEPARATOR }.map { it.id })
+ .setSeparatorIndex(_worlds.value.indexOf(World.SEPARATOR))
+ .build()
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreen.kt b/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreen.kt
new file mode 100644
index 0000000..f81da2c
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreen.kt
@@ -0,0 +1,24 @@
+package eu.m724.pojavbackup.settings.screen.options
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavController
+
+@Composable
+fun OptionsScreen(
+ navController: NavController,
+) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("This is the options screen")
+ Text("It's empty for now")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt b/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt
index 3eac915..2f42e6e 100644
--- a/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt
@@ -1,8 +1,6 @@
package eu.m724.pojavbackup.setup
-import android.R.attr.end
import android.content.Intent
-import android.graphics.Bitmap
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
@@ -10,62 +8,32 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
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.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.CardElevation
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.graphics.painter.BitmapPainter
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.m724.pojavbackup.ui.theme.PojavBackupTheme
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.compose.NavHost
-import eu.m724.pojavbackup.R
+import dagger.hilt.android.AndroidEntryPoint
import eu.m724.pojavbackup.home.HomeActivity
-import kotlinx.coroutines.flow.update
-import org.burnoutcrew.reorderable.ReorderableItem
-import org.burnoutcrew.reorderable.detectReorder
-import org.burnoutcrew.reorderable.detectReorderAfterLongPress
-import org.burnoutcrew.reorderable.rememberReorderableLazyListState
-import org.burnoutcrew.reorderable.reorderable
-import java.time.ZonedDateTime
-import java.time.format.DateTimeFormatter
-import java.time.format.FormatStyle
-import kotlin.jvm.javaClass
-class MainActivity : ComponentActivity() {
+@AndroidEntryPoint
+class SetupActivity : ComponentActivity() {
private val viewModel: SetupViewModel by viewModels()
private val openDocumentTree = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
@@ -126,8 +94,6 @@ fun SetupScreen(
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
- val worldsState by viewModel.worlds.collectAsStateWithLifecycle()
-
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
@@ -177,145 +143,5 @@ fun SetupScreen(
}
}
}
-
- // TODO this is for testing world loading only
- /*val state = rememberReorderableLazyListState(onMove = { from, to ->
- viewModel.moveWorld(from.index, to.index)
- })
-
- LazyColumn(
- state = state.listState,
- modifier = Modifier
- .reorderable(state)
- .detectReorderAfterLongPress(state)
- ) {
- items(
- items = worldsState,
- 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
- )
- }
- }
- }
- }*/
- }
-}
-
-/**
- * A Composable Card that displays a square icon (Bitmap or default drawable) on the left
- * and text information (ID, Display Name, formatted Timestamp) on the right.
- *
- * @param modifier Optional Modifier for the Card.
- * @param bitmap The Bitmap for the icon to display. If null, uses a pack.png-like default icon.
- * @param iconSize The size for the square icon (width and height).
- * @param id The ID text to display.
- * @param displayName The display name text.
- * @param lastPlayed The ZonedDateTime timestamp to display, formatted by locale.
- * @param elevation The elevation of the card.
- * @param internalPadding Padding inside the card, around the content.
- * @param spacingBetweenIconAndText Space between the icon and the text column.
- */
-@Composable
-fun WorldInfoCard(
- modifier: Modifier = Modifier,
- bitmap: Bitmap?,
- 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
-) {
- // Formatter for the timestamp - remember caches the formatter across recompositions
- val formatter = remember {
- DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) // Adjust FormatStyle as needed (SHORT, MEDIUM, LONG, FULL)
- }
- val formattedTimestamp = remember(lastPlayed, formatter) { // Only reformat when timestamp or formatter changes
- lastPlayed.format(formatter)
- }
-
- Card(
- modifier = modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp, vertical = 4.dp),
- elevation = elevation
- ) {
- Row(
- modifier = Modifier
- .padding(internalPadding)
- .fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- // --- Icon ---
- // Determine the correct painter based on whether bitmap is null
- val painter: Painter = if (bitmap != null) {
- // Remember the BitmapPainter based on the bitmap input
- remember(bitmap) { BitmapPainter(bitmap.asImageBitmap()) }
- } else {
- // Use painterResource for the default drawable
- painterResource(id = R.drawable.default_world_icon)
- }
-
- Image(
- painter = painter, // Use the determined painter
- contentDescription = "world icon", // Hardcoded content description
- modifier = Modifier
- .size(iconSize)
- .align(Alignment.CenterVertically)
- .clip(CardDefaults.shape), // TODO match corner radius
- contentScale = ContentScale.Crop // Crop is usually best for fixed aspect ratio
- )
-
- Spacer(modifier = Modifier.width(spacingBetweenIconAndText))
-
- // --- 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,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
- }
-}
-
-@Preview(showBackground = true, name = "Card with Default Icon")
-@Composable
-fun WorldInfoCardPreviewDefaultIcon() {
- MaterialTheme {
- Column(modifier = Modifier.padding(16.dp)) {
- WorldInfoCard(
- bitmap = null, // Test the default icon case
- id = "world-001",
- displayName = "Earth",
- lastPlayed = ZonedDateTime.now().minusDays(1)
- )
- }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/setup/SetupViewModel.kt b/app/src/main/java/eu/m724/pojavbackup/setup/SetupViewModel.kt
index 16621f4..d5d54fc 100644
--- a/app/src/main/java/eu/m724/pojavbackup/setup/SetupViewModel.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/setup/SetupViewModel.kt
@@ -1,26 +1,17 @@
package eu.m724.pojavbackup.setup
-import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
import android.net.Uri
import android.util.Log
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import eu.m724.pojavbackup.core.World
-import eu.m724.pojavbackup.core.WorldDetector
-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
class SetupViewModel : ViewModel() {
private val TAG: String = javaClass.name
@@ -33,9 +24,6 @@ class SetupViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SetupUiState())
val uiState: StateFlow = _uiState.asStateFlow()
- private val _worlds = MutableStateFlow>(emptyList())
- val worlds: StateFlow> = _worlds.asStateFlow()
-
// TODO we could make the check call separate and not pass context here
fun onOpenDocumentTree(context: Context, uri: Uri?, result: (Boolean) -> Unit) {
if (uri != null) {
@@ -79,45 +67,9 @@ class SetupViewModel : ViewModel() {
Log.i(TAG, "Yes we have permission")
- // TODO remove
- listWorlds(context, directory)
-
return true
}
- // TODO World functions certainly don't belong here. This is for testing.
- private fun listWorlds(context: Context, documentFile: DocumentFile) {
- viewModelScope.launch {
- withContext(Dispatchers.IO) {
- Log.i(TAG, "listing worlds")
-
- val worldDetector = WorldDetector(
- contentResolver = context.contentResolver,
- savesDirectory = documentFile.findFile(".minecraft")!!.findFile("saves")!!
- )
-
- worldDetector.listWorlds { world ->
- _worlds.update { it + world }
- }
-
- _worlds.update { it + World.SEPARATOR }
- }
- }
- }
-
- fun moveWorld(fromIndex: Int, toIndex: Int) {
- // Similar to mutableStateOf, create a NEW list
- val currentList = _worlds.value.toMutableList()
- // Check bounds for safety
- if (fromIndex in currentList.indices && toIndex >= 0 && toIndex <= currentList.size) {
- val item = currentList.removeAt(fromIndex)
- currentList.add(toIndex, item)
- _worlds.value = currentList // Assign the new list to the flow
- } else {
- Log.e("Reorder", "Invalid indices: from $fromIndex, to $toIndex, size ${currentList.size}")
- }
- }
-
fun detectInstalledLauncherPackage(packageManager: PackageManager): List {
return POJAV_PACKAGES.filter {
try {
diff --git a/app/src/main/java/eu/m724/pojavbackup/ui/theme/Color.kt b/app/src/main/java/eu/m724/pojavbackup/ui/theme/Color.kt
deleted file mode 100644
index 39b5346..0000000
--- a/app/src/main/java/eu/m724/pojavbackup/ui/theme/Color.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package eu.m724.pojavbackup.ui.theme
-
-import androidx.compose.ui.graphics.Color
-
-val Purple80 = Color(0xFFD0BCFF)
-val PurpleGrey80 = Color(0xFFCCC2DC)
-val Pink80 = Color(0xFFEFB8C8)
-
-val Purple40 = Color(0xFF6650a4)
-val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/ui/theme/Theme.kt b/app/src/main/java/eu/m724/pojavbackup/ui/theme/Theme.kt
index 668f590..0e03768 100644
--- a/app/src/main/java/eu/m724/pojavbackup/ui/theme/Theme.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/ui/theme/Theme.kt
@@ -1,6 +1,5 @@
package eu.m724.pojavbackup.ui.theme
-import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -11,48 +10,23 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
-private val DarkColorScheme = darkColorScheme(
- primary = Purple80,
- secondary = PurpleGrey80,
- tertiary = Pink80
-)
-
-private val LightColorScheme = lightColorScheme(
- primary = Purple40,
- secondary = PurpleGrey40,
- tertiary = Pink40
-
- /* Other default colors to override
- background = Color(0xFFFFFBFE),
- surface = Color(0xFFFFFBFE),
- onPrimary = Color.White,
- onSecondary = Color.White,
- onTertiary = Color.White,
- onBackground = Color(0xFF1C1B1F),
- onSurface = Color(0xFF1C1B1F),
- */
-)
-
@Composable
fun PojavBackupTheme(
- darkTheme: Boolean = isSystemInDarkTheme(),
- // Dynamic color is available on Android 12+
- dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
- val colorScheme = when {
- dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- val context = LocalContext.current
- if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
- }
+ val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+ val darkTheme = isSystemInDarkTheme()
- darkTheme -> DarkColorScheme
- else -> LightColorScheme
+ val colorScheme = when {
+ dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
+ dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
+
+ darkTheme -> darkColorScheme()
+ else -> lightColorScheme()
}
MaterialTheme(
colorScheme = colorScheme,
- typography = Typography,
content = content
)
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/ui/theme/Type.kt b/app/src/main/java/eu/m724/pojavbackup/ui/theme/Type.kt
deleted file mode 100644
index a0fd7db..0000000
--- a/app/src/main/java/eu/m724/pojavbackup/ui/theme/Type.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package eu.m724.pojavbackup.ui.theme
-
-import androidx.compose.material3.Typography
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.sp
-
-// Set of Material typography styles to start with
-val Typography = Typography(
- bodyLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.5.sp
- )
- /* Other default text styles to override
- titleLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 22.sp,
- lineHeight = 28.sp,
- letterSpacing = 0.sp
- ),
- labelSmall = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 11.sp,
- lineHeight = 16.sp,
- letterSpacing = 0.5.sp
- )
- */
-)
\ No newline at end of file
diff --git a/app/src/main/proto/settings.proto b/app/src/main/proto/settings.proto
new file mode 100644
index 0000000..8381791
--- /dev/null
+++ b/app/src/main/proto/settings.proto
@@ -0,0 +1,14 @@
+syntax = "proto3";
+
+option java_package = "eu.m724.pojavbackup.proto";
+option java_multiple_files = true;
+
+message WorldOrder {
+ repeated string worldIds = 1;
+ int32 separatorIndex = 2;
+}
+
+message Settings {
+ WorldOrder worldOrder = 1;
+ repeated string extraPaths = 2;
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/baseline_folder_copy_24.xml b/app/src/main/res/drawable/baseline_folder_copy_24.xml
new file mode 100644
index 0000000..a7c1f0a
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_folder_copy_24.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_settings_24.xml b/app/src/main/res/drawable/baseline_settings_24.xml
new file mode 100644
index 0000000..6593f3a
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_settings_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f6986a1..478a0ae 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,6 @@
PojavBackup
- MainActivity
+ SetupActivity
HomeActivity
+ SettingsActivity
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 5ba8ae0..c6a2f03 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,4 +4,7 @@ plugins {
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
+ alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.hilt) apply false
+ alias(libs.plugins.protobuf) apply false
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7c8d803..6b5ee18 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
agp = "8.9.1"
-kotlin = "2.0.21"
+kotlin = "2.1.20"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
@@ -14,6 +14,12 @@ lifecycleViewmodelCompose = "2.8.7"
knbt = "0.11.8"
reorderable = "0.9.6"
navigation = "2.8.9"
+hilt = "2.56.1"
+ksp = "2.1.20-2.0.0"
+hiltNavigationCompose = "1.2.0"
+datastore = "1.1.4"
+protobufJavalite = "4.30.2"
+protobuf = "0.9.5"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -36,10 +42,17 @@ androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "l
knbt = { group = "net.benwoodworth.knbt", name = "knbt", version.ref = "knbt" }
reorderable = { group = "org.burnoutcrew.composereorderable", name = "reorderable", version.ref = "reorderable" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
+hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
+hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
+androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose"}
+androidx-datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
+protobuf-javalite = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protobufJavalite"}
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
-
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt"}
+protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }