feat(v1.5.0): Complete NoteEditor Redesign with Jetpack Compose
Features: - Migrate NoteEditorActivity from XML to Jetpack Compose - Support both TEXT and CHECKLIST note types - FOSS native drag & drop using Compose Foundation APIs (no external dependencies) - Auto-keyboard focus with explicit keyboard controller show() calls - Consistent placeholder text for empty checklist items - Unified delete dialog with Server/Local deletion options Components: - ComposeNoteEditorActivity: Activity wrapper with ViewModelFactory for SavedStateHandle - NoteEditorScreen: Main editor screen supporting TEXT and CHECKLIST modes - NoteEditorViewModel: State management with WebDav server deletion support - ChecklistItemRow: Individual checklist item with drag handle, checkbox, text input - DragDropState: FOSS drag & drop implementation using LazyListState Improvements: - Auto-keyboard shows when creating new notes (focuses title/content) - Keyboard consistently shows when adding new checklist items - Placeholder text 'Neues Element…' for empty list items - Delete dialog unified with MainScreen (Server/Local options) - Server deletion via WebDavSyncService.deleteNoteFromServer() Debug Enhancements: - Debug builds have orange icon background (#FFB74D) with red badge - Debug builds show 'Simple Notes (Debug)' app name - Easy differentiation between debug and release APKs in launcher Build Status: - compileStandardDebug: SUCCESS - No breaking changes to existing XML-based screens - Material 3 theming with Dynamic Colors (Material You) applied Migration Notes: - Old NoteEditorActivity (XML-based) remains for reference/backwards compatibility - All editor UI is now Compose-based - ComposeMainActivity updated to use new ComposeNoteEditorActivity - Plan document (v1.5.0_EXTENDED_FEATURES_PLAN.md) updated with implementation details
This commit is contained in:
@@ -44,12 +44,19 @@
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.SimpleNotes" />
|
||||
|
||||
<!-- Editor Activity -->
|
||||
<!-- Editor Activity (Legacy - XML-based) -->
|
||||
<activity
|
||||
android:name=".NoteEditorActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".ui.main.ComposeMainActivity" />
|
||||
|
||||
<!-- Editor Activity v1.5.0 (Jetpack Compose) -->
|
||||
<activity
|
||||
android:name=".ui.editor.ComposeNoteEditorActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".ui.main.ComposeMainActivity"
|
||||
android:theme="@style/Theme.SimpleNotes" />
|
||||
|
||||
<!-- Settings Activity (Legacy - XML-based) -->
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package dev.dettmer.simplenotes.ui.editor
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.AbstractSavedStateViewModelFactory
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dev.dettmer.simplenotes.models.NoteType
|
||||
import dev.dettmer.simplenotes.ui.theme.SimpleNotesTheme
|
||||
|
||||
/**
|
||||
* Compose-based Note Editor Activity
|
||||
*
|
||||
* v1.5.0: Jetpack Compose NoteEditor Redesign
|
||||
* Replaces the old NoteEditorActivity with a modern Compose implementation.
|
||||
*
|
||||
* Supports:
|
||||
* - TEXT notes with title and content
|
||||
* - CHECKLIST notes with drag & drop reordering
|
||||
* - Auto-keyboard focus for new checklist items
|
||||
*/
|
||||
class ComposeNoteEditorActivity : ComponentActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_NOTE_ID = "extra_note_id"
|
||||
const val EXTRA_NOTE_TYPE = "extra_note_type"
|
||||
}
|
||||
|
||||
private val viewModel: NoteEditorViewModel by viewModels {
|
||||
NoteEditorViewModelFactory(
|
||||
application = application,
|
||||
owner = this,
|
||||
noteId = intent.getStringExtra(EXTRA_NOTE_ID),
|
||||
noteType = intent.getStringExtra(EXTRA_NOTE_TYPE) ?: NoteType.TEXT.name
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Apply Dynamic Colors for Android 12+ (Material You)
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
SimpleNotesTheme {
|
||||
NoteEditorScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { finish() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom ViewModelFactory to pass SavedStateHandle with intent extras
|
||||
*/
|
||||
class NoteEditorViewModelFactory(
|
||||
private val application: android.app.Application,
|
||||
owner: SavedStateRegistryOwner,
|
||||
private val noteId: String?,
|
||||
private val noteType: String
|
||||
) : AbstractSavedStateViewModelFactory(owner, null) {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(
|
||||
key: String,
|
||||
modelClass: Class<T>,
|
||||
handle: SavedStateHandle
|
||||
): T {
|
||||
// Populate SavedStateHandle with intent extras
|
||||
handle[NoteEditorViewModel.ARG_NOTE_ID] = noteId
|
||||
handle[NoteEditorViewModel.ARG_NOTE_TYPE] = noteType
|
||||
|
||||
return NoteEditorViewModel(application, handle) as T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package dev.dettmer.simplenotes.ui.editor
|
||||
|
||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* FOSS Drag & Drop State für LazyList
|
||||
*
|
||||
* Native Compose-Implementierung ohne externe Dependencies
|
||||
* v1.5.0: NoteEditor Redesign
|
||||
*/
|
||||
class DragDropListState(
|
||||
private val state: LazyListState,
|
||||
private val scope: CoroutineScope,
|
||||
private val onMove: (Int, Int) -> Unit
|
||||
) {
|
||||
var draggingItemIndex by mutableStateOf<Int?>(null)
|
||||
private set
|
||||
|
||||
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
|
||||
private var draggingItemInitialOffset by mutableFloatStateOf(0f)
|
||||
private var overscrollJob by mutableStateOf<Job?>(null)
|
||||
|
||||
val draggingItemOffset: Float
|
||||
get() = draggingItemLayoutInfo?.let { item ->
|
||||
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
|
||||
} ?: 0f
|
||||
|
||||
private val draggingItemLayoutInfo: LazyListItemInfo?
|
||||
get() = state.layoutInfo.visibleItemsInfo
|
||||
.firstOrNull { it.index == draggingItemIndex }
|
||||
|
||||
fun onDragStart(offset: Offset, itemIndex: Int) {
|
||||
draggingItemIndex = itemIndex
|
||||
draggingItemInitialOffset = draggingItemLayoutInfo?.offset?.toFloat() ?: 0f
|
||||
draggingItemDraggedDelta = 0f
|
||||
}
|
||||
|
||||
fun onDragInterrupted() {
|
||||
draggingItemDraggedDelta = 0f
|
||||
draggingItemIndex = null
|
||||
draggingItemInitialOffset = 0f
|
||||
overscrollJob?.cancel()
|
||||
}
|
||||
|
||||
fun onDrag(offset: Offset) {
|
||||
draggingItemDraggedDelta += offset.y
|
||||
|
||||
val draggingItem = draggingItemLayoutInfo ?: return
|
||||
val startOffset = draggingItem.offset + draggingItemOffset
|
||||
val endOffset = startOffset + draggingItem.size
|
||||
|
||||
val middleOffset = startOffset + (endOffset - startOffset) / 2f
|
||||
|
||||
val targetItem = state.layoutInfo.visibleItemsInfo.find { item ->
|
||||
middleOffset.toInt() in item.offset..item.offsetEnd &&
|
||||
draggingItem.index != item.index
|
||||
}
|
||||
|
||||
if (targetItem != null) {
|
||||
val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) {
|
||||
draggingItem.index
|
||||
} else if (draggingItem.index == state.firstVisibleItemIndex) {
|
||||
targetItem.index
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (scrollToIndex != null) {
|
||||
scope.launch {
|
||||
state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
|
||||
onMove(draggingItem.index, targetItem.index)
|
||||
}
|
||||
} else {
|
||||
onMove(draggingItem.index, targetItem.index)
|
||||
}
|
||||
|
||||
draggingItemIndex = targetItem.index
|
||||
} else {
|
||||
val overscroll = when {
|
||||
draggingItemDraggedDelta > 0 ->
|
||||
(endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
|
||||
draggingItemDraggedDelta < 0 ->
|
||||
(startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
if (overscroll != 0f) {
|
||||
if (overscrollJob?.isActive != true) {
|
||||
overscrollJob = scope.launch {
|
||||
state.scrollBy(overscroll)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
overscrollJob?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val LazyListItemInfo.offsetEnd: Int
|
||||
get() = this.offset + this.size
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberDragDropListState(
|
||||
lazyListState: LazyListState,
|
||||
scope: CoroutineScope,
|
||||
onMove: (Int, Int) -> Unit
|
||||
): DragDropListState {
|
||||
return remember(lazyListState, scope) {
|
||||
DragDropListState(
|
||||
state = lazyListState,
|
||||
scope = scope,
|
||||
onMove = onMove
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.dragContainer(
|
||||
dragDropState: DragDropListState,
|
||||
itemIndex: Int
|
||||
): Modifier {
|
||||
return this.pointerInput(dragDropState) {
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDragStart = { offset ->
|
||||
dragDropState.onDragStart(offset, itemIndex)
|
||||
},
|
||||
onDragEnd = {
|
||||
dragDropState.onDragInterrupted()
|
||||
},
|
||||
onDragCancel = {
|
||||
dragDropState.onDragInterrupted()
|
||||
},
|
||||
onDrag = { change, offset ->
|
||||
change.consume()
|
||||
dragDropState.onDrag(offset)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package dev.dettmer.simplenotes.ui.editor
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.lazy.itemsIndexed
|
||||
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.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.dettmer.simplenotes.R
|
||||
import dev.dettmer.simplenotes.models.NoteType
|
||||
import dev.dettmer.simplenotes.ui.editor.components.ChecklistItemRow
|
||||
import dev.dettmer.simplenotes.ui.main.components.DeleteConfirmationDialog
|
||||
import kotlinx.coroutines.delay
|
||||
import dev.dettmer.simplenotes.utils.showToast
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Main Composable for the Note Editor screen.
|
||||
*
|
||||
* v1.5.0: Jetpack Compose NoteEditor Redesign
|
||||
* - Supports both TEXT and CHECKLIST notes
|
||||
* - Drag & Drop reordering for checklist items
|
||||
* - Auto-keyboard focus for new items
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NoteEditorScreen(
|
||||
viewModel: NoteEditorViewModel,
|
||||
onNavigateBack: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val checklistItems by viewModel.checklistItems.collectAsState()
|
||||
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
var focusNewItemId by remember { mutableStateOf<String?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// v1.5.0: Auto-keyboard support
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val titleFocusRequester = remember { FocusRequester() }
|
||||
val contentFocusRequester = remember { FocusRequester() }
|
||||
|
||||
// v1.5.0: Auto-focus and show keyboard
|
||||
LaunchedEffect(uiState.isNewNote, uiState.noteType) {
|
||||
delay(100) // Wait for layout
|
||||
when {
|
||||
uiState.isNewNote -> {
|
||||
// New note: focus title
|
||||
titleFocusRequester.requestFocus()
|
||||
keyboardController?.show()
|
||||
}
|
||||
!uiState.isNewNote && uiState.noteType == NoteType.TEXT -> {
|
||||
// Editing text note: focus content
|
||||
contentFocusRequester.requestFocus()
|
||||
keyboardController?.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle events
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.events.collect { event ->
|
||||
when (event) {
|
||||
is NoteEditorEvent.ShowToast -> {
|
||||
val message = when (event.message) {
|
||||
ToastMessage.NOTE_IS_EMPTY -> context.getString(R.string.note_is_empty)
|
||||
ToastMessage.NOTE_SAVED -> context.getString(R.string.note_saved)
|
||||
ToastMessage.NOTE_DELETED -> context.getString(R.string.note_deleted)
|
||||
}
|
||||
context.showToast(message)
|
||||
}
|
||||
is NoteEditorEvent.NavigateBack -> onNavigateBack()
|
||||
is NoteEditorEvent.ShowDeleteConfirmation -> showDeleteDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = when (uiState.toolbarTitle) {
|
||||
ToolbarTitle.NEW_NOTE -> stringResource(R.string.new_note)
|
||||
ToolbarTitle.EDIT_NOTE -> stringResource(R.string.edit_note)
|
||||
ToolbarTitle.NEW_CHECKLIST -> stringResource(R.string.new_checklist)
|
||||
ToolbarTitle.EDIT_CHECKLIST -> stringResource(R.string.edit_checklist)
|
||||
}
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
// Delete button (only for existing notes)
|
||||
if (viewModel.canDelete()) {
|
||||
IconButton(onClick = { showDeleteDialog = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.delete)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Save button
|
||||
IconButton(onClick = { viewModel.saveNote() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Save,
|
||||
contentDescription = stringResource(R.string.save)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.imePadding()
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Title Input (for both types)
|
||||
OutlinedTextField(
|
||||
value = uiState.title,
|
||||
onValueChange = { viewModel.updateTitle(it) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(titleFocusRequester),
|
||||
label = { Text(stringResource(R.string.title)) },
|
||||
singleLine = false,
|
||||
maxLines = 2,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
when (uiState.noteType) {
|
||||
NoteType.TEXT -> {
|
||||
// Content Input for TEXT notes
|
||||
TextNoteContent(
|
||||
content = uiState.content,
|
||||
onContentChange = { viewModel.updateContent(it) },
|
||||
focusRequester = contentFocusRequester,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
NoteType.CHECKLIST -> {
|
||||
// Checklist Editor
|
||||
ChecklistEditor(
|
||||
items = checklistItems,
|
||||
scope = scope,
|
||||
focusNewItemId = focusNewItemId,
|
||||
onTextChange = { id, text -> viewModel.updateChecklistItemText(id, text) },
|
||||
onCheckedChange = { id, checked -> viewModel.updateChecklistItemChecked(id, checked) },
|
||||
onDelete = { id -> viewModel.deleteChecklistItem(id) },
|
||||
onAddNewItemAfter = { id ->
|
||||
val newId = viewModel.addChecklistItemAfter(id)
|
||||
focusNewItemId = newId
|
||||
},
|
||||
onAddItemAtEnd = {
|
||||
val newId = viewModel.addChecklistItemAtEnd()
|
||||
focusNewItemId = newId
|
||||
},
|
||||
onMove = { from, to -> viewModel.moveChecklistItem(from, to) },
|
||||
onFocusHandled = { focusNewItemId = null },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Confirmation Dialog - v1.5.0: Use shared component with server/local options
|
||||
if (showDeleteDialog) {
|
||||
DeleteConfirmationDialog(
|
||||
noteCount = 1,
|
||||
onDismiss = { showDeleteDialog = false },
|
||||
onDeleteLocal = {
|
||||
showDeleteDialog = false
|
||||
viewModel.deleteNote(deleteOnServer = false)
|
||||
},
|
||||
onDeleteEverywhere = {
|
||||
showDeleteDialog = false
|
||||
viewModel.deleteNote(deleteOnServer = true)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextNoteContent(
|
||||
content: String,
|
||||
onContentChange: (String) -> Unit,
|
||||
focusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = content,
|
||||
onValueChange = onContentChange,
|
||||
modifier = modifier.focusRequester(focusRequester),
|
||||
label = { Text(stringResource(R.string.content)) },
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChecklistEditor(
|
||||
items: List<ChecklistItemState>,
|
||||
scope: kotlinx.coroutines.CoroutineScope,
|
||||
focusNewItemId: String?,
|
||||
onTextChange: (String, String) -> Unit,
|
||||
onCheckedChange: (String, Boolean) -> Unit,
|
||||
onDelete: (String) -> Unit,
|
||||
onAddNewItemAfter: (String) -> Unit,
|
||||
onAddItemAtEnd: () -> Unit,
|
||||
onMove: (Int, Int) -> Unit,
|
||||
onFocusHandled: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val dragDropState = rememberDragDropListState(
|
||||
lazyListState = listState,
|
||||
scope = scope,
|
||||
onMove = onMove
|
||||
)
|
||||
|
||||
Column(modifier = modifier) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = items,
|
||||
key = { _, item -> item.id }
|
||||
) { index, item ->
|
||||
val isDragging = dragDropState.draggingItemIndex == index
|
||||
val elevation by animateDpAsState(
|
||||
targetValue = if (isDragging) 8.dp else 0.dp,
|
||||
label = "elevation"
|
||||
)
|
||||
|
||||
val shouldFocus = item.id == focusNewItemId
|
||||
|
||||
// v1.5.0: Clear focus request after handling
|
||||
LaunchedEffect(shouldFocus) {
|
||||
if (shouldFocus) {
|
||||
onFocusHandled()
|
||||
}
|
||||
}
|
||||
|
||||
ChecklistItemRow(
|
||||
item = item,
|
||||
onTextChange = { onTextChange(item.id, it) },
|
||||
onCheckedChange = { onCheckedChange(item.id, it) },
|
||||
onDelete = { onDelete(item.id) },
|
||||
onAddNewItem = { onAddNewItemAfter(item.id) },
|
||||
requestFocus = shouldFocus,
|
||||
modifier = Modifier
|
||||
.dragContainer(dragDropState, index)
|
||||
.offset {
|
||||
IntOffset(
|
||||
0,
|
||||
if (isDragging) dragDropState.draggingItemOffset.roundToInt() else 0
|
||||
)
|
||||
}
|
||||
.shadow(elevation, shape = RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add Item Button
|
||||
TextButton(
|
||||
onClick = onAddItemAtEnd,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(stringResource(R.string.add_item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v1.5.0: Local DeleteConfirmationDialog removed - now using shared component from ui/main/components/
|
||||
@@ -0,0 +1,383 @@
|
||||
package dev.dettmer.simplenotes.ui.editor
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.dettmer.simplenotes.models.ChecklistItem
|
||||
import dev.dettmer.simplenotes.models.Note
|
||||
import dev.dettmer.simplenotes.models.NoteType
|
||||
import dev.dettmer.simplenotes.models.SyncStatus
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* ViewModel for NoteEditor Compose Screen
|
||||
* v1.5.0: Jetpack Compose NoteEditor Redesign
|
||||
*
|
||||
* Manages note editing state including title, content, and checklist items.
|
||||
*/
|
||||
class NoteEditorViewModel(
|
||||
application: Application,
|
||||
private val savedStateHandle: SavedStateHandle
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NoteEditorViewModel"
|
||||
const val ARG_NOTE_ID = "noteId"
|
||||
const val ARG_NOTE_TYPE = "noteType"
|
||||
}
|
||||
|
||||
private val storage = NotesStorage(application)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// State
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
private val _uiState = MutableStateFlow(NoteEditorUiState())
|
||||
val uiState: StateFlow<NoteEditorUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val _checklistItems = MutableStateFlow<List<ChecklistItemState>>(emptyList())
|
||||
val checklistItems: StateFlow<List<ChecklistItemState>> = _checklistItems.asStateFlow()
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Events
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
private val _events = MutableSharedFlow<NoteEditorEvent>()
|
||||
val events: SharedFlow<NoteEditorEvent> = _events.asSharedFlow()
|
||||
|
||||
// Internal state
|
||||
private var existingNote: Note? = null
|
||||
private var currentNoteType: NoteType = NoteType.TEXT
|
||||
|
||||
init {
|
||||
loadNote()
|
||||
}
|
||||
|
||||
private fun loadNote() {
|
||||
val noteId = savedStateHandle.get<String>(ARG_NOTE_ID)
|
||||
val noteTypeString = savedStateHandle.get<String>(ARG_NOTE_TYPE) ?: NoteType.TEXT.name
|
||||
|
||||
if (noteId != null) {
|
||||
// Load existing note
|
||||
existingNote = storage.loadNote(noteId)
|
||||
existingNote?.let { note ->
|
||||
currentNoteType = note.noteType
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
title = note.title,
|
||||
content = note.content,
|
||||
noteType = note.noteType,
|
||||
isNewNote = false,
|
||||
toolbarTitle = if (note.noteType == NoteType.CHECKLIST) {
|
||||
ToolbarTitle.EDIT_CHECKLIST
|
||||
} else {
|
||||
ToolbarTitle.EDIT_NOTE
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (note.noteType == NoteType.CHECKLIST) {
|
||||
val items = note.checklistItems?.sortedBy { it.order }?.map {
|
||||
ChecklistItemState(
|
||||
id = it.id,
|
||||
text = it.text,
|
||||
isChecked = it.isChecked,
|
||||
order = it.order
|
||||
)
|
||||
} ?: emptyList()
|
||||
_checklistItems.value = items
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// New note
|
||||
currentNoteType = try {
|
||||
NoteType.valueOf(noteTypeString)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.w(TAG, "Invalid note type '$noteTypeString', defaulting to TEXT")
|
||||
NoteType.TEXT
|
||||
}
|
||||
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
noteType = currentNoteType,
|
||||
isNewNote = true,
|
||||
toolbarTitle = if (currentNoteType == NoteType.CHECKLIST) {
|
||||
ToolbarTitle.NEW_CHECKLIST
|
||||
} else {
|
||||
ToolbarTitle.NEW_NOTE
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Add first empty item for new checklists
|
||||
if (currentNoteType == NoteType.CHECKLIST) {
|
||||
_checklistItems.value = listOf(ChecklistItemState.createEmpty(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Actions
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
fun updateTitle(title: String) {
|
||||
_uiState.update { it.copy(title = title) }
|
||||
}
|
||||
|
||||
fun updateContent(content: String) {
|
||||
_uiState.update { it.copy(content = content) }
|
||||
}
|
||||
|
||||
fun updateChecklistItemText(itemId: String, newText: String) {
|
||||
_checklistItems.update { items ->
|
||||
items.map { item ->
|
||||
if (item.id == itemId) item.copy(text = newText) else item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChecklistItemChecked(itemId: String, isChecked: Boolean) {
|
||||
_checklistItems.update { items ->
|
||||
items.map { item ->
|
||||
if (item.id == itemId) item.copy(isChecked = isChecked) else item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addChecklistItemAfter(afterItemId: String): String {
|
||||
val newItem = ChecklistItemState.createEmpty(0)
|
||||
_checklistItems.update { items ->
|
||||
val index = items.indexOfFirst { it.id == afterItemId }
|
||||
if (index >= 0) {
|
||||
val newList = items.toMutableList()
|
||||
newList.add(index + 1, newItem)
|
||||
// Update order values
|
||||
newList.mapIndexed { i, item -> item.copy(order = i) }
|
||||
} else {
|
||||
items + newItem.copy(order = items.size)
|
||||
}
|
||||
}
|
||||
return newItem.id
|
||||
}
|
||||
|
||||
fun addChecklistItemAtEnd(): String {
|
||||
val newItem = ChecklistItemState.createEmpty(_checklistItems.value.size)
|
||||
_checklistItems.update { items -> items + newItem }
|
||||
return newItem.id
|
||||
}
|
||||
|
||||
fun deleteChecklistItem(itemId: String) {
|
||||
_checklistItems.update { items ->
|
||||
val filtered = items.filter { it.id != itemId }
|
||||
// Ensure at least one item exists
|
||||
if (filtered.isEmpty()) {
|
||||
listOf(ChecklistItemState.createEmpty(0))
|
||||
} else {
|
||||
// Update order values
|
||||
filtered.mapIndexed { index, item -> item.copy(order = index) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun moveChecklistItem(fromIndex: Int, toIndex: Int) {
|
||||
_checklistItems.update { items ->
|
||||
val mutableList = items.toMutableList()
|
||||
val item = mutableList.removeAt(fromIndex)
|
||||
mutableList.add(toIndex, item)
|
||||
// Update order values
|
||||
mutableList.mapIndexed { index, i -> i.copy(order = index) }
|
||||
}
|
||||
}
|
||||
|
||||
fun saveNote() {
|
||||
viewModelScope.launch {
|
||||
val state = _uiState.value
|
||||
val title = state.title.trim()
|
||||
|
||||
when (currentNoteType) {
|
||||
NoteType.TEXT -> {
|
||||
val content = state.content.trim()
|
||||
|
||||
if (title.isEmpty() && content.isEmpty()) {
|
||||
_events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_IS_EMPTY))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val note = if (existingNote != null) {
|
||||
existingNote!!.copy(
|
||||
title = title,
|
||||
content = content,
|
||||
noteType = NoteType.TEXT,
|
||||
checklistItems = null,
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
syncStatus = SyncStatus.PENDING
|
||||
)
|
||||
} else {
|
||||
Note(
|
||||
title = title,
|
||||
content = content,
|
||||
noteType = NoteType.TEXT,
|
||||
checklistItems = null,
|
||||
deviceId = DeviceIdGenerator.getDeviceId(getApplication()),
|
||||
syncStatus = SyncStatus.LOCAL_ONLY
|
||||
)
|
||||
}
|
||||
|
||||
storage.saveNote(note)
|
||||
}
|
||||
|
||||
NoteType.CHECKLIST -> {
|
||||
// Filter empty items
|
||||
val validItems = _checklistItems.value
|
||||
.filter { it.text.isNotBlank() }
|
||||
.mapIndexed { index, item ->
|
||||
ChecklistItem(
|
||||
id = item.id,
|
||||
text = item.text,
|
||||
isChecked = item.isChecked,
|
||||
order = index
|
||||
)
|
||||
}
|
||||
|
||||
if (title.isEmpty() && validItems.isEmpty()) {
|
||||
_events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_IS_EMPTY))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val note = if (existingNote != null) {
|
||||
existingNote!!.copy(
|
||||
title = title,
|
||||
content = "", // Empty for checklists
|
||||
noteType = NoteType.CHECKLIST,
|
||||
checklistItems = validItems,
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
syncStatus = SyncStatus.PENDING
|
||||
)
|
||||
} else {
|
||||
Note(
|
||||
title = title,
|
||||
content = "",
|
||||
noteType = NoteType.CHECKLIST,
|
||||
checklistItems = validItems,
|
||||
deviceId = DeviceIdGenerator.getDeviceId(getApplication()),
|
||||
syncStatus = SyncStatus.LOCAL_ONLY
|
||||
)
|
||||
}
|
||||
|
||||
storage.saveNote(note)
|
||||
}
|
||||
}
|
||||
|
||||
_events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_SAVED))
|
||||
_events.emit(NoteEditorEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current note
|
||||
* @param deleteOnServer if true, also triggers server deletion; if false, only deletes locally
|
||||
* v1.5.0: Added deleteOnServer parameter for unified delete dialog
|
||||
*/
|
||||
fun deleteNote(deleteOnServer: Boolean = true) {
|
||||
viewModelScope.launch {
|
||||
existingNote?.let { note ->
|
||||
val noteId = note.id
|
||||
|
||||
// Delete locally first
|
||||
storage.deleteNote(noteId)
|
||||
|
||||
// Delete from server if requested
|
||||
if (deleteOnServer) {
|
||||
try {
|
||||
val webdavService = WebDavSyncService(getApplication())
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
webdavService.deleteNoteFromServer(noteId)
|
||||
}
|
||||
if (success) {
|
||||
Logger.d(TAG, "Note $noteId deleted from server")
|
||||
} else {
|
||||
Logger.w(TAG, "Failed to delete note $noteId from server")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Error deleting note from server: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
_events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_DELETED))
|
||||
_events.emit(NoteEditorEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showDeleteConfirmation() {
|
||||
viewModelScope.launch {
|
||||
_events.emit(NoteEditorEvent.ShowDeleteConfirmation)
|
||||
}
|
||||
}
|
||||
|
||||
fun canDelete(): Boolean = existingNote != null
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// State Classes
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
data class NoteEditorUiState(
|
||||
val title: String = "",
|
||||
val content: String = "",
|
||||
val noteType: NoteType = NoteType.TEXT,
|
||||
val isNewNote: Boolean = true,
|
||||
val toolbarTitle: ToolbarTitle = ToolbarTitle.NEW_NOTE
|
||||
)
|
||||
|
||||
data class ChecklistItemState(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val text: String = "",
|
||||
val isChecked: Boolean = false,
|
||||
val order: Int = 0
|
||||
) {
|
||||
companion object {
|
||||
fun createEmpty(order: Int): ChecklistItemState {
|
||||
return ChecklistItemState(
|
||||
id = UUID.randomUUID().toString(),
|
||||
text = "",
|
||||
isChecked = false,
|
||||
order = order
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class ToolbarTitle {
|
||||
NEW_NOTE,
|
||||
EDIT_NOTE,
|
||||
NEW_CHECKLIST,
|
||||
EDIT_CHECKLIST
|
||||
}
|
||||
|
||||
enum class ToastMessage {
|
||||
NOTE_IS_EMPTY,
|
||||
NOTE_SAVED,
|
||||
NOTE_DELETED
|
||||
}
|
||||
|
||||
sealed interface NoteEditorEvent {
|
||||
data class ShowToast(val message: ToastMessage) : NoteEditorEvent
|
||||
data object NavigateBack : NoteEditorEvent
|
||||
data object ShowDeleteConfirmation : NoteEditorEvent
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package dev.dettmer.simplenotes.ui.editor.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.dettmer.simplenotes.R
|
||||
import dev.dettmer.simplenotes.ui.editor.ChecklistItemState
|
||||
|
||||
/**
|
||||
* A single row in the checklist editor with drag handle, checkbox, text input, and delete button.
|
||||
*
|
||||
* v1.5.0: Jetpack Compose NoteEditor Redesign
|
||||
*/
|
||||
@Composable
|
||||
fun ChecklistItemRow(
|
||||
item: ChecklistItemState,
|
||||
onTextChange: (String) -> Unit,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onAddNewItem: () -> Unit,
|
||||
requestFocus: Boolean = false,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
var textFieldValue by remember(item.id) {
|
||||
mutableStateOf(TextFieldValue(text = item.text, selection = TextRange(item.text.length)))
|
||||
}
|
||||
|
||||
// v1.5.0: Auto-focus AND show keyboard when requestFocus is true (new items)
|
||||
LaunchedEffect(requestFocus) {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
keyboardController?.show()
|
||||
}
|
||||
}
|
||||
|
||||
// Update text field when external state changes
|
||||
LaunchedEffect(item.text) {
|
||||
if (textFieldValue.text != item.text) {
|
||||
textFieldValue = TextFieldValue(
|
||||
text = item.text,
|
||||
selection = TextRange(item.text.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val alpha = if (item.isChecked) 0.6f else 1.0f
|
||||
val textDecoration = if (item.isChecked) TextDecoration.LineThrough else TextDecoration.None
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Drag Handle
|
||||
Icon(
|
||||
imageVector = Icons.Default.DragHandle,
|
||||
contentDescription = stringResource(R.string.drag_to_reorder),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.alpha(0.5f),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
// Checkbox
|
||||
Checkbox(
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
modifier = Modifier.alpha(alpha)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
// Text Input with placeholder
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { newValue ->
|
||||
// Check for newline (Enter key)
|
||||
if (newValue.text.contains("\n")) {
|
||||
val cleanText = newValue.text.replace("\n", "")
|
||||
textFieldValue = TextFieldValue(
|
||||
text = cleanText,
|
||||
selection = TextRange(cleanText.length)
|
||||
)
|
||||
onTextChange(cleanText)
|
||||
onAddNewItem()
|
||||
} else {
|
||||
textFieldValue = newValue
|
||||
onTextChange(newValue.text)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(focusRequester)
|
||||
.alpha(alpha),
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textDecoration = textDecoration
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = { onAddNewItem() }
|
||||
),
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (textFieldValue.text.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.item_placeholder),
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
// Delete Button
|
||||
IconButton(
|
||||
onClick = onDelete,
|
||||
modifier = Modifier.size(36.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.delete_item),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dev.dettmer.simplenotes.NoteEditorActivity
|
||||
import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity
|
||||
import dev.dettmer.simplenotes.models.NoteType
|
||||
import dev.dettmer.simplenotes.models.SyncStatus
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
@@ -230,17 +230,17 @@ class ComposeMainActivity : ComponentActivity() {
|
||||
|
||||
private fun openNoteEditor(noteId: String?) {
|
||||
cameFromEditor = true
|
||||
val intent = Intent(this, NoteEditorActivity::class.java)
|
||||
val intent = Intent(this, ComposeNoteEditorActivity::class.java)
|
||||
noteId?.let {
|
||||
intent.putExtra(NoteEditorActivity.EXTRA_NOTE_ID, it)
|
||||
intent.putExtra(ComposeNoteEditorActivity.EXTRA_NOTE_ID, it)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun createNote(noteType: NoteType) {
|
||||
cameFromEditor = true
|
||||
val intent = Intent(this, NoteEditorActivity::class.java)
|
||||
intent.putExtra(NoteEditorActivity.EXTRA_NOTE_TYPE, noteType.name)
|
||||
val intent = Intent(this, ComposeNoteEditorActivity::class.java)
|
||||
intent.putExtra(ComposeNoteEditorActivity.EXTRA_NOTE_TYPE, noteType.name)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<string name="content">Inhalt</string>
|
||||
<string name="save">Speichern</string>
|
||||
<string name="delete">Löschen</string>
|
||||
<string name="back">Zurück</string>
|
||||
|
||||
<!-- Note List Item (Preview placeholders) -->
|
||||
<string name="note_title_placeholder">Note Title</string>
|
||||
@@ -81,6 +82,7 @@
|
||||
<string name="add_item">Element hinzufügen</string>
|
||||
<string name="item_placeholder">Neues Element…</string>
|
||||
<string name="reorder_item">Element verschieben</string>
|
||||
<string name="drag_to_reorder">Ziehen zum Sortieren</string>
|
||||
<string name="delete_item">Element löschen</string>
|
||||
<string name="note_is_empty">Notiz ist leer</string>
|
||||
<string name="note_saved">Notiz gespeichert</string>
|
||||
|
||||
Reference in New Issue
Block a user