diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt index 71534af..928f39a 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt @@ -182,6 +182,11 @@ class MainActivity : AppCompatActivity() { swipeRefreshLayout.isRefreshing = false syncStatusBanner.visibility = View.GONE } + // v1.5.0: Silent-Sync - Banner nicht anzeigen, aber Sync-Controls deaktivieren + SyncStateManager.SyncState.SYNCING_SILENT -> { + setSyncControlsEnabled(false) + // Kein Banner anzeigen bei Silent-Sync (z.B. onResume Auto-Sync) + } } } } @@ -222,6 +227,7 @@ class MainActivity : AppCompatActivity() { * - Nur Success-Toast (kein "Auto-Sync..." Toast) * * NOTE: WiFi-Connect Sync nutzt WorkManager (auch wenn App geschlossen!) + * v1.5.0: Silent-Sync - kein Banner während des Syncs, Fehler werden trotzdem angezeigt */ private fun triggerAutoSync(source: String = "unknown") { // Throttling: Max 1 Sync pro Minute @@ -230,7 +236,8 @@ class MainActivity : AppCompatActivity() { } // 🔄 v1.3.1: Check if sync already running - if (!SyncStateManager.tryStartSync("auto-$source")) { + // v1.5.0: silent=true - kein Banner bei Auto-Sync + if (!SyncStateManager.tryStartSync("auto-$source", silent = true)) { Logger.d(TAG, "⏭️ Auto-sync ($source): Another sync already in progress") return } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt index edc0d1d..e43f513 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt @@ -19,7 +19,8 @@ object SyncStateManager { */ enum class SyncState { IDLE, // Kein Sync aktiv - SYNCING, // Sync läuft gerade + SYNCING, // Sync läuft gerade (Banner sichtbar) + SYNCING_SILENT, // v1.5.0: Sync läuft im Hintergrund (kein Banner, z.B. onResume) COMPLETED, // Sync erfolgreich abgeschlossen (kurz anzeigen) ERROR // Sync fehlgeschlagen (kurz anzeigen) } @@ -31,6 +32,7 @@ object SyncStateManager { val state: SyncState = SyncState.IDLE, val message: String? = null, val source: String? = null, // "manual", "auto", "pullToRefresh", "background" + val silent: Boolean = false, // v1.5.0: Wenn true, wird nach Completion kein Banner angezeigt val timestamp: Long = System.currentTimeMillis() ) @@ -44,28 +46,35 @@ object SyncStateManager { private val lock = Any() /** - * Prüft ob gerade ein Sync läuft + * Prüft ob gerade ein Sync läuft (inkl. Silent-Sync) */ val isSyncing: Boolean - get() = _syncStatus.value?.state == SyncState.SYNCING + get() { + val state = _syncStatus.value?.state + return state == SyncState.SYNCING || state == SyncState.SYNCING_SILENT + } /** * Versucht einen Sync zu starten. + * @param source Quelle des Syncs (für Logging) + * @param silent v1.5.0: Wenn true, wird kein Banner angezeigt (z.B. bei onResume Auto-Sync) * @return true wenn Sync gestartet werden kann, false wenn bereits einer läuft */ - fun tryStartSync(source: String): Boolean { + fun tryStartSync(source: String, silent: Boolean = false): Boolean { synchronized(lock) { if (isSyncing) { Logger.d(TAG, "⚠️ Sync already in progress, rejecting new sync from: $source") return false } - Logger.d(TAG, "🔄 Starting sync from: $source") + val syncState = if (silent) SyncState.SYNCING_SILENT else SyncState.SYNCING + Logger.d(TAG, "🔄 Starting sync from: $source (silent=$silent)") _syncStatus.postValue( SyncStatus( - state = SyncState.SYNCING, + state = syncState, message = "Synchronisiere...", - source = source + source = source, + silent = silent // v1.5.0: Merkt sich ob silent für markCompleted() ) ) return true @@ -74,18 +83,29 @@ object SyncStateManager { /** * Markiert Sync als erfolgreich abgeschlossen + * v1.5.0: Bei Silent-Sync direkt auf IDLE wechseln (kein Banner) */ fun markCompleted(message: String? = null) { synchronized(lock) { - val currentSource = _syncStatus.value?.source - Logger.d(TAG, "✅ Sync completed from: $currentSource") - _syncStatus.postValue( - SyncStatus( - state = SyncState.COMPLETED, - message = message, - source = currentSource + val current = _syncStatus.value + val currentSource = current?.source + val wasSilent = current?.silent == true + + Logger.d(TAG, "✅ Sync completed from: $currentSource (silent=$wasSilent)") + + if (wasSilent) { + // v1.5.0: Silent-Sync - direkt auf IDLE, kein Banner anzeigen + _syncStatus.postValue(SyncStatus()) + } else { + // Normaler Sync - COMPLETED State anzeigen + _syncStatus.postValue( + SyncStatus( + state = SyncState.COMPLETED, + message = message, + source = currentSource + ) ) - ) + } } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/ComposeNoteEditorActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/ComposeNoteEditorActivity.kt index 98d7ec2..b3f4435 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/ComposeNoteEditorActivity.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/ComposeNoteEditorActivity.kt @@ -2,6 +2,7 @@ package dev.dettmer.simplenotes.ui.editor import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedCallback import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels @@ -48,11 +49,30 @@ class ComposeNoteEditorActivity : ComponentActivity() { enableEdgeToEdge() + // v1.5.0: Handle back button with slide animation + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finish() + @Suppress("DEPRECATION") + overridePendingTransition( + dev.dettmer.simplenotes.R.anim.slide_in_left, + dev.dettmer.simplenotes.R.anim.slide_out_right + ) + } + }) + setContent { SimpleNotesTheme { NoteEditorScreen( viewModel = viewModel, - onNavigateBack = { finish() } + onNavigateBack = { + finish() + @Suppress("DEPRECATION") + overridePendingTransition( + dev.dettmer.simplenotes.R.anim.slide_in_left, + dev.dettmer.simplenotes.R.anim.slide_out_right + ) + } ) } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt index 58f207d..d951026 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape @@ -43,6 +44,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -252,9 +255,30 @@ private fun TextNoteContent( focusRequester: FocusRequester, modifier: Modifier = Modifier ) { + // v1.5.0: Use TextFieldValue to control cursor position + var textFieldValue by remember(content) { + mutableStateOf(TextFieldValue( + text = content, + selection = TextRange(content.length) + )) + } + + // Sync external changes + LaunchedEffect(content) { + if (textFieldValue.text != content) { + textFieldValue = TextFieldValue( + text = content, + selection = TextRange(content.length) + ) + } + } + OutlinedTextField( - value = content, - onValueChange = onContentChange, + value = textFieldValue, + onValueChange = { newValue -> + textFieldValue = newValue + onContentChange(newValue.text) + }, modifier = modifier.focusRequester(focusRequester), label = { Text(stringResource(R.string.content)) }, shape = RoundedCornerShape(16.dp) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt index f645c9e..3af1b2b 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt @@ -142,7 +142,8 @@ fun ChecklistItemRow( keyboardActions = KeyboardActions( onNext = { onAddNewItem() } ), - singleLine = true, + singleLine = false, + maxLines = 5, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), decorationBox = { innerTextField -> Box { diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt index bc57b73..21dcb2c 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt @@ -234,14 +234,28 @@ class ComposeMainActivity : ComponentActivity() { noteId?.let { intent.putExtra(ComposeNoteEditorActivity.EXTRA_NOTE_ID, it) } - startActivity(intent) + + // v1.5.0: Add slide animation + val options = ActivityOptions.makeCustomAnimation( + this, + dev.dettmer.simplenotes.R.anim.slide_in_right, + dev.dettmer.simplenotes.R.anim.slide_out_left + ) + startActivity(intent, options.toBundle()) } private fun createNote(noteType: NoteType) { cameFromEditor = true val intent = Intent(this, ComposeNoteEditorActivity::class.java) intent.putExtra(ComposeNoteEditorActivity.EXTRA_NOTE_TYPE, noteType.name) - startActivity(intent) + + // v1.5.0: Add slide animation + val options = ActivityOptions.makeCustomAnimation( + this, + dev.dettmer.simplenotes.R.anim.slide_in_right, + dev.dettmer.simplenotes.R.anim.slide_out_left + ) + startActivity(intent, options.toBundle()) } private fun openSettings() { diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt index 0ef1b88..0921d92 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt @@ -470,6 +470,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { /** * Trigger auto-sync (onResume) * Only runs if server is configured and interval has passed + * v1.5.0: Silent-Sync - kein Banner während des Syncs, Fehler werden trotzdem angezeigt */ fun triggerAutoSync(source: String = "auto") { // Throttling check @@ -483,7 +484,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { return } - if (!SyncStateManager.tryStartSync("auto-$source")) { + // v1.5.0: silent=true - kein Banner bei Auto-Sync, aber Fehler werden trotzdem angezeigt + if (!SyncStateManager.tryStartSync("auto-$source", silent = true)) { Logger.d(TAG, "⏭️ Auto-sync ($source): Another sync already in progress") return } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt index b72c768..2436b27 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt @@ -22,6 +22,7 @@ import dev.dettmer.simplenotes.sync.SyncStateManager /** * Sync status banner shown below the toolbar during sync * v1.5.0: Jetpack Compose MainActivity Redesign + * v1.5.0: SYNCING_SILENT ignorieren - Banner nur bei manuellen Syncs oder Fehlern anzeigen */ @Composable fun SyncStatusBanner( @@ -29,7 +30,10 @@ fun SyncStatusBanner( message: String?, modifier: Modifier = Modifier ) { - val isVisible = syncState != SyncStateManager.SyncState.IDLE + // v1.5.0: Banner nicht anzeigen bei IDLE oder SYNCING_SILENT (Auto-Sync im Hintergrund) + // Fehler werden trotzdem angezeigt (ERROR state nach Silent-Sync wechselt zu ERROR, nicht SYNCING_SILENT) + val isVisible = syncState != SyncStateManager.SyncState.IDLE + && syncState != SyncStateManager.SyncState.SYNCING_SILENT AnimatedVisibility( visible = isVisible, @@ -57,6 +61,7 @@ fun SyncStatusBanner( Text( text = when (syncState) { SyncStateManager.SyncState.SYNCING -> "Synchronisiere..." + SyncStateManager.SyncState.SYNCING_SILENT -> "" // v1.5.0: Wird nicht angezeigt (isVisible = false) SyncStateManager.SyncState.COMPLETED -> message ?: "Synchronisiert" SyncStateManager.SyncState.ERROR -> message ?: "Fehler" SyncStateManager.SyncState.IDLE -> "" 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 index c506c72..ad742a2 100644 --- 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 @@ -1,8 +1,13 @@ package dev.dettmer.simplenotes.ui.settings.screens import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas import android.net.Uri +import androidx.compose.foundation.Image +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 @@ -13,6 +18,7 @@ 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.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight @@ -25,11 +31,14 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import dev.dettmer.simplenotes.BuildConfig import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold @@ -76,9 +85,26 @@ fun AboutScreen( .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = "📝", - style = MaterialTheme.typography.displayMedium + // v1.5.0: App icon loaded from PackageManager and converted to Bitmap + val context = LocalContext.current + val appIcon = remember { + val drawable = context.packageManager.getApplicationIcon(context.packageName) + // Convert any Drawable (including AdaptiveIconDrawable) to Bitmap + val bitmap = Bitmap.createBitmap( + drawable.intrinsicWidth.coerceAtLeast(1), + drawable.intrinsicHeight.coerceAtLeast(1), + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + bitmap.asImageBitmap() + } + + Image( + bitmap = appIcon, + contentDescription = "App Icon", + modifier = Modifier.size(96.dp) ) Spacer(modifier = Modifier.height(8.dp))