update
This commit is contained in:
@@ -6,7 +6,7 @@ Native App (Kotlin, Jetpack Compose) mit drei Tabs: Kalender, Todos, Notizen. Gl
|
||||
|
||||
- **Kalender:** Monat, Woche, Tag. Tipp auf einen Tag im Monat öffnet den Tag. Langes Drücken auf einen Tag oder eine Stunde: Termin oder Todo erstellen. Kurzer Tipp auf eine Stunde in der Woche: Termin ab dieser Stunde. Tipp auf einen Block bearbeitet, im Dialog steht auch Löschen.
|
||||
- **Todos:** Häkchen erledigt, Tipp auf den Text bearbeitet, Plus legt an.
|
||||
- **Notizen:** Tipp öffnet Ordner oder Notiz. Langes Drücken: Erstellen, Umbenennen, Löschen. In der Notiz bearbeitet ein Tipp auf den Text, gespeichert wird alle 5 Sekunden und bei „Fertig“.
|
||||
- **Notizen:** Tipp öffnet Ordner oder Notiz, der Pfeil zeigt, ob ein Ordner aufgeklappt ist. Langes Drücken: Erstellen, Umbenennen, Löschen. In der Notiz bearbeitet ein Tipp auf den Text, gespeichert wird alle 5 Sekunden und bei „Fertig“. Die Leiste darüber setzt H1 bis H6, Fett, Aufzählung, Nummerierung, Aus- und Einrücken. Enter in einer Liste setzt den nächsten Punkt, Enter auf einem leeren Punkt beendet die Liste.
|
||||
- **Server:** Zugang steckt schon in der APK (aus der `.env` beim Bauen). Hier lässt er sich ändern. Der Sync läuft im Hintergrund, solange die App offen ist.
|
||||
|
||||
## Bauen und installieren
|
||||
|
||||
@@ -6,7 +6,11 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -18,9 +22,13 @@ import androidx.compose.foundation.verticalScroll
|
||||
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.Description
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.automirrored.filled.FormatIndentDecrease
|
||||
import androidx.compose.material.icons.automirrored.filled.FormatIndentIncrease
|
||||
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.FormatBold
|
||||
import androidx.compose.material.icons.filled.FormatListNumbered
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
@@ -41,9 +49,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.marha.planer.data.Editor
|
||||
import de.marha.planer.data.Markdown
|
||||
import de.marha.planer.data.Store
|
||||
import de.marha.planer.data.TreeNode
|
||||
@@ -155,8 +168,12 @@ private fun TreeRow(row: Row, onClick: () -> Unit, onLongClick: () -> Unit) {
|
||||
.padding(start = (12 + row.depth * 20).dp, end = 12.dp, top = 10.dp, bottom = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val icon = if (!row.isFolder) Icons.Filled.Description else if (row.open) Icons.Filled.FolderOpen else Icons.Filled.Folder
|
||||
Icon(icon, contentDescription = null, tint = Muted, modifier = Modifier.padding(end = 10.dp))
|
||||
if (row.isFolder) {
|
||||
val icon = if (row.open) Icons.Filled.KeyboardArrowDown else Icons.AutoMirrored.Filled.KeyboardArrowRight
|
||||
Icon(icon, contentDescription = if (row.open) "aufgeklappt" else "zugeklappt", tint = Muted, modifier = Modifier.padding(end = 10.dp))
|
||||
} else {
|
||||
Spacer(Modifier.width(34.dp))
|
||||
}
|
||||
Text(row.name, style = if (row.isFolder) MaterialTheme.typography.titleMedium else MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
@@ -167,11 +184,11 @@ fun NoteView(store: Store, id: String, onClose: () -> Unit) {
|
||||
store.version.intValue
|
||||
val note = store.note(id)
|
||||
var editing by rememberSaveable { mutableStateOf(false) }
|
||||
var text by rememberSaveable { mutableStateOf(note?.content ?: "") }
|
||||
var value by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(note?.content ?: "")) }
|
||||
val focus = remember { FocusRequester() }
|
||||
|
||||
fun flush() {
|
||||
if (editing) store.writeNote(id, text)
|
||||
if (editing) store.writeNote(id, value.text)
|
||||
}
|
||||
fun stopEdit() {
|
||||
flush()
|
||||
@@ -184,7 +201,8 @@ fun NoteView(store: Store, id: String, onClose: () -> Unit) {
|
||||
LaunchedEffect(editing) {
|
||||
if (editing) {
|
||||
store.lockedNote = id
|
||||
text = store.note(id)?.content ?: ""
|
||||
val content = store.note(id)?.content ?: ""
|
||||
value = TextFieldValue(content, TextRange(content.length))
|
||||
focus.requestFocus()
|
||||
while (true) {
|
||||
delay(AUTOSAVE_MS)
|
||||
@@ -200,9 +218,10 @@ fun NoteView(store: Store, id: String, onClose: () -> Unit) {
|
||||
if (editing) Text("Fertig", color = Orange, modifier = Modifier.clickable { stopEdit() }.padding(12.dp))
|
||||
}
|
||||
if (editing) {
|
||||
EditorBar { change -> value = change(value) }
|
||||
BasicTextField(
|
||||
value = text,
|
||||
onValueChange = { text = it },
|
||||
value = value,
|
||||
onValueChange = { value = Editor.afterInput(value, it) },
|
||||
textStyle = TextStyle(color = MaterialTheme.colorScheme.onBackground, fontFamily = FontFamily.Monospace, fontSize = MaterialTheme.typography.bodyMedium.fontSize),
|
||||
cursorBrush = SolidColor(Orange),
|
||||
modifier = Modifier.fillMaxSize().padding(16.dp).focusRequester(focus),
|
||||
@@ -215,3 +234,26 @@ fun NoteView(store: Store, id: String, onClose: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Werkzeugleiste über dem Editor: Überschriften, Fett, Listen, Ein- und Ausrücken
|
||||
@Composable
|
||||
private fun EditorBar(apply: ((TextFieldValue) -> TextFieldValue) -> Unit) {
|
||||
Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
for (level in 1..6) {
|
||||
Text(
|
||||
"H$level", color = Muted, fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.clickable { apply { Editor.heading(it, level) } }.padding(horizontal = 5.dp, vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
BarIcon(Icons.Filled.FormatBold, "Fett") { apply(Editor::bold) }
|
||||
BarIcon(Icons.AutoMirrored.Filled.FormatListBulleted, "Aufzählung") { apply { Editor.list(it, false) } }
|
||||
BarIcon(Icons.Filled.FormatListNumbered, "Nummerierung") { apply { Editor.list(it, true) } }
|
||||
BarIcon(Icons.AutoMirrored.Filled.FormatIndentDecrease, "Ausrücken") { apply { Editor.indent(it, -1) } }
|
||||
BarIcon(Icons.AutoMirrored.Filled.FormatIndentIncrease, "Einrücken") { apply { Editor.indent(it, 1) } }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BarIcon(icon: ImageVector, label: String, onClick: () -> Unit) {
|
||||
IconButton(onClick = onClick, modifier = Modifier.size(40.dp)) { Icon(icon, contentDescription = label, tint = Muted) }
|
||||
}
|
||||
|
||||
68
android/app/src/test/kotlin/de/marha/planer/EditorTest.kt
Normal file
68
android/app/src/test/kotlin/de/marha/planer/EditorTest.kt
Normal file
@@ -0,0 +1,68 @@
|
||||
package de.marha.planer
|
||||
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import de.marha.planer.data.Editor
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class EditorTest {
|
||||
private fun v(text: String, cursor: Int = text.length, end: Int = cursor) = TextFieldValue(text, TextRange(cursor, end))
|
||||
|
||||
@Test fun continueList() {
|
||||
assertEquals("- ", Editor.continueList("- eins"))
|
||||
assertEquals(" * ", Editor.continueList(" * eins"))
|
||||
assertEquals("- [ ] ", Editor.continueList("- [x] fertig"))
|
||||
assertEquals("4. ", Editor.continueList("3. drei"))
|
||||
assertEquals("", Editor.continueList("- "))
|
||||
assertEquals("", Editor.continueList("2. "))
|
||||
assertNull(Editor.continueList("nur Text"))
|
||||
assertNull(Editor.continueList("---"))
|
||||
}
|
||||
|
||||
@Test fun enterContinuesAndEndsList() {
|
||||
val next = Editor.afterInput(v("- a"), v("- a\n"))
|
||||
assertEquals("- a\n- ", next.text)
|
||||
assertEquals(6, next.selection.start)
|
||||
val ended = Editor.afterInput(v("- a\n- "), v("- a\n- \n"))
|
||||
assertEquals("- a\n", ended.text)
|
||||
assertEquals(4, ended.selection.start)
|
||||
// Umbruch mitten im Text: der Rest wandert hinter den neuen Marker
|
||||
val mid = Editor.afterInput(v("1. ab", 4), v("1. a\nb", 5))
|
||||
assertEquals("1. a\n2. b", mid.text)
|
||||
assertEquals(8, mid.selection.start)
|
||||
assertEquals("x\n", Editor.afterInput(v("x"), v("x\n")).text)
|
||||
}
|
||||
|
||||
@Test fun headings() {
|
||||
val h2 = Editor.heading(v("abc"), 2)
|
||||
assertEquals("## abc", h2.text)
|
||||
assertEquals(6, h2.selection.start)
|
||||
assertEquals("abc", Editor.heading(h2, 2).text)
|
||||
assertEquals("# abc", Editor.heading(h2, 1).text)
|
||||
assertEquals("x\n### y\nz", Editor.heading(v("x\ny\nz", 3), 3).text)
|
||||
}
|
||||
|
||||
@Test fun lists() {
|
||||
val ul = Editor.list(v("abc", 1), false)
|
||||
assertEquals("- abc", ul.text)
|
||||
assertEquals(3, ul.selection.start)
|
||||
assertEquals("abc", Editor.list(ul, false).text)
|
||||
assertEquals("1. abc", Editor.list(ul, true).text)
|
||||
assertEquals("1. x\n2. y", Editor.list(v("1. x\ny"), true).text)
|
||||
assertEquals(" t", Editor.list(v(" - [ ] t"), false).text)
|
||||
}
|
||||
|
||||
@Test fun indentAndBold() {
|
||||
assertEquals(" - a", Editor.indent(v("- a"), 1).text)
|
||||
assertEquals("- a", Editor.indent(v(" - a"), -1).text)
|
||||
assertEquals("- a", Editor.indent(v("- a"), -1).text)
|
||||
val b = Editor.bold(v("abc", 0, 3))
|
||||
assertEquals("**abc**", b.text)
|
||||
assertEquals(TextRange(2, 5), b.selection)
|
||||
val empty = Editor.bold(v("ab", 1))
|
||||
assertEquals("a****b", empty.text)
|
||||
assertEquals(3, empty.selection.start)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user