fix(v1.5.0): Silent-Sync Mode + UI Improvements
Silent-Sync Implementation (Auto-Sync Banner Fix): - Add SYNCING_SILENT state to SyncStateManager for background syncs - Auto-sync (onResume) now triggers silently without banner interruption - Silent-sync state blocks additional manual syncs (mutual exclusion) - Error banners still display even after silent-sync failures - SyncStatus tracks 'silent' flag to hide COMPLETED banners after silent-sync UI/UX Improvements (from v1.5.0 post-migration fixes): - Fix text wrapping in checklist items (singleLine=false, maxLines=5) - Fix cursor position in text notes (use TextFieldValue with TextRange) - Display app icon instead of emoji in AboutScreen - Add smooth slide animations for NoteEditor transitions - Remove visual noise from AboutScreen icon Technical Changes: - ComposeNoteEditorActivity: Add back animation with OnBackPressedCallback - ComposeMainActivity: Add entry/exit slide animations for note editing - NoteEditorScreen: Use TextFieldValue for proper cursor positioning - ChecklistItemRow: Enable text wrapping for long checklist items - AboutScreen: Convert Drawable to Bitmap via Canvas (supports AdaptiveIcon) - SyncStatusBanner: Exclude SYNCING_SILENT from visibility checks - MainActivity: Update legacy auto-sync to use silent mode Fixes #[auto-sync-banner], improves #[user-experience] Branch: feature/v1.5.0
This commit is contained in:
@@ -182,6 +182,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
swipeRefreshLayout.isRefreshing = false
|
swipeRefreshLayout.isRefreshing = false
|
||||||
syncStatusBanner.visibility = View.GONE
|
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)
|
* - Nur Success-Toast (kein "Auto-Sync..." Toast)
|
||||||
*
|
*
|
||||||
* NOTE: WiFi-Connect Sync nutzt WorkManager (auch wenn App geschlossen!)
|
* 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") {
|
private fun triggerAutoSync(source: String = "unknown") {
|
||||||
// Throttling: Max 1 Sync pro Minute
|
// Throttling: Max 1 Sync pro Minute
|
||||||
@@ -230,7 +236,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔄 v1.3.1: Check if sync already running
|
// 🔄 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")
|
Logger.d(TAG, "⏭️ Auto-sync ($source): Another sync already in progress")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ object SyncStateManager {
|
|||||||
*/
|
*/
|
||||||
enum class SyncState {
|
enum class SyncState {
|
||||||
IDLE, // Kein Sync aktiv
|
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)
|
COMPLETED, // Sync erfolgreich abgeschlossen (kurz anzeigen)
|
||||||
ERROR // Sync fehlgeschlagen (kurz anzeigen)
|
ERROR // Sync fehlgeschlagen (kurz anzeigen)
|
||||||
}
|
}
|
||||||
@@ -31,6 +32,7 @@ object SyncStateManager {
|
|||||||
val state: SyncState = SyncState.IDLE,
|
val state: SyncState = SyncState.IDLE,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val source: String? = null, // "manual", "auto", "pullToRefresh", "background"
|
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()
|
val timestamp: Long = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,28 +46,35 @@ object SyncStateManager {
|
|||||||
private val lock = Any()
|
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
|
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.
|
* 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
|
* @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) {
|
synchronized(lock) {
|
||||||
if (isSyncing) {
|
if (isSyncing) {
|
||||||
Logger.d(TAG, "⚠️ Sync already in progress, rejecting new sync from: $source")
|
Logger.d(TAG, "⚠️ Sync already in progress, rejecting new sync from: $source")
|
||||||
return false
|
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.postValue(
|
||||||
SyncStatus(
|
SyncStatus(
|
||||||
state = SyncState.SYNCING,
|
state = syncState,
|
||||||
message = "Synchronisiere...",
|
message = "Synchronisiere...",
|
||||||
source = source
|
source = source,
|
||||||
|
silent = silent // v1.5.0: Merkt sich ob silent für markCompleted()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
@@ -74,11 +83,21 @@ object SyncStateManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Markiert Sync als erfolgreich abgeschlossen
|
* Markiert Sync als erfolgreich abgeschlossen
|
||||||
|
* v1.5.0: Bei Silent-Sync direkt auf IDLE wechseln (kein Banner)
|
||||||
*/
|
*/
|
||||||
fun markCompleted(message: String? = null) {
|
fun markCompleted(message: String? = null) {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
val currentSource = _syncStatus.value?.source
|
val current = _syncStatus.value
|
||||||
Logger.d(TAG, "✅ Sync completed from: $currentSource")
|
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.postValue(
|
||||||
SyncStatus(
|
SyncStatus(
|
||||||
state = SyncState.COMPLETED,
|
state = SyncState.COMPLETED,
|
||||||
@@ -88,6 +107,7 @@ object SyncStateManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Markiert Sync als fehlgeschlagen
|
* Markiert Sync als fehlgeschlagen
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package dev.dettmer.simplenotes.ui.editor
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
@@ -48,11 +49,30 @@ class ComposeNoteEditorActivity : ComponentActivity() {
|
|||||||
|
|
||||||
enableEdgeToEdge()
|
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 {
|
setContent {
|
||||||
SimpleNotesTheme {
|
SimpleNotesTheme {
|
||||||
NoteEditorScreen(
|
NoteEditorScreen(
|
||||||
viewModel = viewModel,
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.imePadding
|
|||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -43,6 +44,8 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
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.focus.focusRequester
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
@@ -252,9 +255,30 @@ private fun TextNoteContent(
|
|||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
modifier: Modifier = Modifier
|
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(
|
OutlinedTextField(
|
||||||
value = content,
|
value = textFieldValue,
|
||||||
onValueChange = onContentChange,
|
onValueChange = { newValue ->
|
||||||
|
textFieldValue = newValue
|
||||||
|
onContentChange(newValue.text)
|
||||||
|
},
|
||||||
modifier = modifier.focusRequester(focusRequester),
|
modifier = modifier.focusRequester(focusRequester),
|
||||||
label = { Text(stringResource(R.string.content)) },
|
label = { Text(stringResource(R.string.content)) },
|
||||||
shape = RoundedCornerShape(16.dp)
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ fun ChecklistItemRow(
|
|||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onNext = { onAddNewItem() }
|
onNext = { onAddNewItem() }
|
||||||
),
|
),
|
||||||
singleLine = true,
|
singleLine = false,
|
||||||
|
maxLines = 5,
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
Box {
|
Box {
|
||||||
|
|||||||
@@ -234,14 +234,28 @@ class ComposeMainActivity : ComponentActivity() {
|
|||||||
noteId?.let {
|
noteId?.let {
|
||||||
intent.putExtra(ComposeNoteEditorActivity.EXTRA_NOTE_ID, it)
|
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) {
|
private fun createNote(noteType: NoteType) {
|
||||||
cameFromEditor = true
|
cameFromEditor = true
|
||||||
val intent = Intent(this, ComposeNoteEditorActivity::class.java)
|
val intent = Intent(this, ComposeNoteEditorActivity::class.java)
|
||||||
intent.putExtra(ComposeNoteEditorActivity.EXTRA_NOTE_TYPE, noteType.name)
|
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() {
|
private fun openSettings() {
|
||||||
|
|||||||
@@ -470,6 +470,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
/**
|
/**
|
||||||
* Trigger auto-sync (onResume)
|
* Trigger auto-sync (onResume)
|
||||||
* Only runs if server is configured and interval has passed
|
* 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") {
|
fun triggerAutoSync(source: String = "auto") {
|
||||||
// Throttling check
|
// Throttling check
|
||||||
@@ -483,7 +484,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return
|
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")
|
Logger.d(TAG, "⏭️ Auto-sync ($source): Another sync already in progress")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import dev.dettmer.simplenotes.sync.SyncStateManager
|
|||||||
/**
|
/**
|
||||||
* Sync status banner shown below the toolbar during sync
|
* Sync status banner shown below the toolbar during sync
|
||||||
* v1.5.0: Jetpack Compose MainActivity Redesign
|
* v1.5.0: Jetpack Compose MainActivity Redesign
|
||||||
|
* v1.5.0: SYNCING_SILENT ignorieren - Banner nur bei manuellen Syncs oder Fehlern anzeigen
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SyncStatusBanner(
|
fun SyncStatusBanner(
|
||||||
@@ -29,7 +30,10 @@ fun SyncStatusBanner(
|
|||||||
message: String?,
|
message: String?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
// 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
|
val isVisible = syncState != SyncStateManager.SyncState.IDLE
|
||||||
|
&& syncState != SyncStateManager.SyncState.SYNCING_SILENT
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isVisible,
|
visible = isVisible,
|
||||||
@@ -57,6 +61,7 @@ fun SyncStatusBanner(
|
|||||||
Text(
|
Text(
|
||||||
text = when (syncState) {
|
text = when (syncState) {
|
||||||
SyncStateManager.SyncState.SYNCING -> "Synchronisiere..."
|
SyncStateManager.SyncState.SYNCING -> "Synchronisiere..."
|
||||||
|
SyncStateManager.SyncState.SYNCING_SILENT -> "" // v1.5.0: Wird nicht angezeigt (isVisible = false)
|
||||||
SyncStateManager.SyncState.COMPLETED -> message ?: "Synchronisiert"
|
SyncStateManager.SyncState.COMPLETED -> message ?: "Synchronisiert"
|
||||||
SyncStateManager.SyncState.ERROR -> message ?: "Fehler"
|
SyncStateManager.SyncState.ERROR -> message ?: "Fehler"
|
||||||
SyncStateManager.SyncState.IDLE -> ""
|
SyncStateManager.SyncState.IDLE -> ""
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
package dev.dettmer.simplenotes.ui.settings.screens
|
package dev.dettmer.simplenotes.ui.settings.screens
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import dev.dettmer.simplenotes.BuildConfig
|
import dev.dettmer.simplenotes.BuildConfig
|
||||||
import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
|
import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
|
||||||
import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
|
import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
|
||||||
@@ -76,9 +85,26 @@ fun AboutScreen(
|
|||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text(
|
// v1.5.0: App icon loaded from PackageManager and converted to Bitmap
|
||||||
text = "📝",
|
val context = LocalContext.current
|
||||||
style = MaterialTheme.typography.displayMedium
|
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))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
Reference in New Issue
Block a user