Add simple settings
This commit is contained in:
parent
e90d0f21d1
commit
068d672eee
34 changed files with 994 additions and 568 deletions
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="2.0.21" />
|
<option name="version" value="2.1.20" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
|
|
@ -1,4 +1,3 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ plugins {
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
|
alias(libs.plugins.hilt)
|
||||||
|
alias(libs.plugins.protobuf)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -41,7 +44,6 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
|
|
@ -56,6 +58,10 @@ dependencies {
|
||||||
implementation(libs.knbt)
|
implementation(libs.knbt)
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
implementation(libs.androidx.navigation.compose)
|
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)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
@ -63,4 +69,20 @@ dependencies {
|
||||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||||
debugImplementation(libs.androidx.ui.tooling)
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
debugImplementation(libs.androidx.ui.test.manifest)
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".PojavBackupApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
|
@ -16,17 +17,21 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.PojavBackup"
|
android:theme="@style/Theme.PojavBackup">
|
||||||
tools:targetApi="31">
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".home.HomeActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/title_activity_home"
|
android:label="@string/title_activity_settings"
|
||||||
android:theme="@style/Theme.PojavBackup" />
|
android:theme="@style/Theme.PojavBackup" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".setup.MainActivity"
|
android:name=".setup.SetupActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_setup"
|
||||||
|
android:theme="@style/Theme.PojavBackup" />
|
||||||
|
<activity
|
||||||
|
android:name=".home.HomeActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/title_activity_main"
|
android:label="@string/title_activity_home"
|
||||||
android:theme="@style/Theme.PojavBackup">
|
android:theme="@style/Theme.PojavBackup">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
@ -34,17 +39,6 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name=".MyCloudProvider"
|
|
||||||
android:authorities="eu.m724.pojavbackup"
|
|
||||||
android:exported="true"
|
|
||||||
android:grantUriPermissions="true"
|
|
||||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
|
||||||
</intent-filter>
|
|
||||||
</provider>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -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<String> = 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<String> = 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<out String>?): Cursor {
|
|
||||||
Log.d(TAG, "queryRoots called")
|
|
||||||
|
|
||||||
// Use the default projection if none is provided
|
|
||||||
val resolvedProjection: Array<out String> = 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<out String>?): Cursor {
|
|
||||||
println("queryDocument called for ID: $documentId")
|
|
||||||
|
|
||||||
throw FileNotFoundException("Document not found: $documentId")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun queryChildDocuments(
|
|
||||||
parentDocumentId: String?,
|
|
||||||
projection: Array<out String>?,
|
|
||||||
sortOrder: String?
|
|
||||||
): Cursor {
|
|
||||||
println("queryChildDocuments called for parent ID: $parentDocumentId")
|
|
||||||
|
|
||||||
val resolvedProjection: Array<out String> = 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package eu.m724.pojavbackup
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
|
@HiltAndroidApp
|
||||||
|
class PojavBackupApplication : Application() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<World> {
|
|
||||||
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")!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,10 +4,11 @@ import android.content.ContentResolver
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import net.benwoodworth.knbt.Nbt
|
import net.benwoodworth.knbt.Nbt
|
||||||
import net.benwoodworth.knbt.NbtCompound
|
import net.benwoodworth.knbt.NbtCompound
|
||||||
import net.benwoodworth.knbt.NbtCompression
|
import net.benwoodworth.knbt.NbtCompression
|
||||||
import net.benwoodworth.knbt.NbtTag
|
|
||||||
import net.benwoodworth.knbt.NbtVariant
|
import net.benwoodworth.knbt.NbtVariant
|
||||||
import net.benwoodworth.knbt.decodeFromStream
|
import net.benwoodworth.knbt.decodeFromStream
|
||||||
import net.benwoodworth.knbt.nbtCompound
|
import net.benwoodworth.knbt.nbtCompound
|
||||||
|
|
@ -27,19 +28,14 @@ class WorldDetector(
|
||||||
compression = NbtCompression.Gzip
|
compression = NbtCompression.Gzip
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO not at once
|
fun listWorlds(): Flow<World> {
|
||||||
/**
|
return flow {
|
||||||
* List worlds in the savesDirectory
|
savesDirectory.listFiles().mapNotNull {
|
||||||
*
|
try {
|
||||||
* @return The worlds
|
emit(getWorldFromDirectory(it))
|
||||||
*/
|
} catch (e: InvalidWorldException) {
|
||||||
fun listWorlds(): List<World> {
|
Log.i(TAG, "${it.name} is invalid: ${e.message}")
|
||||||
return savesDirectory.listFiles().mapNotNull {
|
}
|
||||||
try {
|
|
||||||
getWorldFromDirectory(it)
|
|
||||||
} catch (e: InvalidWorldException) {
|
|
||||||
Log.i(TAG, "${it.name} is invalid: ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<World>? = 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<World> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Settings> {
|
||||||
|
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 */)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
@ -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<Settings>
|
||||||
|
) {
|
||||||
|
fun getSettingsFlow(): Flow<Settings> {
|
||||||
|
return dataStore.data
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getSettings(): Settings {
|
||||||
|
return dataStore.data.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateWorldOrder(worldOrder: WorldOrder) {
|
||||||
|
dataStore.updateData {
|
||||||
|
it.toBuilder()
|
||||||
|
.clearWorldOrder()
|
||||||
|
.setWorldOrder(worldOrder)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Settings> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,24 @@
|
||||||
package eu.m724.pojavbackup.home
|
package eu.m724.pojavbackup.home
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
|
@ -26,76 +29,126 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import eu.m724.pojavbackup.R
|
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.dashboard.DashboardScreen
|
||||||
import eu.m724.pojavbackup.home.screen.history.HistoryScreen
|
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
|
import eu.m724.pojavbackup.ui.theme.PojavBackupTheme
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class HomeActivity : ComponentActivity() {
|
class HomeActivity : ComponentActivity() {
|
||||||
|
private val viewModel: HomeViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
val navController = rememberNavController()
|
|
||||||
|
|
||||||
PojavBackupTheme {
|
PojavBackupTheme {
|
||||||
Scaffold(
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
bottomBar = {
|
if (uiState.loading) {
|
||||||
NavigationBar {
|
Column(
|
||||||
ScreenNavigationBarItem(
|
modifier = Modifier.fillMaxSize(),
|
||||||
navController = navController,
|
verticalArrangement = Arrangement.Center,
|
||||||
label = "Dashboard",
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
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 })
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
composable<Screen.Dashboard> {
|
CircularProgressIndicator()
|
||||||
DashboardScreen(navController)
|
|
||||||
}
|
|
||||||
composable<Screen.History> {
|
|
||||||
HistoryScreen()
|
|
||||||
}
|
|
||||||
// Add more destinations similarly.
|
|
||||||
}
|
}
|
||||||
|
} 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<HomeScreen.Dashboard> {
|
||||||
|
DashboardScreen(
|
||||||
|
navController = navController,
|
||||||
|
onWorldsIncludedClick = {
|
||||||
|
onSettingsOpen("content")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable<HomeScreen.History> {
|
||||||
|
HistoryScreen()
|
||||||
|
}
|
||||||
|
// Add more destinations similarly.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RowScope.ScreenNavigationBarItem(
|
fun RowScope.ScreenNavigationBarItem(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
label: String,
|
label: String,
|
||||||
route: Screen,
|
route: HomeScreen,
|
||||||
iconResourceId: Int
|
iconResourceId: Int
|
||||||
) {
|
) {
|
||||||
val selected = isSelected(navController, route)
|
val selected = isSelected(navController, route)
|
||||||
|
|
@ -122,7 +175,7 @@ fun RowScope.ScreenNavigationBarItem(
|
||||||
@Composable
|
@Composable
|
||||||
fun isSelected(
|
fun isSelected(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
route: Screen
|
route: HomeScreen
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentDestination = navBackStackEntry?.destination
|
val currentDestination = navBackStackEntry?.destination
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package eu.m724.pojavbackup.home
|
||||||
|
|
||||||
|
data class HomeUiState(
|
||||||
|
val loading: Boolean = true
|
||||||
|
)
|
||||||
|
|
@ -1,4 +1,87 @@
|
||||||
package eu.m724.pojavbackup.home
|
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<HomeUiState> = _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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.m724.pojavbackup.home.screen
|
package eu.m724.pojavbackup.home.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
|
@ -11,9 +10,9 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable sealed interface Screen {
|
@Serializable sealed interface HomeScreen {
|
||||||
@Serializable data object Dashboard : Screen
|
@Serializable data object Dashboard : HomeScreen
|
||||||
@Serializable data object History : Screen
|
@Serializable data object History : HomeScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -9,42 +9,43 @@ import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
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.ElevatedCard
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.m724.pojavbackup.R
|
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
|
import eu.m724.pojavbackup.home.screen.ScreenColumn
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardScreen(
|
fun DashboardScreen(
|
||||||
navController: NavController
|
navController: NavController,
|
||||||
|
onWorldsIncludedClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val viewModel: DashboardScreenViewModel = hiltViewModel()
|
||||||
|
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
ScreenColumn {
|
ScreenColumn {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
maxItemsInEachRow = 3
|
maxItemsInEachRow = 3
|
||||||
) {
|
) {
|
||||||
DashboardCard(
|
DashboardCard(
|
||||||
title = "Worlds included",
|
title = "Worlds included",
|
||||||
value = "1",
|
value = settings.worldOrder.separatorIndex,
|
||||||
iconResourceId = R.drawable.baseline_mosque_24,
|
iconResourceId = R.drawable.baseline_mosque_24,
|
||||||
onClick = {
|
onClick = onWorldsIncludedClick
|
||||||
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
DashboardCard(
|
DashboardCard(
|
||||||
|
|
@ -58,7 +59,7 @@ fun DashboardScreen(
|
||||||
value = "1d ago",
|
value = "1d ago",
|
||||||
iconResourceId = R.drawable.baseline_access_time_filled_24,
|
iconResourceId = R.drawable.baseline_access_time_filled_24,
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate(Screen.History)
|
navController.navigate(HomeScreen.History)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>(Settings.getDefaultInstance())
|
||||||
|
val settings: StateFlow<Settings> = _settings.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
settingsRepository.getSettingsFlow().collect { newSettings ->
|
||||||
|
_settings.update { newSettings }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SettingsScreen.Options> {
|
||||||
|
OptionsScreen(navController)
|
||||||
|
}
|
||||||
|
composable<SettingsScreen.Content> {
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<List<World>>(emptyList())
|
||||||
|
val worlds: StateFlow<List<World>> = _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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
package eu.m724.pojavbackup.setup
|
package eu.m724.pojavbackup.setup
|
||||||
|
|
||||||
import android.R.attr.end
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
|
@ -10,62 +8,32 @@ import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
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.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CardElevation
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.m724.pojavbackup.ui.theme.PojavBackupTheme
|
import eu.m724.pojavbackup.ui.theme.PojavBackupTheme
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import androidx.navigation.compose.NavHost
|
|
||||||
import eu.m724.pojavbackup.R
|
|
||||||
import eu.m724.pojavbackup.home.HomeActivity
|
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 viewModel: SetupViewModel by viewModels()
|
||||||
|
|
||||||
private val openDocumentTree = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
|
private val openDocumentTree = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
|
||||||
|
|
@ -126,8 +94,6 @@ fun SetupScreen(
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val worldsState by viewModel.worlds.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,26 +1,17 @@
|
||||||
package eu.m724.pojavbackup.setup
|
package eu.m724.pojavbackup.setup
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.ViewModel
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class SetupViewModel : ViewModel() {
|
class SetupViewModel : ViewModel() {
|
||||||
private val TAG: String = javaClass.name
|
private val TAG: String = javaClass.name
|
||||||
|
|
@ -33,9 +24,6 @@ class SetupViewModel : ViewModel() {
|
||||||
private val _uiState = MutableStateFlow(SetupUiState())
|
private val _uiState = MutableStateFlow(SetupUiState())
|
||||||
val uiState: StateFlow<SetupUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<SetupUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private val _worlds = MutableStateFlow<List<World>>(emptyList())
|
|
||||||
val worlds: StateFlow<List<World>> = _worlds.asStateFlow()
|
|
||||||
|
|
||||||
// TODO we could make the check call separate and not pass context here
|
// TODO we could make the check call separate and not pass context here
|
||||||
fun onOpenDocumentTree(context: Context, uri: Uri?, result: (Boolean) -> Unit) {
|
fun onOpenDocumentTree(context: Context, uri: Uri?, result: (Boolean) -> Unit) {
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
|
|
@ -79,45 +67,9 @@ class SetupViewModel : ViewModel() {
|
||||||
|
|
||||||
Log.i(TAG, "Yes we have permission")
|
Log.i(TAG, "Yes we have permission")
|
||||||
|
|
||||||
// TODO remove
|
|
||||||
listWorlds(context, directory)
|
|
||||||
|
|
||||||
return true
|
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<String> {
|
fun detectInstalledLauncherPackage(packageManager: PackageManager): List<String> {
|
||||||
return POJAV_PACKAGES.filter {
|
return POJAV_PACKAGES.filter {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.m724.pojavbackup.ui.theme
|
package eu.m724.pojavbackup.ui.theme
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -11,48 +10,23 @@ import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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
|
@Composable
|
||||||
fun PojavBackupTheme(
|
fun PojavBackupTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
||||||
// Dynamic color is available on Android 12+
|
|
||||||
dynamicColor: Boolean = true,
|
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
val darkTheme = isSystemInDarkTheme()
|
||||||
val context = LocalContext.current
|
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
val colorScheme = when {
|
||||||
else -> LightColorScheme
|
dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
|
||||||
|
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
|
||||||
|
|
||||||
|
darkTheme -> darkColorScheme()
|
||||||
|
else -> lightColorScheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography,
|
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
)
|
|
||||||
14
app/src/main/proto/settings.proto
Normal file
14
app/src/main/proto/settings.proto
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
7
app/src/main/res/drawable/baseline_folder_copy_24.xml
Normal file
7
app/src/main/res/drawable/baseline_folder_copy_24.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<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="M3,6H1v13c0,1.1 0.9,2 2,2h17v-2H3V6z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M21,4h-7l-2,-2H7C5.9,2 5.01,2.9 5.01,4L5,15c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V6C23,4.9 22.1,4 21,4z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/baseline_settings_24.xml
Normal file
5
app/src/main/res/drawable/baseline_settings_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.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">PojavBackup</string>
|
<string name="app_name">PojavBackup</string>
|
||||||
<string name="title_activity_main">MainActivity</string>
|
<string name="title_activity_setup">SetupActivity</string>
|
||||||
<string name="title_activity_home">HomeActivity</string>
|
<string name="title_activity_home">HomeActivity</string>
|
||||||
|
<string name="title_activity_settings">SettingsActivity</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -4,4 +4,7 @@ plugins {
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
alias(libs.plugins.kotlin.serialization) 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
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[versions]
|
[versions]
|
||||||
agp = "8.9.1"
|
agp = "8.9.1"
|
||||||
kotlin = "2.0.21"
|
kotlin = "2.1.20"
|
||||||
coreKtx = "1.16.0"
|
coreKtx = "1.16.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
|
|
@ -14,6 +14,12 @@ lifecycleViewmodelCompose = "2.8.7"
|
||||||
knbt = "0.11.8"
|
knbt = "0.11.8"
|
||||||
reorderable = "0.9.6"
|
reorderable = "0.9.6"
|
||||||
navigation = "2.8.9"
|
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]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
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" }
|
knbt = { group = "net.benwoodworth.knbt", name = "knbt", version.ref = "knbt" }
|
||||||
reorderable = { group = "org.burnoutcrew.composereorderable", name = "reorderable", version.ref = "reorderable" }
|
reorderable = { group = "org.burnoutcrew.composereorderable", name = "reorderable", version.ref = "reorderable" }
|
||||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
|
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]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", 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"}
|
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" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue