diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 495c3ea..9e188b2 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose) // v1.5.0: Jetpack Compose Compiler
// β‘ v1.3.1: ktlint deaktiviert wegen Parser-Problemen, aktivieren in v1.4.0
// alias(libs.plugins.ktlint)
alias(libs.plugins.detekt)
@@ -20,8 +21,8 @@ android {
applicationId = "dev.dettmer.simplenotes"
minSdk = 24
targetSdk = 36
- versionCode = 12 // π§ v1.4.1: Bugfixes (Root-Delete, Checklist Compat)
- versionName = "1.4.1" // π§ v1.4.1: Root-Folder Delete Fix, Checklisten-Sync AbwΓ€rtskompatibilitΓ€t
+ versionCode = 13 // π§ v1.5.0: Jetpack Compose Settings Redesign
+ versionName = "1.5.0" // π§ v1.5.0: Jetpack Compose Settings Redesign
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -96,6 +97,7 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true // Enable BuildConfig generation
+ compose = true // v1.5.0: Jetpack Compose fΓΌr Settings Redesign
}
compileOptions {
@@ -135,6 +137,20 @@ dependencies {
// SwipeRefreshLayout fΓΌr Pull-to-Refresh
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // v1.5.0: Jetpack Compose fΓΌr Settings Redesign
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.graphics)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.compose.material3)
+ implementation(libs.androidx.compose.material.icons)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.lifecycle.runtime.compose)
+ debugImplementation(libs.androidx.compose.ui.tooling)
+
// Testing (bleiben so)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index be931a7..0231cc6 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -43,11 +43,17 @@
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".MainActivity" />
-
+
+
+
+
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ SettingsNavHost(
+ navController = navController,
+ viewModel = viewModel,
+ onFinish = {
+ setResult(RESULT_OK)
+ finish()
+ }
+ )
+ }
+ }
+ }
+
+ /**
+ * Collect events from ViewModel for Activity-level actions
+ * v1.5.0: Ported from old SettingsActivity
+ */
+ private fun collectViewModelEvents() {
+ lifecycleScope.launch {
+ viewModel.events.collect { event ->
+ when (event) {
+ is SettingsViewModel.SettingsEvent.RequestBatteryOptimization -> {
+ checkBatteryOptimization()
+ }
+ is SettingsViewModel.SettingsEvent.RestartNetworkMonitor -> {
+ restartNetworkMonitor()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if battery optimization is disabled for this app
+ * v1.5.0: Ported from old SettingsActivity
+ */
+ private fun checkBatteryOptimization() {
+ val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
+
+ if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
+ showBatteryOptimizationDialog()
+ }
+ }
+
+ /**
+ * Show dialog asking user to disable battery optimization
+ * v1.5.0: Ported from old SettingsActivity
+ */
+ private fun showBatteryOptimizationDialog() {
+ AlertDialog.Builder(this)
+ .setTitle("Hintergrund-Synchronisation")
+ .setMessage(
+ "Damit die App im Hintergrund synchronisieren kann, " +
+ "muss die Akku-Optimierung deaktiviert werden.\n\n" +
+ "Bitte wΓ€hle 'Nicht optimieren' fΓΌr Simple Notes."
+ )
+ .setPositiveButton("Einstellungen ΓΆffnen") { _, _ ->
+ openBatteryOptimizationSettings()
+ }
+ .setNegativeButton("SpΓ€ter") { dialog, _ ->
+ dialog.dismiss()
+ }
+ .setCancelable(false)
+ .show()
+ }
+
+ /**
+ * Open system battery optimization settings
+ * v1.5.0: Ported from old SettingsActivity
+ */
+ private fun openBatteryOptimizationSettings() {
+ try {
+ val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
+ intent.data = Uri.parse("package:$packageName")
+ startActivity(intent)
+ } catch (e: Exception) {
+ Logger.w(TAG, "Failed to open battery optimization settings: ${e.message}")
+ // Fallback: Open general battery settings
+ try {
+ val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
+ startActivity(intent)
+ } catch (e2: Exception) {
+ Logger.w(TAG, "Failed to open fallback battery settings: ${e2.message}")
+ Toast.makeText(this, "Bitte Akku-Optimierung manuell deaktivieren", Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ /**
+ * Restart the network monitor after sync settings change
+ * v1.5.0: Ported from old SettingsActivity
+ */
+ private fun restartNetworkMonitor() {
+ try {
+ val app = application as SimpleNotesApplication
+ Logger.d(TAG, "π Restarting NetworkMonitor with new settings")
+ app.networkMonitor.stopMonitoring()
+ app.networkMonitor.startMonitoring()
+ Logger.d(TAG, "β
NetworkMonitor restarted successfully")
+ } catch (e: Exception) {
+ Logger.e(TAG, "β Failed to restart NetworkMonitor: ${e.message}")
+ }
+ }
+}
+
+/**
+ * Material 3 Theme with Dynamic Colors support
+ */
+@Composable
+fun SimpleNotesSettingsTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val context = LocalContext.current
+
+ val colorScheme = when {
+ // Dynamic colors are available on Android 12+
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ if (darkTheme) {
+ dynamicDarkColorScheme(context)
+ } else {
+ dynamicLightColorScheme(context)
+ }
+ }
+ // Fallback to static Material 3 colors
+ darkTheme -> darkColorScheme()
+ else -> lightColorScheme()
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ content = content
+ )
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsNavigation.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsNavigation.kt
new file mode 100644
index 0000000..2b8f5d9
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsNavigation.kt
@@ -0,0 +1,85 @@
+package dev.dettmer.simplenotes.ui.settings
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import dev.dettmer.simplenotes.ui.settings.screens.AboutScreen
+import dev.dettmer.simplenotes.ui.settings.screens.BackupSettingsScreen
+import dev.dettmer.simplenotes.ui.settings.screens.DebugSettingsScreen
+import dev.dettmer.simplenotes.ui.settings.screens.MarkdownSettingsScreen
+import dev.dettmer.simplenotes.ui.settings.screens.ServerSettingsScreen
+import dev.dettmer.simplenotes.ui.settings.screens.SettingsMainScreen
+import dev.dettmer.simplenotes.ui.settings.screens.SyncSettingsScreen
+
+/**
+ * Settings navigation host with all routes
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun SettingsNavHost(
+ navController: NavHostController,
+ viewModel: SettingsViewModel,
+ onFinish: () -> Unit
+) {
+ NavHost(
+ navController = navController,
+ startDestination = SettingsRoute.Main.route
+ ) {
+ // Main Settings Overview
+ composable(SettingsRoute.Main.route) {
+ SettingsMainScreen(
+ viewModel = viewModel,
+ onNavigate = { route -> navController.navigate(route.route) },
+ onBack = onFinish
+ )
+ }
+
+ // Server Settings
+ composable(SettingsRoute.Server.route) {
+ ServerSettingsScreen(
+ viewModel = viewModel,
+ onBack = { navController.popBackStack() }
+ )
+ }
+
+ // Sync Settings
+ composable(SettingsRoute.Sync.route) {
+ SyncSettingsScreen(
+ viewModel = viewModel,
+ onBack = { navController.popBackStack() }
+ )
+ }
+
+ // Markdown Settings
+ composable(SettingsRoute.Markdown.route) {
+ MarkdownSettingsScreen(
+ viewModel = viewModel,
+ onBack = { navController.popBackStack() }
+ )
+ }
+
+ // Backup Settings
+ composable(SettingsRoute.Backup.route) {
+ BackupSettingsScreen(
+ viewModel = viewModel,
+ onBack = { navController.popBackStack() }
+ )
+ }
+
+ // About Screen
+ composable(SettingsRoute.About.route) {
+ AboutScreen(
+ onBack = { navController.popBackStack() }
+ )
+ }
+
+ // Debug Settings
+ composable(SettingsRoute.Debug.route) {
+ DebugSettingsScreen(
+ viewModel = viewModel,
+ onBack = { navController.popBackStack() }
+ )
+ }
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsRoute.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsRoute.kt
new file mode 100644
index 0000000..3ce1c25
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsRoute.kt
@@ -0,0 +1,15 @@
+package dev.dettmer.simplenotes.ui.settings
+
+/**
+ * Navigation routes for Settings screens
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+sealed class SettingsRoute(val route: String) {
+ data object Main : SettingsRoute("settings_main")
+ data object Server : SettingsRoute("settings_server")
+ data object Sync : SettingsRoute("settings_sync")
+ data object Markdown : SettingsRoute("settings_markdown")
+ data object Backup : SettingsRoute("settings_backup")
+ data object About : SettingsRoute("settings_about")
+ data object Debug : SettingsRoute("settings_debug")
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt
new file mode 100644
index 0000000..533240c
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt
@@ -0,0 +1,493 @@
+package dev.dettmer.simplenotes.ui.settings
+
+import android.app.Application
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import dev.dettmer.simplenotes.backup.BackupManager
+import dev.dettmer.simplenotes.backup.RestoreMode
+import dev.dettmer.simplenotes.sync.WebDavSyncService
+import dev.dettmer.simplenotes.utils.Constants
+import dev.dettmer.simplenotes.utils.Logger
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.net.HttpURLConnection
+import java.net.URL
+
+/**
+ * ViewModel for Settings screens
+ * v1.5.0: Jetpack Compose Settings Redesign
+ *
+ * Manages all settings state and actions across the Settings navigation graph.
+ */
+class SettingsViewModel(application: Application) : AndroidViewModel(application) {
+
+ companion object {
+ private const val TAG = "SettingsViewModel"
+ private const val CONNECTION_TIMEOUT_MS = 3000
+ }
+
+ private val prefs = application.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
+ val backupManager = BackupManager(application)
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Server Settings State
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ // v1.5.0 Fix: Initialize URL with protocol prefix if empty
+ private val storedUrl = prefs.getString(Constants.KEY_SERVER_URL, "") ?: ""
+ private val initialUrl = if (storedUrl.isEmpty()) "http://" else storedUrl
+
+ private val _serverUrl = MutableStateFlow(initialUrl)
+ val serverUrl: StateFlow = _serverUrl.asStateFlow()
+
+ private val _username = MutableStateFlow(prefs.getString(Constants.KEY_USERNAME, "") ?: "")
+ val username: StateFlow = _username.asStateFlow()
+
+ private val _password = MutableStateFlow(prefs.getString(Constants.KEY_PASSWORD, "") ?: "")
+ val password: StateFlow = _password.asStateFlow()
+
+ // v1.5.0 Fix: isHttps based on stored URL (false = HTTP if empty)
+ private val _isHttps = MutableStateFlow(storedUrl.startsWith("https://"))
+ val isHttps: StateFlow = _isHttps.asStateFlow()
+
+ private val _serverStatus = MutableStateFlow(ServerStatus.Unknown)
+ val serverStatus: StateFlow = _serverStatus.asStateFlow()
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Events (for Activity-level actions like dialogs, intents)
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ private val _events = MutableSharedFlow()
+ val events: SharedFlow = _events.asSharedFlow()
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Markdown Export Progress State
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ private val _markdownExportProgress = MutableStateFlow(null)
+ val markdownExportProgress: StateFlow = _markdownExportProgress.asStateFlow()
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Sync Settings State
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ private val _autoSyncEnabled = MutableStateFlow(prefs.getBoolean(Constants.KEY_AUTO_SYNC, false))
+ val autoSyncEnabled: StateFlow = _autoSyncEnabled.asStateFlow()
+
+ private val _syncInterval = MutableStateFlow(
+ prefs.getLong(Constants.PREF_SYNC_INTERVAL_MINUTES, Constants.DEFAULT_SYNC_INTERVAL_MINUTES)
+ )
+ val syncInterval: StateFlow = _syncInterval.asStateFlow()
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Markdown Settings State
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ private val _markdownAutoSync = MutableStateFlow(
+ prefs.getBoolean(Constants.KEY_MARKDOWN_EXPORT, false) &&
+ prefs.getBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, false)
+ )
+ val markdownAutoSync: StateFlow = _markdownAutoSync.asStateFlow()
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Debug Settings State
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ private val _fileLoggingEnabled = MutableStateFlow(
+ prefs.getBoolean(Constants.KEY_FILE_LOGGING_ENABLED, false)
+ )
+ val fileLoggingEnabled: StateFlow = _fileLoggingEnabled.asStateFlow()
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // UI State
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ private val _isSyncing = MutableStateFlow(false)
+ val isSyncing: StateFlow = _isSyncing.asStateFlow()
+
+ private val _isBackupInProgress = MutableStateFlow(false)
+ val isBackupInProgress: StateFlow = _isBackupInProgress.asStateFlow()
+
+ private val _showToast = MutableSharedFlow()
+ val showToast: SharedFlow = _showToast.asSharedFlow()
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Server Settings Actions
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ fun updateServerUrl(url: String) {
+ _serverUrl.value = url
+ saveServerSettings()
+ }
+
+ fun updateProtocol(useHttps: Boolean) {
+ _isHttps.value = useHttps
+ val currentUrl = _serverUrl.value
+
+ // v1.5.0 Fix: Automatisch Prefix setzen, auch bei leerem Feld
+ val newUrl = if (useHttps) {
+ when {
+ currentUrl.isEmpty() || currentUrl == "http://" -> "https://"
+ currentUrl.startsWith("http://") -> currentUrl.replace("http://", "https://")
+ !currentUrl.startsWith("https://") -> "https://$currentUrl"
+ else -> currentUrl
+ }
+ } else {
+ when {
+ currentUrl.isEmpty() || currentUrl == "https://" -> "http://"
+ currentUrl.startsWith("https://") -> currentUrl.replace("https://", "http://")
+ !currentUrl.startsWith("http://") -> "http://$currentUrl"
+ else -> currentUrl
+ }
+ }
+ _serverUrl.value = newUrl
+ saveServerSettings()
+ }
+
+ fun updateUsername(value: String) {
+ _username.value = value
+ saveServerSettings()
+ }
+
+ fun updatePassword(value: String) {
+ _password.value = value
+ saveServerSettings()
+ }
+
+ private fun saveServerSettings() {
+ prefs.edit().apply {
+ putString(Constants.KEY_SERVER_URL, _serverUrl.value)
+ putString(Constants.KEY_USERNAME, _username.value)
+ putString(Constants.KEY_PASSWORD, _password.value)
+ apply()
+ }
+ }
+
+ fun testConnection() {
+ viewModelScope.launch {
+ _serverStatus.value = ServerStatus.Checking
+ try {
+ val syncService = WebDavSyncService(getApplication())
+ val result = syncService.testConnection()
+ _serverStatus.value = if (result.isSuccess) {
+ ServerStatus.Reachable
+ } else {
+ ServerStatus.Unreachable(result.errorMessage)
+ }
+ emitToast(if (result.isSuccess) "β
Verbindung erfolgreich!" else "β ${result.errorMessage}")
+ } catch (e: Exception) {
+ _serverStatus.value = ServerStatus.Unreachable(e.message)
+ emitToast("β Fehler: ${e.message}")
+ }
+ }
+ }
+
+ fun checkServerStatus() {
+ val serverUrl = _serverUrl.value
+ // v1.5.0 Fix: URL mit nur Prefix gilt als "nicht konfiguriert"
+ if (serverUrl.isEmpty() || serverUrl == "http://" || serverUrl == "https://") {
+ _serverStatus.value = ServerStatus.NotConfigured
+ return
+ }
+
+ viewModelScope.launch {
+ _serverStatus.value = ServerStatus.Checking
+ val isReachable = withContext(Dispatchers.IO) {
+ try {
+ val url = URL(serverUrl)
+ val connection = url.openConnection() as HttpURLConnection
+ connection.connectTimeout = CONNECTION_TIMEOUT_MS
+ connection.readTimeout = CONNECTION_TIMEOUT_MS
+ val code = connection.responseCode
+ connection.disconnect()
+ code in 200..299 || code == 401
+ } catch (e: Exception) {
+ Log.e(TAG, "Server check failed: ${e.message}")
+ false
+ }
+ }
+ _serverStatus.value = if (isReachable) ServerStatus.Reachable else ServerStatus.Unreachable(null)
+ }
+ }
+
+ fun syncNow() {
+ if (_isSyncing.value) return
+ viewModelScope.launch {
+ _isSyncing.value = true
+ try {
+ emitToast("π Synchronisiere...")
+ val syncService = WebDavSyncService(getApplication())
+
+ if (!syncService.hasUnsyncedChanges()) {
+ emitToast("β
Bereits synchronisiert")
+ return@launch
+ }
+
+ val result = syncService.syncNotes()
+ if (result.isSuccess) {
+ emitToast("β
${result.syncedCount} Notizen synchronisiert")
+ } else {
+ emitToast("β ${result.errorMessage}")
+ }
+ } catch (e: Exception) {
+ emitToast("β Fehler: ${e.message}")
+ } finally {
+ _isSyncing.value = false
+ }
+ }
+ }
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Sync Settings Actions
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ fun setAutoSync(enabled: Boolean) {
+ _autoSyncEnabled.value = enabled
+ prefs.edit().putBoolean(Constants.KEY_AUTO_SYNC, enabled).apply()
+
+ viewModelScope.launch {
+ if (enabled) {
+ // v1.5.0 Fix: Trigger battery optimization check and network monitor restart
+ _events.emit(SettingsEvent.RequestBatteryOptimization)
+ _events.emit(SettingsEvent.RestartNetworkMonitor)
+ emitToast("β
Auto-Sync aktiviert")
+ } else {
+ _events.emit(SettingsEvent.RestartNetworkMonitor)
+ emitToast("Auto-Sync deaktiviert")
+ }
+ }
+ }
+
+ fun setSyncInterval(minutes: Long) {
+ _syncInterval.value = minutes
+ prefs.edit().putLong(Constants.PREF_SYNC_INTERVAL_MINUTES, minutes).apply()
+ viewModelScope.launch {
+ val text = when (minutes) {
+ 15L -> "15 Minuten"
+ 60L -> "60 Minuten"
+ else -> "30 Minuten"
+ }
+ emitToast("β±οΈ Sync-Intervall: $text")
+ }
+ }
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Markdown Settings Actions
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ fun setMarkdownAutoSync(enabled: Boolean) {
+ if (enabled) {
+ // v1.5.0 Fix: Perform initial export when enabling (like old SettingsActivity)
+ viewModelScope.launch {
+ try {
+ // Check server configuration first
+ val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, "") ?: ""
+ val username = prefs.getString(Constants.KEY_USERNAME, "") ?: ""
+ val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: ""
+
+ if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) {
+ emitToast("β οΈ Bitte zuerst WebDAV-Server konfigurieren")
+ // Don't enable - revert state
+ return@launch
+ }
+
+ // Check if there are notes to export
+ val noteStorage = dev.dettmer.simplenotes.storage.NotesStorage(getApplication())
+ val noteCount = noteStorage.loadAllNotes().size
+
+ if (noteCount > 0) {
+ // Show progress and perform initial export
+ _markdownExportProgress.value = MarkdownExportProgress(0, noteCount)
+
+ val syncService = WebDavSyncService(getApplication())
+ val exportedCount = withContext(Dispatchers.IO) {
+ syncService.exportAllNotesToMarkdown(
+ serverUrl = serverUrl,
+ username = username,
+ password = password,
+ onProgress = { current, total ->
+ _markdownExportProgress.value = MarkdownExportProgress(current, total)
+ }
+ )
+ }
+
+ // Export successful - save settings
+ _markdownAutoSync.value = true
+ prefs.edit()
+ .putBoolean(Constants.KEY_MARKDOWN_EXPORT, true)
+ .putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, true)
+ .apply()
+
+ _markdownExportProgress.value = MarkdownExportProgress(noteCount, noteCount, isComplete = true)
+ emitToast("β
$exportedCount Notizen nach Markdown exportiert")
+
+ // Clear progress after short delay
+ kotlinx.coroutines.delay(500)
+ _markdownExportProgress.value = null
+
+ } else {
+ // No notes - just enable the feature
+ _markdownAutoSync.value = true
+ prefs.edit()
+ .putBoolean(Constants.KEY_MARKDOWN_EXPORT, true)
+ .putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, true)
+ .apply()
+ emitToast("π Markdown Auto-Sync aktiviert")
+ }
+
+ } catch (e: Exception) {
+ _markdownExportProgress.value = null
+ emitToast("β Export fehlgeschlagen: ${e.message}")
+ // Don't enable on error
+ }
+ }
+ } else {
+ // Disable - simple
+ _markdownAutoSync.value = false
+ prefs.edit()
+ .putBoolean(Constants.KEY_MARKDOWN_EXPORT, false)
+ .putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, false)
+ .apply()
+ viewModelScope.launch {
+ emitToast("π Markdown Auto-Sync deaktiviert")
+ }
+ }
+ }
+
+ fun performManualMarkdownSync() {
+ viewModelScope.launch {
+ try {
+ emitToast("π Markdown-Sync lΓ€uft...")
+ val syncService = WebDavSyncService(getApplication())
+ val result = syncService.manualMarkdownSync()
+ emitToast("β
Export: ${result.exportedCount} β’ Import: ${result.importedCount}")
+ } catch (e: Exception) {
+ emitToast("β Fehler: ${e.message}")
+ }
+ }
+ }
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Backup Actions
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ fun createBackup(uri: Uri) {
+ viewModelScope.launch {
+ _isBackupInProgress.value = true
+ try {
+ val result = backupManager.createBackup(uri)
+ emitToast(if (result.success) "β
${result.message}" else "β ${result.error}")
+ } catch (e: Exception) {
+ emitToast("β Backup fehlgeschlagen: ${e.message}")
+ } finally {
+ _isBackupInProgress.value = false
+ }
+ }
+ }
+
+ fun restoreFromFile(uri: Uri, mode: RestoreMode) {
+ viewModelScope.launch {
+ _isBackupInProgress.value = true
+ try {
+ val result = backupManager.restoreBackup(uri, mode)
+ emitToast(if (result.success) "β
${result.importedNotes} Notizen wiederhergestellt" else "β ${result.error}")
+ } catch (e: Exception) {
+ emitToast("β Wiederherstellung fehlgeschlagen: ${e.message}")
+ } finally {
+ _isBackupInProgress.value = false
+ }
+ }
+ }
+
+ fun restoreFromServer(mode: RestoreMode) {
+ viewModelScope.launch {
+ _isBackupInProgress.value = true
+ try {
+ emitToast("π₯ Lade vom Server...")
+ val syncService = WebDavSyncService(getApplication())
+ val result = withContext(Dispatchers.IO) {
+ syncService.restoreFromServer(mode)
+ }
+ emitToast(if (result.isSuccess) "β
${result.restoredCount} Notizen wiederhergestellt" else "β ${result.errorMessage}")
+ } catch (e: Exception) {
+ emitToast("β Fehler: ${e.message}")
+ } finally {
+ _isBackupInProgress.value = false
+ }
+ }
+ }
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Debug Settings Actions
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ fun setFileLogging(enabled: Boolean) {
+ _fileLoggingEnabled.value = enabled
+ prefs.edit().putBoolean(Constants.KEY_FILE_LOGGING_ENABLED, enabled).apply()
+ Logger.setFileLoggingEnabled(enabled)
+ viewModelScope.launch {
+ emitToast(if (enabled) "π Datei-Logging aktiviert" else "π Datei-Logging deaktiviert")
+ }
+ }
+
+ fun clearLogs() {
+ viewModelScope.launch {
+ try {
+ val cleared = Logger.clearLogFile(getApplication())
+ emitToast(if (cleared) "ποΈ Logs gelΓΆscht" else "π Keine Logs zum LΓΆschen")
+ } catch (e: Exception) {
+ emitToast("β Fehler: ${e.message}")
+ }
+ }
+ }
+
+ fun getLogFile() = Logger.getLogFile(getApplication())
+
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ // Helper
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ private suspend fun emitToast(message: String) {
+ _showToast.emit(message)
+ }
+
+ /**
+ * Server status states
+ */
+ sealed class ServerStatus {
+ data object Unknown : ServerStatus()
+ data object NotConfigured : ServerStatus()
+ data object Checking : ServerStatus()
+ data object Reachable : ServerStatus()
+ data class Unreachable(val error: String?) : ServerStatus()
+ }
+
+ /**
+ * Events for Activity-level actions (dialogs, intents, etc.)
+ * v1.5.0: Ported from old SettingsActivity
+ */
+ sealed class SettingsEvent {
+ data object RequestBatteryOptimization : SettingsEvent()
+ data object RestartNetworkMonitor : SettingsEvent()
+ }
+
+ /**
+ * Progress state for Markdown export
+ * v1.5.0: For initial export progress dialog
+ */
+ data class MarkdownExportProgress(
+ val current: Int,
+ val total: Int,
+ val isComplete: Boolean = false
+ )
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsCard.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsCard.kt
new file mode 100644
index 0000000..7146bb9
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsCard.kt
@@ -0,0 +1,113 @@
+package dev.dettmer.simplenotes.ui.settings.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+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.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+
+/**
+ * Clickable Settings group card with icon, title, subtitle and optional status
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun SettingsCard(
+ icon: ImageVector,
+ title: String,
+ modifier: Modifier = Modifier,
+ subtitle: String? = null,
+ statusText: String? = null,
+ statusColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 4.dp)
+ .clickable(onClick = onClick),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Icon with circle background
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .background(
+ color = MaterialTheme.colorScheme.primaryContainer,
+ shape = CircleShape
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ // Content
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ if (subtitle != null) {
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ if (statusText != null) {
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = statusText,
+ style = MaterialTheme.typography.bodySmall,
+ color = statusColor
+ )
+ }
+ }
+
+ // Arrow
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsComponents.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsComponents.kt
new file mode 100644
index 0000000..30f83ff
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsComponents.kt
@@ -0,0 +1,152 @@
+package dev.dettmer.simplenotes.ui.settings.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+/**
+ * Primary filled button for settings actions
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun SettingsButton(
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ isLoading: Boolean = false
+) {
+ Button(
+ onClick = onClick,
+ enabled = enabled && !isLoading,
+ modifier = modifier.fillMaxWidth()
+ ) {
+ if (isLoading) {
+ CircularProgressIndicator(
+ modifier = Modifier.height(20.dp),
+ strokeWidth = 2.dp,
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+ } else {
+ Text(text)
+ }
+ }
+}
+
+/**
+ * Outlined secondary button for settings actions
+ */
+@Composable
+fun SettingsOutlinedButton(
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ isLoading: Boolean = false
+) {
+ OutlinedButton(
+ onClick = onClick,
+ enabled = enabled && !isLoading,
+ modifier = modifier.fillMaxWidth()
+ ) {
+ if (isLoading) {
+ CircularProgressIndicator(
+ modifier = Modifier.height(20.dp),
+ strokeWidth = 2.dp,
+ color = MaterialTheme.colorScheme.primary
+ )
+ } else {
+ Text(text)
+ }
+ }
+}
+
+/**
+ * Danger/destructive button for settings actions
+ */
+@Composable
+fun SettingsDangerButton(
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true
+) {
+ OutlinedButton(
+ onClick = onClick,
+ enabled = enabled,
+ modifier = modifier.fillMaxWidth(),
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text(text)
+ }
+}
+
+/**
+ * Info card with description text
+ */
+@Composable
+fun SettingsInfoCard(
+ text: String,
+ modifier: Modifier = Modifier
+) {
+ androidx.compose.material3.Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ colors = androidx.compose.material3.CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
+ )
+ ) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(16.dp),
+ lineHeight = MaterialTheme.typography.bodySmall.lineHeight * 1.3f
+ )
+ }
+}
+
+/**
+ * Section header text
+ */
+@Composable
+fun SettingsSectionHeader(
+ text: String,
+ modifier: Modifier = Modifier
+) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+ )
+}
+
+/**
+ * Divider between settings groups
+ */
+@Composable
+fun SettingsDivider(
+ modifier: Modifier = Modifier
+) {
+ Spacer(modifier = modifier.height(8.dp))
+ androidx.compose.material3.HorizontalDivider(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ color = MaterialTheme.colorScheme.outlineVariant
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsRadioGroup.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsRadioGroup.kt
new file mode 100644
index 0000000..6fd267c
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsRadioGroup.kt
@@ -0,0 +1,94 @@
+package dev.dettmer.simplenotes.ui.settings.components
+
+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.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+
+/**
+ * Data class for radio option
+ */
+data class RadioOption(
+ val value: T,
+ val title: String,
+ val subtitle: String? = null
+)
+
+/**
+ * Settings radio group for selecting one option
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun SettingsRadioGroup(
+ options: List>,
+ selectedValue: T,
+ onValueSelected: (T) -> Unit,
+ modifier: Modifier = Modifier,
+ title: String? = null
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .selectableGroup()
+ ) {
+ if (title != null) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+ )
+ }
+
+ options.forEach { option ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .selectable(
+ selected = option.value == selectedValue,
+ onClick = { onValueSelected(option.value) },
+ role = Role.RadioButton
+ )
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = option.value == selectedValue,
+ onClick = null // handled by selectable
+ )
+
+ Column(
+ modifier = Modifier
+ .padding(start = 16.dp)
+ .weight(1f)
+ ) {
+ Text(
+ text = option.title,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ if (option.subtitle != null) {
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = option.subtitle,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsScaffold.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsScaffold.kt
new file mode 100644
index 0000000..a08b383
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsScaffold.kt
@@ -0,0 +1,57 @@
+package dev.dettmer.simplenotes.ui.settings.components
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+/**
+ * Reusable Scaffold with back-navigation TopAppBar
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsScaffold(
+ title: String,
+ onBack: () -> Unit,
+ modifier: Modifier = Modifier,
+ content: @Composable (PaddingValues) -> Unit
+) {
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleLarge
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "ZurΓΌck"
+ )
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surface,
+ titleContentColor = MaterialTheme.colorScheme.onSurface
+ )
+ )
+ },
+ containerColor = MaterialTheme.colorScheme.surface
+ ) { paddingValues ->
+ content(paddingValues)
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsSwitch.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsSwitch.kt
new file mode 100644
index 0000000..63cf998
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsSwitch.kt
@@ -0,0 +1,83 @@
+package dev.dettmer.simplenotes.ui.settings.components
+
+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.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+
+/**
+ * Settings switch item with title, optional subtitle and icon
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun SettingsSwitch(
+ title: String,
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ subtitle: String? = null,
+ icon: ImageVector? = null,
+ enabled: Boolean = true
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (icon != null) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = if (enabled) {
+ MaterialTheme.colorScheme.onSurfaceVariant
+ } else {
+ MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
+ },
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ }
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.bodyLarge,
+ color = if (enabled) {
+ MaterialTheme.colorScheme.onSurface
+ } else {
+ MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
+ }
+ )
+ if (subtitle != null) {
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ color = if (enabled) {
+ MaterialTheme.colorScheme.onSurfaceVariant
+ } else {
+ MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
+ }
+ )
+ }
+ }
+
+ Switch(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ enabled = enabled
+ )
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/AboutScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/AboutScreen.kt
new file mode 100644
index 0000000..c506c72
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/AboutScreen.kt
@@ -0,0 +1,218 @@
+package dev.dettmer.simplenotes.ui.settings.screens
+
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.foundation.clickable
+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.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
+import androidx.compose.material.icons.filled.Code
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material.icons.filled.Policy
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import dev.dettmer.simplenotes.BuildConfig
+import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
+import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
+import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader
+
+/**
+ * About app information screen
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun AboutScreen(
+ onBack: () -> Unit
+) {
+ val context = LocalContext.current
+
+ val githubRepoUrl = "https://github.com/inventory69/simple-notes-sync"
+ val githubProfileUrl = "https://github.com/inventory69"
+ val licenseUrl = "https://github.com/inventory69/simple-notes-sync/blob/main/LICENSE"
+
+ SettingsScaffold(
+ title = "Γber diese App",
+ onBack = onBack
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // App Info Card
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer
+ )
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "π",
+ style = MaterialTheme.typography.displayMedium
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = "Simple Notes Sync",
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ SettingsSectionHeader(text = "Links")
+
+ // GitHub Repository
+ AboutLinkItem(
+ icon = Icons.Default.Code,
+ title = "GitHub Repository",
+ subtitle = "Quellcode, Issues & Dokumentation",
+ onClick = {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubRepoUrl))
+ context.startActivity(intent)
+ }
+ )
+
+ // Developer
+ AboutLinkItem(
+ icon = Icons.Default.Person,
+ title = "Entwickler",
+ subtitle = "GitHub Profil: @inventory69",
+ onClick = {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubProfileUrl))
+ context.startActivity(intent)
+ }
+ )
+
+ // License
+ AboutLinkItem(
+ icon = Icons.Default.Policy,
+ title = "Lizenz",
+ subtitle = "MIT License - Open Source",
+ onClick = {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(licenseUrl))
+ context.startActivity(intent)
+ }
+ )
+
+ SettingsDivider()
+
+ // Data Privacy Info
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
+ )
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ Text(
+ text = "π Datenschutz",
+ style = MaterialTheme.typography.titleSmall
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = "Diese App sammelt keine Daten. Alle Notizen werden " +
+ "nur lokal auf deinem GerΓ€t und auf deinem eigenen " +
+ "WebDAV-Server gespeichert. Keine Telemetrie, keine Werbung.",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+}
+
+/**
+ * Clickable link item for About section
+ */
+@Composable
+private fun AboutLinkItem(
+ icon: ImageVector,
+ title: String,
+ subtitle: String,
+ onClick: () -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(24.dp)
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/BackupSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/BackupSettingsScreen.kt
new file mode 100644
index 0000000..e3ca5c2
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/BackupSettingsScreen.kt
@@ -0,0 +1,256 @@
+package dev.dettmer.simplenotes.ui.settings.screens
+
+import android.net.Uri
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import dev.dettmer.simplenotes.backup.RestoreMode
+import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
+import dev.dettmer.simplenotes.ui.settings.components.RadioOption
+import dev.dettmer.simplenotes.ui.settings.components.SettingsButton
+import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
+import dev.dettmer.simplenotes.ui.settings.components.SettingsInfoCard
+import dev.dettmer.simplenotes.ui.settings.components.SettingsOutlinedButton
+import dev.dettmer.simplenotes.ui.settings.components.SettingsRadioGroup
+import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
+import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Backup and restore settings screen
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun BackupSettingsScreen(
+ viewModel: SettingsViewModel,
+ onBack: () -> Unit
+) {
+ val isBackupInProgress by viewModel.isBackupInProgress.collectAsState()
+
+ // Restore dialog state
+ var showRestoreDialog by remember { mutableStateOf(false) }
+ var restoreSource by remember { mutableStateOf(RestoreSource.LocalFile) }
+ var pendingRestoreUri by remember { mutableStateOf(null) }
+ var selectedRestoreMode by remember { mutableStateOf(RestoreMode.MERGE) }
+
+ // File picker launchers
+ val createBackupLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.CreateDocument("application/json")
+ ) { uri ->
+ uri?.let { viewModel.createBackup(it) }
+ }
+
+ val restoreFileLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.OpenDocument()
+ ) { uri ->
+ uri?.let {
+ pendingRestoreUri = it
+ restoreSource = RestoreSource.LocalFile
+ showRestoreDialog = true
+ }
+ }
+
+ SettingsScaffold(
+ title = "Backup & Wiederherstellung",
+ onBack = onBack
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Info Card
+ SettingsInfoCard(
+ text = "π¦ Bei jeder Wiederherstellung wird automatisch ein " +
+ "Sicherheits-Backup erstellt."
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Local Backup Section
+ SettingsSectionHeader(text = "Lokales Backup")
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ SettingsButton(
+ text = "πΎ Backup erstellen",
+ onClick = {
+ val timestamp = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
+ .format(Date())
+ val filename = "simplenotes_backup_$timestamp.json"
+ createBackupLauncher.launch(filename)
+ },
+ isLoading = isBackupInProgress,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ SettingsOutlinedButton(
+ text = "π Aus Datei wiederherstellen",
+ onClick = {
+ restoreFileLauncher.launch(arrayOf("application/json"))
+ },
+ isLoading = isBackupInProgress,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+
+ SettingsDivider()
+
+ // Server Backup Section
+ SettingsSectionHeader(text = "Server-Backup")
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ SettingsOutlinedButton(
+ text = "βοΈ Vom Server wiederherstellen",
+ onClick = {
+ restoreSource = RestoreSource.Server
+ showRestoreDialog = true
+ },
+ isLoading = isBackupInProgress,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+
+ // Restore Mode Dialog
+ if (showRestoreDialog) {
+ RestoreModeDialog(
+ source = restoreSource,
+ selectedMode = selectedRestoreMode,
+ onModeSelected = { selectedRestoreMode = it },
+ onConfirm = {
+ showRestoreDialog = false
+ when (restoreSource) {
+ RestoreSource.LocalFile -> {
+ pendingRestoreUri?.let { uri ->
+ viewModel.restoreFromFile(uri, selectedRestoreMode)
+ }
+ }
+ RestoreSource.Server -> {
+ viewModel.restoreFromServer(selectedRestoreMode)
+ }
+ }
+ },
+ onDismiss = {
+ showRestoreDialog = false
+ pendingRestoreUri = null
+ }
+ )
+ }
+}
+
+/**
+ * Restore source enum
+ */
+private enum class RestoreSource {
+ LocalFile,
+ Server
+}
+
+/**
+ * Dialog for selecting restore mode
+ */
+@Composable
+private fun RestoreModeDialog(
+ source: RestoreSource,
+ selectedMode: RestoreMode,
+ onModeSelected: (RestoreMode) -> Unit,
+ onConfirm: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ val sourceText = when (source) {
+ RestoreSource.LocalFile -> "Lokale Datei"
+ RestoreSource.Server -> "WebDAV Server"
+ }
+
+ val modeOptions = listOf(
+ RadioOption(
+ value = RestoreMode.MERGE,
+ title = "βͺ ZusammenfΓΌhren (Standard)",
+ subtitle = "Neue hinzufΓΌgen, Bestehende behalten"
+ ),
+ RadioOption(
+ value = RestoreMode.REPLACE,
+ title = "βͺ Ersetzen",
+ subtitle = "Alle lΓΆschen & Backup importieren"
+ ),
+ RadioOption(
+ value = RestoreMode.OVERWRITE_DUPLICATES,
+ title = "βͺ Duplikate ΓΌberschreiben",
+ subtitle = "Backup gewinnt bei Konflikten"
+ )
+ )
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("β οΈ Backup wiederherstellen?") },
+ text = {
+ Column {
+ Text(
+ text = "Quelle: $sourceText",
+ style = MaterialTheme.typography.bodyMedium
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = "Wiederherstellungs-Modus:",
+ style = MaterialTheme.typography.labelLarge
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ SettingsRadioGroup(
+ options = modeOptions,
+ selectedValue = selectedMode,
+ onValueSelected = onModeSelected
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = "βΉοΈ Ein Sicherheits-Backup wird vor dem Wiederherstellen automatisch erstellt.",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onConfirm) {
+ Text("Wiederherstellen")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text("Abbrechen")
+ }
+ }
+ )
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt
new file mode 100644
index 0000000..c68593c
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt
@@ -0,0 +1,148 @@
+package dev.dettmer.simplenotes.ui.settings.screens
+
+import android.content.Intent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Notes
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.core.content.FileProvider
+import dev.dettmer.simplenotes.BuildConfig
+import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
+import dev.dettmer.simplenotes.ui.settings.components.SettingsButton
+import dev.dettmer.simplenotes.ui.settings.components.SettingsDangerButton
+import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
+import dev.dettmer.simplenotes.ui.settings.components.SettingsInfoCard
+import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
+import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader
+import dev.dettmer.simplenotes.ui.settings.components.SettingsSwitch
+
+/**
+ * Debug and diagnostics settings screen
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun DebugSettingsScreen(
+ viewModel: SettingsViewModel,
+ onBack: () -> Unit
+) {
+ val context = LocalContext.current
+ val fileLoggingEnabled by viewModel.fileLoggingEnabled.collectAsState()
+
+ var showClearLogsDialog by remember { mutableStateOf(false) }
+
+ SettingsScaffold(
+ title = "Debug & Diagnose",
+ onBack = onBack
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // File Logging Toggle
+ SettingsSwitch(
+ title = "Datei-Logging",
+ subtitle = "Sync-Logs in Datei speichern",
+ checked = fileLoggingEnabled,
+ onCheckedChange = { viewModel.setFileLogging(it) },
+ icon = Icons.AutoMirrored.Filled.Notes
+ )
+
+ // Privacy Info
+ SettingsInfoCard(
+ text = "π Datenschutz: Logs werden nur lokal auf deinem GerΓ€t gespeichert " +
+ "und niemals an externe Server gesendet. Die Logs enthalten " +
+ "Sync-AktivitΓ€ten zur Fehlerdiagnose. Du kannst sie jederzeit lΓΆschen " +
+ "oder exportieren."
+ )
+
+ SettingsDivider()
+
+ SettingsSectionHeader(text = "Log-Aktionen")
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Export Logs Button
+ SettingsButton(
+ text = "π€ Logs exportieren & teilen",
+ onClick = {
+ val logFile = viewModel.getLogFile()
+ if (logFile != null && logFile.exists() && logFile.length() > 0L) {
+ val logUri = FileProvider.getUriForFile(
+ context,
+ "${BuildConfig.APPLICATION_ID}.fileprovider",
+ logFile
+ )
+
+ val shareIntent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_STREAM, logUri)
+ putExtra(Intent.EXTRA_SUBJECT, "SimpleNotes Sync Logs")
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+
+ context.startActivity(Intent.createChooser(shareIntent, "Logs teilen via..."))
+ }
+ },
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Clear Logs Button
+ SettingsDangerButton(
+ text = "ποΈ Logs lΓΆschen",
+ onClick = { showClearLogsDialog = true },
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+
+ // Clear Logs Confirmation Dialog
+ if (showClearLogsDialog) {
+ AlertDialog(
+ onDismissRequest = { showClearLogsDialog = false },
+ title = { Text("Logs lΓΆschen?") },
+ text = {
+ Text("Alle gespeicherten Sync-Logs werden unwiderruflich gelΓΆscht.")
+ },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ showClearLogsDialog = false
+ viewModel.clearLogs()
+ }
+ ) {
+ Text("LΓΆschen")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showClearLogsDialog = false }) {
+ Text("Abbrechen")
+ }
+ }
+ )
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/MarkdownSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/MarkdownSettingsScreen.kt
new file mode 100644
index 0000000..0b245af
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/MarkdownSettingsScreen.kt
@@ -0,0 +1,128 @@
+package dev.dettmer.simplenotes.ui.settings.screens
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Description
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
+import dev.dettmer.simplenotes.ui.settings.components.SettingsButton
+import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
+import dev.dettmer.simplenotes.ui.settings.components.SettingsInfoCard
+import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
+import dev.dettmer.simplenotes.ui.settings.components.SettingsSwitch
+
+/**
+ * Markdown Desktop integration settings screen
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun MarkdownSettingsScreen(
+ viewModel: SettingsViewModel,
+ onBack: () -> Unit
+) {
+ val markdownAutoSync by viewModel.markdownAutoSync.collectAsState()
+ val exportProgress by viewModel.markdownExportProgress.collectAsState()
+
+ // v1.5.0 Fix: Progress Dialog for initial export
+ exportProgress?.let { progress ->
+ AlertDialog(
+ onDismissRequest = { /* Not dismissable */ },
+ title = { Text("Markdown Auto-Sync") },
+ text = {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ text = if (progress.isComplete) {
+ "β
Export abgeschlossen"
+ } else {
+ "Exportiere ${progress.current}/${progress.total} Notizen..."
+ },
+ style = MaterialTheme.typography.bodyMedium
+ )
+
+ LinearProgressIndicator(
+ progress = {
+ if (progress.total > 0) {
+ progress.current.toFloat() / progress.total.toFloat()
+ } else 0f
+ },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ },
+ confirmButton = { /* No button - auto dismiss */ }
+ )
+ }
+
+ SettingsScaffold(
+ title = "Markdown Desktop-Integration",
+ onBack = onBack
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Info Card
+ SettingsInfoCard(
+ text = "π Exportiert Notizen zusΓ€tzlich als .md-Dateien. Mounte " +
+ "WebDAV als Netzlaufwerk um mit VS Code, Typora oder jedem " +
+ "Markdown-Editor zu bearbeiten. JSON-Sync bleibt primΓ€res Format."
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Markdown Auto-Sync Toggle
+ SettingsSwitch(
+ title = "Markdown Auto-Sync",
+ subtitle = "Synchronisiert Notizen automatisch als .md-Dateien (Upload + Download bei jedem Sync)",
+ checked = markdownAutoSync,
+ onCheckedChange = { viewModel.setMarkdownAutoSync(it) },
+ icon = Icons.Default.Description
+ )
+
+ // Manual sync button (only visible when auto-sync is off)
+ if (!markdownAutoSync) {
+ SettingsDivider()
+
+ SettingsInfoCard(
+ text = "Manueller Sync exportiert alle Notizen als .md-Dateien und " +
+ "importiert .md-Dateien vom Server. NΓΌtzlich fΓΌr einmalige Synchronisation."
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ SettingsButton(
+ text = "π Manueller Markdown-Sync",
+ onClick = { viewModel.performManualMarkdownSync() },
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/ServerSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/ServerSettingsScreen.kt
new file mode 100644
index 0000000..6927805
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/ServerSettingsScreen.kt
@@ -0,0 +1,241 @@
+package dev.dettmer.simplenotes.ui.settings.screens
+
+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.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Language
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
+import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
+
+/**
+ * Server configuration settings screen
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun ServerSettingsScreen(
+ viewModel: SettingsViewModel,
+ onBack: () -> Unit
+) {
+ val serverUrl by viewModel.serverUrl.collectAsState()
+ val username by viewModel.username.collectAsState()
+ val password by viewModel.password.collectAsState()
+ val isHttps by viewModel.isHttps.collectAsState()
+ val serverStatus by viewModel.serverStatus.collectAsState()
+ val isSyncing by viewModel.isSyncing.collectAsState()
+
+ var passwordVisible by remember { mutableStateOf(false) }
+
+ // Check server status on load
+ LaunchedEffect(Unit) {
+ viewModel.checkServerStatus()
+ }
+
+ SettingsScaffold(
+ title = "Server-Einstellungen",
+ onBack = onBack
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp)
+ ) {
+ // Verbindungstyp
+ Text(
+ text = "Verbindungstyp",
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ FilterChip(
+ selected = !isHttps,
+ onClick = { viewModel.updateProtocol(false) },
+ label = { Text("π Intern (HTTP)") },
+ modifier = Modifier.weight(1f)
+ )
+ FilterChip(
+ selected = isHttps,
+ onClick = { viewModel.updateProtocol(true) },
+ label = { Text("π Extern (HTTPS)") },
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ Text(
+ text = if (!isHttps) {
+ "HTTP nur fΓΌr lokale Netzwerke (z.B. 192.168.x.x, 10.x.x.x)"
+ } else {
+ "HTTPS fΓΌr sichere Verbindungen ΓΌber das Internet"
+ },
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(top = 4.dp, bottom = 16.dp)
+ )
+
+ // Server-Adresse
+ OutlinedTextField(
+ value = serverUrl,
+ onValueChange = { viewModel.updateServerUrl(it) },
+ label = { Text("Server-Adresse") },
+ supportingText = { Text("z.B. http://192.168.0.188:8080/notes") },
+ leadingIcon = { Icon(Icons.Default.Language, null) },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri)
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Benutzername
+ OutlinedTextField(
+ value = username,
+ onValueChange = { viewModel.updateUsername(it) },
+ label = { Text("Benutzername") },
+ leadingIcon = { Icon(Icons.Default.Person, null) },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Passwort
+ OutlinedTextField(
+ value = password,
+ onValueChange = { viewModel.updatePassword(it) },
+ label = { Text("Passwort") },
+ leadingIcon = { Icon(Icons.Default.Lock, null) },
+ trailingIcon = {
+ IconButton(onClick = { passwordVisible = !passwordVisible }) {
+ Icon(
+ imageVector = if (passwordVisible) {
+ Icons.Default.VisibilityOff
+ } else {
+ Icons.Default.Visibility
+ },
+ contentDescription = if (passwordVisible) "Verstecken" else "Anzeigen"
+ )
+ }
+ },
+ visualTransformation = if (passwordVisible) {
+ VisualTransformation.None
+ } else {
+ PasswordVisualTransformation()
+ },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Server-Status
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text("Server-Status:", style = MaterialTheme.typography.labelLarge)
+ Text(
+ text = when (serverStatus) {
+ is SettingsViewModel.ServerStatus.Reachable -> "β
Erreichbar"
+ is SettingsViewModel.ServerStatus.Unreachable -> "β Nicht erreichbar"
+ is SettingsViewModel.ServerStatus.Checking -> "π PrΓΌfe..."
+ is SettingsViewModel.ServerStatus.NotConfigured -> "β οΈ Nicht konfiguriert"
+ else -> "β Unbekannt"
+ },
+ color = when (serverStatus) {
+ is SettingsViewModel.ServerStatus.Reachable -> Color(0xFF4CAF50)
+ is SettingsViewModel.ServerStatus.Unreachable -> Color(0xFFF44336)
+ is SettingsViewModel.ServerStatus.NotConfigured -> Color(0xFFFF9800)
+ else -> MaterialTheme.colorScheme.onSurfaceVariant
+ }
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ // Action Buttons
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ OutlinedButton(
+ onClick = { viewModel.testConnection() },
+ modifier = Modifier.weight(1f)
+ ) {
+ Text("Verbindung testen")
+ }
+
+ Button(
+ onClick = { viewModel.syncNow() },
+ enabled = !isSyncing,
+ modifier = Modifier.weight(1f)
+ ) {
+ if (isSyncing) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(16.dp),
+ strokeWidth = 2.dp,
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ }
+ Text("Jetzt synchronisieren")
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SettingsMainScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SettingsMainScreen.kt
new file mode 100644
index 0000000..25c256f
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SettingsMainScreen.kt
@@ -0,0 +1,143 @@
+package dev.dettmer.simplenotes.ui.settings.screens
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Backup
+import androidx.compose.material.icons.filled.BugReport
+import androidx.compose.material.icons.filled.Cloud
+import androidx.compose.material.icons.filled.Description
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Sync
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import dev.dettmer.simplenotes.BuildConfig
+import dev.dettmer.simplenotes.ui.settings.SettingsRoute
+import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
+import dev.dettmer.simplenotes.ui.settings.components.SettingsCard
+import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
+
+/**
+ * Main Settings overview screen with clickable group cards
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun SettingsMainScreen(
+ viewModel: SettingsViewModel,
+ onNavigate: (SettingsRoute) -> Unit,
+ onBack: () -> Unit
+) {
+ val serverUrl by viewModel.serverUrl.collectAsState()
+ val serverStatus by viewModel.serverStatus.collectAsState()
+ val autoSyncEnabled by viewModel.autoSyncEnabled.collectAsState()
+ val syncInterval by viewModel.syncInterval.collectAsState()
+ val markdownAutoSync by viewModel.markdownAutoSync.collectAsState()
+ val fileLoggingEnabled by viewModel.fileLoggingEnabled.collectAsState()
+
+ // Check server status on first load
+ LaunchedEffect(Unit) {
+ viewModel.checkServerStatus()
+ }
+
+ SettingsScaffold(
+ title = "Einstellungen",
+ onBack = onBack
+ ) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(vertical = 8.dp)
+ ) {
+ // Server-Einstellungen
+ item {
+ // v1.5.0 Fix: Nur Prefix-URLs gelten als "nicht konfiguriert"
+ val isConfigured = serverUrl.isNotEmpty() &&
+ serverUrl != "http://" &&
+ serverUrl != "https://"
+
+ SettingsCard(
+ icon = Icons.Default.Cloud,
+ title = "Server-Einstellungen",
+ subtitle = if (isConfigured) serverUrl else null,
+ statusText = when (serverStatus) {
+ is SettingsViewModel.ServerStatus.Reachable -> "β
Erreichbar"
+ is SettingsViewModel.ServerStatus.Unreachable -> "β Nicht erreichbar"
+ is SettingsViewModel.ServerStatus.Checking -> "π PrΓΌfe..."
+ is SettingsViewModel.ServerStatus.NotConfigured -> "β οΈ Nicht konfiguriert"
+ else -> null
+ },
+ statusColor = when (serverStatus) {
+ is SettingsViewModel.ServerStatus.Reachable -> Color(0xFF4CAF50)
+ is SettingsViewModel.ServerStatus.Unreachable -> Color(0xFFF44336)
+ is SettingsViewModel.ServerStatus.NotConfigured -> Color(0xFFFF9800)
+ else -> Color.Gray
+ },
+ onClick = { onNavigate(SettingsRoute.Server) }
+ )
+ }
+
+ // Sync-Einstellungen
+ item {
+ val intervalText = when (syncInterval) {
+ 15L -> "15 Min"
+ 60L -> "60 Min"
+ else -> "30 Min"
+ }
+ SettingsCard(
+ icon = Icons.Default.Sync,
+ title = "Sync-Einstellungen",
+ subtitle = if (autoSyncEnabled) "Auto-Sync: An β’ $intervalText" else "Auto-Sync: Aus",
+ onClick = { onNavigate(SettingsRoute.Sync) }
+ )
+ }
+
+ // Markdown-Integration
+ item {
+ SettingsCard(
+ icon = Icons.Default.Description,
+ title = "Markdown Desktop-Integration",
+ subtitle = if (markdownAutoSync) "Auto-Sync: An" else "Auto-Sync: Aus",
+ onClick = { onNavigate(SettingsRoute.Markdown) }
+ )
+ }
+
+ // Backup & Wiederherstellung
+ item {
+ SettingsCard(
+ icon = Icons.Default.Backup,
+ title = "Backup & Wiederherstellung",
+ subtitle = "Lokales oder Server-Backup",
+ onClick = { onNavigate(SettingsRoute.Backup) }
+ )
+ }
+
+ // Γber diese App
+ item {
+ SettingsCard(
+ icon = Icons.Default.Info,
+ title = "Γber diese App",
+ subtitle = "Version ${BuildConfig.VERSION_NAME}",
+ onClick = { onNavigate(SettingsRoute.About) }
+ )
+ }
+
+ // Debug & Diagnose
+ item {
+ SettingsCard(
+ icon = Icons.Default.BugReport,
+ title = "Debug & Diagnose",
+ subtitle = if (fileLoggingEnabled) "Logging: An" else "Logging: Aus",
+ onClick = { onNavigate(SettingsRoute.Debug) }
+ )
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SyncSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SyncSettingsScreen.kt
new file mode 100644
index 0000000..7452bae
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SyncSettingsScreen.kt
@@ -0,0 +1,112 @@
+package dev.dettmer.simplenotes.ui.settings.screens
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Sync
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
+import dev.dettmer.simplenotes.ui.settings.components.RadioOption
+import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
+import dev.dettmer.simplenotes.ui.settings.components.SettingsInfoCard
+import dev.dettmer.simplenotes.ui.settings.components.SettingsRadioGroup
+import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
+import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader
+import dev.dettmer.simplenotes.ui.settings.components.SettingsSwitch
+
+/**
+ * Sync settings screen (Auto-Sync toggle and interval selection)
+ * v1.5.0: Jetpack Compose Settings Redesign
+ */
+@Composable
+fun SyncSettingsScreen(
+ viewModel: SettingsViewModel,
+ onBack: () -> Unit
+) {
+ val autoSyncEnabled by viewModel.autoSyncEnabled.collectAsState()
+ val syncInterval by viewModel.syncInterval.collectAsState()
+
+ SettingsScaffold(
+ title = "Sync-Einstellungen",
+ onBack = onBack
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Auto-Sync Info
+ SettingsInfoCard(
+ text = "π Auto-Sync:\n" +
+ "β’ PrΓΌft alle 30 Min. ob Server erreichbar\n" +
+ "β’ Funktioniert bei jeder WiFi-Verbindung\n" +
+ "β’ LΓ€uft auch im Hintergrund\n" +
+ "β’ Minimaler Akkuverbrauch (~0.4%/Tag)"
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Auto-Sync Toggle
+ SettingsSwitch(
+ title = "Auto-Sync aktiviert",
+ checked = autoSyncEnabled,
+ onCheckedChange = { viewModel.setAutoSync(it) },
+ icon = Icons.Default.Sync
+ )
+
+ SettingsDivider()
+
+ // Sync Interval Section
+ SettingsSectionHeader(text = "Sync-Intervall")
+
+ SettingsInfoCard(
+ text = "Legt fest, wie oft die App im Hintergrund synchronisiert. " +
+ "KΓΌrzere Intervalle bedeuten aktuellere Daten, verbrauchen aber etwas mehr Akku.\n\n" +
+ "β±οΈ Hinweis: Wenn dein Smartphone im Standby ist, kann Android die " +
+ "Synchronisation verzΓΆgern (bis zu 60 Min.), um Akku zu sparen. " +
+ "Das ist normal und betrifft alle Hintergrund-Apps."
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Interval Radio Group
+ val intervalOptions = listOf(
+ RadioOption(
+ value = 15L,
+ title = "β‘ Alle 15 Minuten",
+ subtitle = "Schnellste Synchronisation β’ ~0.8% Akku/Tag (~23 mAh)"
+ ),
+ RadioOption(
+ value = 30L,
+ title = "β Alle 30 Minuten (Empfohlen)",
+ subtitle = "Ausgewogenes VerhΓ€ltnis β’ ~0.4% Akku/Tag (~12 mAh)"
+ ),
+ RadioOption(
+ value = 60L,
+ title = "π Alle 60 Minuten",
+ subtitle = "Maximale Akkulaufzeit β’ ~0.2% Akku/Tag (~6 mAh geschΓ€tzt)"
+ )
+ )
+
+ SettingsRadioGroup(
+ options = intervalOptions,
+ selectedValue = syncInterval,
+ onValueSelected = { viewModel.setSyncInterval(it) }
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+}
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index 2aaf92c..4b29661 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -2,6 +2,7 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false // v1.5.0: Jetpack Compose
alias(libs.plugins.ktlint) apply false
alias(libs.plugins.detekt) apply false
}
\ No newline at end of file
diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml
index 59f739b..ea93011 100644
--- a/android/gradle/libs.versions.toml
+++ b/android/gradle/libs.versions.toml
@@ -11,6 +11,11 @@ activity = "1.8.0"
constraintlayout = "2.1.4"
ktlint = "12.1.0"
detekt = "1.23.4"
+# Jetpack Compose v1.5.0
+composeBom = "2024.02.00"
+navigationCompose = "2.7.6"
+lifecycleRuntimeCompose = "2.7.0"
+activityCompose = "1.8.2"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -21,10 +26,22 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+# Jetpack Compose v1.5.0
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
+androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
[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" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }