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
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Note> {
|
||||
return notesDir.listFiles()
|
||||
?.filter { it.extension == "json" }
|
||||
?.mapNotNull { Note.fromJson(it.readText()) }
|
||||
?.sortedByDescending { it.updatedAt }
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String?>(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<ChecklistItemState>,
|
||||
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,11 +420,15 @@ 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
|
||||
) {
|
||||
TextButton(onClick = onAddItemAtEnd) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
@@ -408,6 +436,15 @@ private fun ChecklistEditor(
|
||||
)
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Boolean> = _isOfflineMode.asStateFlow()
|
||||
|
||||
// 🔀 v1.8.0 (IMPL_020): Letzte Checklist-Sortierung (Session-Scope)
|
||||
private val _lastChecklistSortOption = MutableStateFlow(ChecklistSortOption.MANUAL)
|
||||
val lastChecklistSortOption: StateFlow<ChecklistSortOption> = _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
|
||||
// 🆕 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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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> = _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> = _sortDirection.asStateFlow()
|
||||
|
||||
/**
|
||||
* 🔀 v1.8.0: Sortierte Notizen — kombiniert aus Notes + SortOption + SortDirection.
|
||||
* Reagiert automatisch auf Änderungen in allen drei Flows.
|
||||
*/
|
||||
val sortedNotes: StateFlow<List<Note>> = 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<Note>,
|
||||
option: SortOption,
|
||||
direction: SortDirection
|
||||
): List<Note> {
|
||||
val comparator: Comparator<Note> = 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<Note> { 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
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -86,6 +86,25 @@
|
||||
<string name="sync_phase_completed">Sync abgeschlossen</string>
|
||||
<string name="sync_phase_error">Sync fehlgeschlagen</string>
|
||||
|
||||
<!-- 🔀 v1.8.0 (IMPL_020): Sortierung: Notizliste -->
|
||||
<string name="sort_notes">Notizen sortieren</string>
|
||||
<string name="sort_ascending">Aufsteigend</string>
|
||||
<string name="sort_descending">Absteigend</string>
|
||||
<string name="sort_by_updated">Zuletzt bearbeitet</string>
|
||||
<string name="sort_by_created">Erstelldatum</string>
|
||||
<string name="sort_by_name">Name</string>
|
||||
<string name="sort_by_type">Typ</string>
|
||||
<string name="close">Schließen</string>
|
||||
|
||||
<!-- 🔀 v1.8.0 (IMPL_020): Sortierung: Checkliste -->
|
||||
<string name="sort_checklist">Checkliste sortieren</string>
|
||||
<string name="sort_checklist_manual">Manuell</string>
|
||||
<string name="sort_checklist_alpha_asc">A → Z</string>
|
||||
<string name="sort_checklist_alpha_desc">Z → A</string>
|
||||
<string name="sort_checklist_unchecked_first">Unerledigte zuerst</string>
|
||||
<string name="sort_checklist_checked_first">Erledigte zuerst</string>
|
||||
<string name="apply">Anwenden</string>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- DELETE DIALOGS -->
|
||||
<!-- ============================= -->
|
||||
|
||||
@@ -88,6 +88,25 @@
|
||||
<string name="sync_phase_checking">Checking server…</string>
|
||||
<string name="sync_phase_uploading">Uploading…</string>
|
||||
<string name="sync_phase_downloading">Downloading…</string>
|
||||
|
||||
<!-- 🔀 v1.8.0 (IMPL_020): Sort: Note List -->
|
||||
<string name="sort_notes">Sort notes</string>
|
||||
<string name="sort_ascending">Ascending</string>
|
||||
<string name="sort_descending">Descending</string>
|
||||
<string name="sort_by_updated">Last modified</string>
|
||||
<string name="sort_by_created">Date created</string>
|
||||
<string name="sort_by_name">Name</string>
|
||||
<string name="sort_by_type">Type</string>
|
||||
<string name="close">Close</string>
|
||||
|
||||
<!-- 🔀 v1.8.0 (IMPL_020): Sort: Checklist -->
|
||||
<string name="sort_checklist">Sort checklist</string>
|
||||
<string name="sort_checklist_manual">Manual</string>
|
||||
<string name="sort_checklist_alpha_asc">A → Z</string>
|
||||
<string name="sort_checklist_alpha_desc">Z → A</string>
|
||||
<string name="sort_checklist_unchecked_first">Unchecked first</string>
|
||||
<string name="sort_checklist_checked_first">Checked first</string>
|
||||
<string name="apply">Apply</string>
|
||||
<string name="sync_phase_importing_markdown">Importing Markdown…</string>
|
||||
<string name="sync_phase_saving">Saving…</string>
|
||||
<string name="sync_phase_completed">Sync complete</string>
|
||||
|
||||
Reference in New Issue
Block a user