From 96c819b15457a41cf56a8dd201e2634b3c686ba5 Mon Sep 17 00:00:00 2001 From: inventory69 Date: Tue, 10 Feb 2026 11:30:06 +0100 Subject: [PATCH] feat(v1.8.0): IMPL_020 Note & Checklist Sorting - Add SortOption enum for note sorting (Updated, Created, Title, Type) - Add SortDirection enum with ASCENDING/DESCENDING and toggle() - Add ChecklistSortOption enum for in-editor sorting (Manual, Alphabetical, Unchecked/Checked First) - Implement persistent note sort preferences in SharedPreferences - Add SortDialog for main screen with sort option and direction selection - Add ChecklistSortDialog for editor screen with current sort option state - Implement sort logic in MainViewModel with combined sortedNotes StateFlow - Implement sort logic in NoteEditorViewModel with auto-sort for MANUAL and UNCHECKED_FIRST - Add separator display logic for MANUAL and UNCHECKED_FIRST sort options - Add 16 sorting-related strings (English and German) - Update Constants.kt with sort preference keys - Update MainScreen.kt, NoteEditorScreen.kt with sort UI integration --- .../simplenotes/models/ChecklistSortOption.kt | 21 +++ .../simplenotes/models/SortDirection.kt | 20 +++ .../dettmer/simplenotes/models/SortOption.kt | 24 +++ .../simplenotes/storage/NotesStorage.kt | 7 +- .../simplenotes/ui/editor/NoteEditorScreen.kt | 61 +++++-- .../ui/editor/NoteEditorViewModel.kt | 46 ++++- .../editor/components/ChecklistSortDialog.kt | 123 ++++++++++++++ .../dettmer/simplenotes/ui/main/MainScreen.kt | 34 +++- .../simplenotes/ui/main/MainViewModel.kt | 89 ++++++++++ .../ui/main/components/SortDialog.kt | 160 ++++++++++++++++++ .../dettmer/simplenotes/utils/Constants.kt | 6 + .../app/src/main/res/values-de/strings.xml | 19 +++ android/app/src/main/res/values/strings.xml | 19 +++ 13 files changed, 613 insertions(+), 16 deletions(-) create mode 100644 android/app/src/main/java/dev/dettmer/simplenotes/models/ChecklistSortOption.kt create mode 100644 android/app/src/main/java/dev/dettmer/simplenotes/models/SortDirection.kt create mode 100644 android/app/src/main/java/dev/dettmer/simplenotes/models/SortOption.kt create mode 100644 android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistSortDialog.kt create mode 100644 android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SortDialog.kt diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/models/ChecklistSortOption.kt b/android/app/src/main/java/dev/dettmer/simplenotes/models/ChecklistSortOption.kt new file mode 100644 index 0000000..f39211e --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/models/ChecklistSortOption.kt @@ -0,0 +1,21 @@ +package dev.dettmer.simplenotes.models + +/** + * πŸ†• v1.8.0: Sortieroptionen fΓΌr Checklist-Items im Editor + */ +enum class ChecklistSortOption { + /** Manuelle Reihenfolge (Drag & Drop) β€” kein Re-Sort */ + MANUAL, + + /** Alphabetisch Aβ†’Z */ + ALPHABETICAL_ASC, + + /** Alphabetisch Zβ†’A */ + ALPHABETICAL_DESC, + + /** Unchecked zuerst, dann Checked */ + UNCHECKED_FIRST, + + /** Checked zuerst, dann Unchecked */ + CHECKED_FIRST +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/models/SortDirection.kt b/android/app/src/main/java/dev/dettmer/simplenotes/models/SortDirection.kt new file mode 100644 index 0000000..542a5eb --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/models/SortDirection.kt @@ -0,0 +1,20 @@ +package dev.dettmer.simplenotes.models + +/** + * πŸ†• v1.8.0: Sortierrichtung + */ +enum class SortDirection(val prefsValue: String) { + ASCENDING("asc"), + DESCENDING("desc"); + + fun toggle(): SortDirection = when (this) { + ASCENDING -> DESCENDING + DESCENDING -> ASCENDING + } + + companion object { + fun fromPrefsValue(value: String): SortDirection { + return entries.find { it.prefsValue == value } ?: DESCENDING + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/models/SortOption.kt b/android/app/src/main/java/dev/dettmer/simplenotes/models/SortOption.kt new file mode 100644 index 0000000..47b1a29 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/models/SortOption.kt @@ -0,0 +1,24 @@ +package dev.dettmer.simplenotes.models + +/** + * πŸ†• v1.8.0: Sortieroptionen fΓΌr die Notizliste + */ +enum class SortOption(val prefsValue: String) { + /** Zuletzt bearbeitete zuerst (Default) */ + UPDATED_AT("updatedAt"), + + /** Zuletzt erstellte zuerst */ + CREATED_AT("createdAt"), + + /** Alphabetisch nach Titel */ + TITLE("title"), + + /** Nach Notiz-Typ (Text / Checkliste) */ + NOTE_TYPE("noteType"); + + companion object { + fun fromPrefsValue(value: String): SortOption { + return entries.find { it.prefsValue == value } ?: UPDATED_AT + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/storage/NotesStorage.kt b/android/app/src/main/java/dev/dettmer/simplenotes/storage/NotesStorage.kt index 75dec82..027b64f 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/storage/NotesStorage.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/storage/NotesStorage.kt @@ -35,11 +35,16 @@ class NotesStorage(private val context: Context) { } } + /** + * LΓ€dt alle Notizen aus dem lokalen Speicher. + * + * πŸ”€ v1.8.0: Sortierung entfernt β€” wird jetzt im ViewModel durchgefΓΌhrt, + * damit der User die Sortierung konfigurieren kann. + */ fun loadAllNotes(): List { return notesDir.listFiles() ?.filter { it.extension == "json" } ?.mapNotNull { Note.fromJson(it.readText()) } - ?.sortedByDescending { it.updatedAt } ?: emptyList() } 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 9926986..7f366e3 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 @@ -10,6 +10,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -23,6 +24,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.outlined.Sort import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Save @@ -57,9 +59,11 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.models.ChecklistSortOption import dev.dettmer.simplenotes.models.NoteType import dev.dettmer.simplenotes.ui.editor.components.CheckedItemsSeparator import dev.dettmer.simplenotes.ui.editor.components.ChecklistItemRow +import dev.dettmer.simplenotes.ui.editor.components.ChecklistSortDialog import dev.dettmer.simplenotes.ui.main.components.DeleteConfirmationDialog import kotlinx.coroutines.delay import dev.dettmer.simplenotes.utils.showToast @@ -87,6 +91,8 @@ fun NoteEditorScreen( val isOfflineMode by viewModel.isOfflineMode.collectAsState() var showDeleteDialog by remember { mutableStateOf(false) } + var showChecklistSortDialog by remember { mutableStateOf(false) } // πŸ”€ v1.8.0 + val lastChecklistSortOption by viewModel.lastChecklistSortOption.collectAsState() // πŸ”€ v1.8.0 var focusNewItemId by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() @@ -222,6 +228,7 @@ fun NoteEditorScreen( items = checklistItems, scope = scope, focusNewItemId = focusNewItemId, + currentSortOption = lastChecklistSortOption, // πŸ”€ v1.8.0 onTextChange = { id, text -> viewModel.updateChecklistItemText(id, text) }, onCheckedChange = { id, checked -> viewModel.updateChecklistItemChecked(id, checked) }, onDelete = { id -> viewModel.deleteChecklistItem(id) }, @@ -235,6 +242,7 @@ fun NoteEditorScreen( }, onMove = { from, to -> viewModel.moveChecklistItem(from, to) }, onFocusHandled = { focusNewItemId = null }, + onSortClick = { showChecklistSortDialog = true }, // πŸ”€ v1.8.0 modifier = Modifier .fillMaxWidth() .weight(1f) @@ -260,6 +268,18 @@ fun NoteEditorScreen( } ) } + + // πŸ”€ v1.8.0: Checklist Sort Dialog + if (showChecklistSortDialog) { + ChecklistSortDialog( + currentOption = lastChecklistSortOption, + onOptionSelected = { option -> + viewModel.sortChecklistItems(option) + showChecklistSortDialog = false + }, + onDismiss = { showChecklistSortDialog = false } + ) + } } @Composable @@ -309,6 +329,7 @@ private fun ChecklistEditor( items: List, scope: kotlinx.coroutines.CoroutineScope, focusNewItemId: String?, + currentSortOption: ChecklistSortOption, // πŸ”€ v1.8.0: Aktuelle Sortierung onTextChange: (String, String) -> Unit, onCheckedChange: (String, Boolean) -> Unit, onDelete: (String) -> Unit, @@ -316,6 +337,7 @@ private fun ChecklistEditor( onAddItemAtEnd: () -> Unit, onMove: (Int, Int) -> Unit, onFocusHandled: () -> Unit, + onSortClick: () -> Unit, // πŸ”€ v1.8.0 modifier: Modifier = Modifier ) { val listState = rememberLazyListState() @@ -325,10 +347,12 @@ private fun ChecklistEditor( onMove = onMove ) - // πŸ†• v1.8.0 (IMPL_017): Separator-Position berechnen + // πŸ†• v1.8.0 (IMPL_017 + IMPL_020): Separator nur bei MANUAL und UNCHECKED_FIRST anzeigen val uncheckedCount = items.count { !it.isChecked } val checkedCount = items.count { it.isChecked } - val showSeparator = uncheckedCount > 0 && checkedCount > 0 + val shouldShowSeparator = currentSortOption == ChecklistSortOption.MANUAL || + currentSortOption == ChecklistSortOption.UNCHECKED_FIRST + val showSeparator = shouldShowSeparator && uncheckedCount > 0 && checkedCount > 0 Column(modifier = modifier) { LazyColumn( @@ -396,17 +420,30 @@ private fun ChecklistEditor( } } - // Add Item Button - TextButton( - onClick = onAddItemAtEnd, - modifier = Modifier.padding(start = 8.dp) + // πŸ”€ v1.8.0: Add Item Button + Sort Button + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp, end = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier.padding(end = 8.dp) - ) - Text(stringResource(R.string.add_item)) + TextButton(onClick = onAddItemAtEnd) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp) + ) + Text(stringResource(R.string.add_item)) + } + + IconButton(onClick = onSortClick) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.Sort, + contentDescription = stringResource(R.string.sort_checklist), + modifier = androidx.compose.ui.Modifier.padding(4.dp) + ) + } } } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt index adeacf3..8f634a5 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import dev.dettmer.simplenotes.models.ChecklistItem +import dev.dettmer.simplenotes.models.ChecklistSortOption import dev.dettmer.simplenotes.models.Note import dev.dettmer.simplenotes.models.NoteType import dev.dettmer.simplenotes.models.SyncStatus @@ -65,6 +66,10 @@ class NoteEditorViewModel( ) val isOfflineMode: StateFlow = _isOfflineMode.asStateFlow() + // πŸ”€ v1.8.0 (IMPL_020): Letzte Checklist-Sortierung (Session-Scope) + private val _lastChecklistSortOption = MutableStateFlow(ChecklistSortOption.MANUAL) + val lastChecklistSortOption: StateFlow = _lastChecklistSortOption.asStateFlow() + // ═══════════════════════════════════════════════════════════════════════ // Events // ═══════════════════════════════════════════════════════════════════════ @@ -182,8 +187,14 @@ class NoteEditorViewModel( val updatedItems = items.map { item -> if (item.id == itemId) item.copy(isChecked = isChecked) else item } - // πŸ†• v1.8.0 (IMPL_017): Nach Toggle sortieren - sortChecklistItems(updatedItems) + // πŸ†• v1.8.0 (IMPL_017 + IMPL_020): Auto-Sort nur bei MANUAL und UNCHECKED_FIRST + val currentSort = _lastChecklistSortOption.value + if (currentSort == ChecklistSortOption.MANUAL || currentSort == ChecklistSortOption.UNCHECKED_FIRST) { + sortChecklistItems(updatedItems) + } else { + // Bei anderen Sortierungen (alphabetisch, checked first) nicht auto-sortieren + updatedItems.mapIndexed { index, item -> item.copy(order = index) } + } } } @@ -241,6 +252,37 @@ class NoteEditorViewModel( } } + /** + * πŸ”€ v1.8.0 (IMPL_020): Sortiert Checklist-Items nach gewΓ€hlter Option. + * Einmalige Aktion (nicht persistiert) β€” User kann danach per Drag & Drop feinjustieren. + */ + fun sortChecklistItems(option: ChecklistSortOption) { + // Merke die Auswahl fΓΌr diesen Editor-Session + _lastChecklistSortOption.value = option + + _checklistItems.update { items -> + val sorted = when (option) { + // Bei MANUAL: Sortiere nach checked/unchecked, damit Separator korrekt platziert wird + ChecklistSortOption.MANUAL -> items.sortedBy { it.isChecked } + + ChecklistSortOption.ALPHABETICAL_ASC -> + items.sortedBy { it.text.lowercase() } + + ChecklistSortOption.ALPHABETICAL_DESC -> + items.sortedByDescending { it.text.lowercase() } + + ChecklistSortOption.UNCHECKED_FIRST -> + items.sortedBy { it.isChecked } + + ChecklistSortOption.CHECKED_FIRST -> + items.sortedByDescending { it.isChecked } + } + + // Order-Werte neu zuweisen + sorted.mapIndexed { index, item -> item.copy(order = index) } + } + } + fun saveNote() { viewModelScope.launch { val state = _uiState.value diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistSortDialog.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistSortDialog.kt new file mode 100644 index 0000000..e7f2825 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistSortDialog.kt @@ -0,0 +1,123 @@ +package dev.dettmer.simplenotes.ui.editor.components + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +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.res.stringResource +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.models.ChecklistSortOption + +/** + * πŸ”€ v1.8.0: Dialog zur Auswahl der Checklist-Sortierung. + * + * Einmalige Sortier-Aktion (nicht persistiert). + * User kann danach per Drag & Drop feinjustieren. + * + * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + * β”‚ Sort Checklist β”‚ + * β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + * β”‚ ( ) Manual β”‚ + * β”‚ ( ) A β†’ Z β”‚ + * β”‚ ( ) Z β†’ A β”‚ + * β”‚ (●) Unchecked first β”‚ + * β”‚ ( ) Checked first β”‚ + * β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + * β”‚ [Cancel] [Apply] β”‚ + * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + */ +@Composable +fun ChecklistSortDialog( + currentOption: ChecklistSortOption, // πŸ”€ v1.8.0: Aktuelle Auswahl merken + onOptionSelected: (ChecklistSortOption) -> Unit, + onDismiss: () -> Unit +) { + var selectedOption by remember { mutableStateOf(currentOption) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = stringResource(R.string.sort_checklist), + style = MaterialTheme.typography.headlineSmall + ) + }, + text = { + Column { + ChecklistSortOption.entries.forEach { option -> + SortOptionRow( + label = stringResource(option.toStringRes()), + isSelected = selectedOption == option, + onClick = { selectedOption = option } + ) + } + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + onOptionSelected(selectedOption) + } + ) { + Text(stringResource(R.string.apply)) + } + } + ) +} + +@Composable +private fun SortOptionRow( + label: String, + isSelected: Boolean, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = isSelected, + onClick = onClick + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = label, + style = MaterialTheme.typography.bodyLarge + ) + } +} + +/** + * Extension: ChecklistSortOption β†’ String-Resource-ID + */ +fun ChecklistSortOption.toStringRes(): Int = when (this) { + ChecklistSortOption.MANUAL -> R.string.sort_checklist_manual + ChecklistSortOption.ALPHABETICAL_ASC -> R.string.sort_checklist_alpha_asc + ChecklistSortOption.ALPHABETICAL_DESC -> R.string.sort_checklist_alpha_desc + ChecklistSortOption.UNCHECKED_FIRST -> R.string.sort_checklist_unchecked_first + ChecklistSortOption.CHECKED_FIRST -> R.string.sort_checklist_checked_first +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt index f4159a4..c2561e3 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.automirrored.outlined.Sort import androidx.compose.material3.ExperimentalMaterial3Api // FabPosition nicht mehr benΓΆtigt - FAB wird manuell platziert import androidx.compose.material3.Icon @@ -48,6 +49,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import dev.dettmer.simplenotes.R import dev.dettmer.simplenotes.models.NoteType +import dev.dettmer.simplenotes.ui.main.components.SortDialog import dev.dettmer.simplenotes.sync.SyncStateManager import dev.dettmer.simplenotes.ui.main.components.DeleteConfirmationDialog import dev.dettmer.simplenotes.ui.main.components.EmptyState @@ -77,7 +79,7 @@ fun MainScreen( onOpenSettings: () -> Unit, onCreateNote: (NoteType) -> Unit ) { - val notes by viewModel.notes.collectAsState() + val notes by viewModel.sortedNotes.collectAsState() val syncState by viewModel.syncState.collectAsState() val scrollToTop by viewModel.scrollToTop.collectAsState() @@ -100,6 +102,11 @@ fun MainScreen( // πŸ†• v1.8.0: Sync status legend dialog var showSyncLegend by remember { mutableStateOf(false) } + // πŸ”€ v1.8.0: Sort dialog state + var showSortDialog by remember { mutableStateOf(false) } + val sortOption by viewModel.sortOption.collectAsState() + val sortDirection by viewModel.sortDirection.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() val listState = rememberLazyListState() @@ -180,6 +187,7 @@ fun MainScreen( syncEnabled = canSync, showSyncLegend = isSyncAvailable, // πŸ†• v1.8.0: Nur wenn Sync verfΓΌgbar onSyncLegendClick = { showSyncLegend = true }, // πŸ†• v1.8.0 + onSortClick = { showSortDialog = true }, // πŸ”€ v1.8.0 onSyncClick = { viewModel.triggerManualSync("toolbar") }, onSettingsClick = onOpenSettings ) @@ -293,6 +301,21 @@ fun MainScreen( onDismiss = { showSyncLegend = false } ) } + + // πŸ”€ v1.8.0: Sort Dialog + if (showSortDialog) { + SortDialog( + currentOption = sortOption, + currentDirection = sortDirection, + onOptionSelected = { option -> + viewModel.setSortOption(option) + }, + onDirectionToggled = { + viewModel.toggleSortDirection() + }, + onDismiss = { showSortDialog = false } + ) + } } } @@ -302,6 +325,7 @@ private fun MainTopBar( syncEnabled: Boolean, showSyncLegend: Boolean, // πŸ†• v1.8.0: Ob der Hilfe-Button sichtbar sein soll onSyncLegendClick: () -> Unit, // πŸ†• v1.8.0 + onSortClick: () -> Unit, // πŸ”€ v1.8.0: Sort-Button onSyncClick: () -> Unit, onSettingsClick: () -> Unit ) { @@ -313,6 +337,14 @@ private fun MainTopBar( ) }, actions = { + // πŸ”€ v1.8.0: Sort Button + IconButton(onClick = onSortClick) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.Sort, + contentDescription = stringResource(R.string.sort_notes) + ) + } + // πŸ†• v1.8.0: Sync Status Legend Button (nur wenn Sync verfΓΌgbar) if (showSyncLegend) { IconButton(onClick = onSyncLegendClick) { 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 eb02412..9843eaa 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 @@ -5,6 +5,8 @@ import android.content.Context import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import dev.dettmer.simplenotes.models.Note +import dev.dettmer.simplenotes.models.SortDirection +import dev.dettmer.simplenotes.models.SortOption import dev.dettmer.simplenotes.R import dev.dettmer.simplenotes.storage.NotesStorage import dev.dettmer.simplenotes.sync.SyncProgress @@ -20,6 +22,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -102,6 +105,40 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { Logger.d(TAG, "πŸ”„ refreshDisplayMode: displayMode=${_displayMode.value} β†’ $newValue") } + // ═══════════════════════════════════════════════════════════════════════ + // πŸ”€ v1.8.0: Sort State + // ═══════════════════════════════════════════════════════════════════════ + + private val _sortOption = MutableStateFlow( + SortOption.fromPrefsValue( + prefs.getString(Constants.KEY_SORT_OPTION, Constants.DEFAULT_SORT_OPTION) ?: Constants.DEFAULT_SORT_OPTION + ) + ) + val sortOption: StateFlow = _sortOption.asStateFlow() + + private val _sortDirection = MutableStateFlow( + SortDirection.fromPrefsValue( + prefs.getString(Constants.KEY_SORT_DIRECTION, Constants.DEFAULT_SORT_DIRECTION) ?: Constants.DEFAULT_SORT_DIRECTION + ) + ) + val sortDirection: StateFlow = _sortDirection.asStateFlow() + + /** + * πŸ”€ v1.8.0: Sortierte Notizen β€” kombiniert aus Notes + SortOption + SortDirection. + * Reagiert automatisch auf Γ„nderungen in allen drei Flows. + */ + val sortedNotes: StateFlow> = combine( + _notes, + _sortOption, + _sortDirection + ) { notes, option, direction -> + sortNotes(notes, option, direction) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList() + ) + // ═══════════════════════════════════════════════════════════════════════ // Sync State // ═══════════════════════════════════════════════════════════════════════ @@ -688,6 +725,58 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { return true } + // ═══════════════════════════════════════════════════════════════════════ + // πŸ”€ v1.8.0: Sortierung + // ═══════════════════════════════════════════════════════════════════════ + + /** + * πŸ”€ v1.8.0: Sortiert Notizen nach gewΓ€hlter Option und Richtung. + */ + private fun sortNotes( + notes: List, + option: SortOption, + direction: SortDirection + ): List { + val comparator: Comparator = when (option) { + SortOption.UPDATED_AT -> compareBy { it.updatedAt } + SortOption.CREATED_AT -> compareBy { it.createdAt } + SortOption.TITLE -> compareBy(String.CASE_INSENSITIVE_ORDER) { it.title } + SortOption.NOTE_TYPE -> compareBy { it.noteType.ordinal } + .thenByDescending { it.updatedAt } // SekundΓ€r: Datum innerhalb gleicher Typen + } + + return when (direction) { + SortDirection.ASCENDING -> notes.sortedWith(comparator) + SortDirection.DESCENDING -> notes.sortedWith(comparator.reversed()) + } + } + + /** + * πŸ”€ v1.8.0: Setzt die Sortieroption und speichert in SharedPreferences. + */ + fun setSortOption(option: SortOption) { + _sortOption.value = option + prefs.edit().putString(Constants.KEY_SORT_OPTION, option.prefsValue).apply() + Logger.d(TAG, "πŸ”€ Sort option changed to: ${option.prefsValue}") + } + + /** + * πŸ”€ v1.8.0: Setzt die Sortierrichtung und speichert in SharedPreferences. + */ + fun setSortDirection(direction: SortDirection) { + _sortDirection.value = direction + prefs.edit().putString(Constants.KEY_SORT_DIRECTION, direction.prefsValue).apply() + Logger.d(TAG, "πŸ”€ Sort direction changed to: ${direction.prefsValue}") + } + + /** + * πŸ”€ v1.8.0: Toggelt die Sortierrichtung. + */ + fun toggleSortDirection() { + val newDirection = _sortDirection.value.toggle() + setSortDirection(newDirection) + } + // ═══════════════════════════════════════════════════════════════════════ // Helpers // ═══════════════════════════════════════════════════════════════════════ diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SortDialog.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SortDialog.kt new file mode 100644 index 0000000..e1d6652 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SortDialog.kt @@ -0,0 +1,160 @@ +package dev.dettmer.simplenotes.ui.main.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDownward +import androidx.compose.material.icons.filled.ArrowUpward +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.models.SortDirection +import dev.dettmer.simplenotes.models.SortOption + +/** + * πŸ”€ v1.8.0: Dialog zur Auswahl der Sortierung fΓΌr die Notizliste. + * + * Zeigt RadioButtons fΓΌr die Sortieroption und einen Toggle fΓΌr die Richtung. + * + * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + * β”‚ Sort Notes β”‚ + * β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + * β”‚ (●) Last modified ↓↑ β”‚ + * β”‚ ( ) Date created β”‚ + * β”‚ ( ) Name β”‚ + * β”‚ ( ) Type β”‚ + * β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + * β”‚ [Close] β”‚ + * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + */ +@Composable +fun SortDialog( + currentOption: SortOption, + currentDirection: SortDirection, + onOptionSelected: (SortOption) -> Unit, + onDirectionToggled: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.sort_notes), + style = MaterialTheme.typography.headlineSmall + ) + + // Direction Toggle Button + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + IconButton(onClick = onDirectionToggled) { + Icon( + imageVector = if (currentDirection == SortDirection.DESCENDING) { + Icons.Default.ArrowDownward + } else { + Icons.Default.ArrowUpward + }, + contentDescription = stringResource( + if (currentDirection == SortDirection.DESCENDING) { + R.string.sort_descending + } else { + R.string.sort_ascending + } + ), + modifier = Modifier.size(24.dp) + ) + } + Text( + text = stringResource( + if (currentDirection == SortDirection.DESCENDING) { + R.string.sort_descending + } else { + R.string.sort_ascending + } + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + }, + text = { + Column { + HorizontalDivider() + Spacer(modifier = Modifier.height(8.dp)) + + SortOption.entries.forEach { option -> + SortOptionRow( + label = stringResource(option.toStringRes()), + isSelected = currentOption == option, + onClick = { onOptionSelected(option) } + ) + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.close)) + } + } + ) +} + +@Composable +private fun SortOptionRow( + label: String, + isSelected: Boolean, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = isSelected, + onClick = onClick + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = label, + style = MaterialTheme.typography.bodyLarge + ) + } +} + +/** + * Extension: SortOption β†’ String-Resource-ID + */ +fun SortOption.toStringRes(): Int = when (this) { + SortOption.UPDATED_AT -> R.string.sort_by_updated + SortOption.CREATED_AT -> R.string.sort_by_created + SortOption.TITLE -> R.string.sort_by_name + SortOption.NOTE_TYPE -> R.string.sort_by_type +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt index 1d0ec70..30b206b 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt @@ -73,4 +73,10 @@ object Constants { const val DEFAULT_MAX_PARALLEL_DOWNLOADS = 5 const val MIN_PARALLEL_DOWNLOADS = 1 const val MAX_PARALLEL_DOWNLOADS = 10 + + // πŸ”€ v1.8.0: Sortierung + const val KEY_SORT_OPTION = "sort_option" + const val KEY_SORT_DIRECTION = "sort_direction" + const val DEFAULT_SORT_OPTION = "updatedAt" + const val DEFAULT_SORT_DIRECTION = "desc" } diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index b8902ed..d5f2823 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -86,6 +86,25 @@ Sync abgeschlossen Sync fehlgeschlagen + + Notizen sortieren + Aufsteigend + Absteigend + Zuletzt bearbeitet + Erstelldatum + Name + Typ + Schließen + + + Checkliste sortieren + Manuell + A β†’ Z + Z β†’ A + Unerledigte zuerst + Erledigte zuerst + Anwenden + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index f3e565a..d7190f8 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -88,6 +88,25 @@ Checking server… Uploading… Downloading… + + + Sort notes + Ascending + Descending + Last modified + Date created + Name + Type + Close + + + Sort checklist + Manual + A β†’ Z + Z β†’ A + Unchecked first + Checked first + Apply Importing Markdown… Saving… Sync complete