This commit is contained in:
team3
2026-10-09 22:27:49 +02:00
parent 1923567c62
commit 77634392ec
7 changed files with 102 additions and 65 deletions

View File

@@ -18,6 +18,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -27,6 +29,7 @@ import de.marha.planer.data.Sync
import de.marha.planer.ui.CalendarScreen import de.marha.planer.ui.CalendarScreen
import de.marha.planer.ui.NotesScreen import de.marha.planer.ui.NotesScreen
import de.marha.planer.ui.PlanerTheme import de.marha.planer.ui.PlanerTheme
import de.marha.planer.ui.Scheme
import de.marha.planer.ui.SettingsScreen import de.marha.planer.ui.SettingsScreen
import de.marha.planer.ui.TodosScreen import de.marha.planer.ui.TodosScreen
import java.io.File import java.io.File
@@ -40,8 +43,12 @@ class MainActivity : ComponentActivity() {
val dir = File(filesDir, "data") val dir = File(filesDir, "data")
store = Store(dir) store = Store(dir)
sync = Sync(store, getSharedPreferences("sync", Context.MODE_PRIVATE), dir, lifecycleScope) sync = Sync(store, getSharedPreferences("sync", Context.MODE_PRIVATE), dir, lifecycleScope)
val prefs = getSharedPreferences("settings", Context.MODE_PRIVATE)
setContent { setContent {
PlanerTheme { App(store, sync) } var scheme by remember { mutableStateOf(Scheme.load(prefs)) }
PlanerTheme(scheme) {
App(store, sync, scheme) { scheme = it; prefs.edit().putString("scheme", it.name).apply() }
}
} }
} }
@@ -65,7 +72,7 @@ private val tabs = listOf(
) )
@Composable @Composable
fun App(store: Store, sync: Sync) { fun App(store: Store, sync: Sync, scheme: Scheme, onScheme: (Scheme) -> Unit) {
// Ohne Server-Zugang zuerst die Einstellungen zeigen // Ohne Server-Zugang zuerst die Einstellungen zeigen
var tab by rememberSaveable { mutableIntStateOf(if (sync.active) 0 else 3) } var tab by rememberSaveable { mutableIntStateOf(if (sync.active) 0 else 3) }
Scaffold( Scaffold(
@@ -87,7 +94,7 @@ fun App(store: Store, sync: Sync) {
0 -> CalendarScreen(store, modifier) 0 -> CalendarScreen(store, modifier)
1 -> TodosScreen(store, modifier) 1 -> TodosScreen(store, modifier)
2 -> NotesScreen(store, modifier) 2 -> NotesScreen(store, modifier)
else -> SettingsScreen(sync, modifier) else -> SettingsScreen(sync, scheme, onScheme, modifier)
} }
} }
} }

View File

@@ -209,7 +209,7 @@ private fun MonthView(
Column(Modifier.fillMaxSize().padding(horizontal = 4.dp)) { Column(Modifier.fillMaxSize().padding(horizontal = 4.dp)) {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
for (name in Dates.WEEKDAYS) Text(name, Modifier.weight(1f), color = Muted, fontSize = 11.sp, textAlign = androidx.compose.ui.text.style.TextAlign.Center) for (name in Dates.WEEKDAYS) Text(name, Modifier.weight(1f), color = colors.onSurfaceVariant, fontSize = 11.sp, textAlign = androidx.compose.ui.text.style.TextAlign.Center)
} }
for (week in 0 until 6) { for (week in 0 until 6) {
Row(Modifier.fillMaxWidth().weight(1f)) { Row(Modifier.fillMaxWidth().weight(1f)) {
@@ -219,25 +219,25 @@ private fun MonthView(
val todos = store.todosOn(day) val todos = store.todosOn(day)
Box( Box(
Modifier.weight(1f).fillMaxSize().padding(1.dp) Modifier.weight(1f).fillMaxSize().padding(1.dp)
.background(Color(0x0AFFFFFF), RoundedCornerShape(6.dp)) .background(colors.onBackground.copy(alpha = 0.04f), RoundedCornerShape(6.dp))
.then(if (day == today) Modifier.border(1.dp, Orange, RoundedCornerShape(6.dp)) else Modifier) .then(if (day == today) Modifier.border(1.dp, colors.primary, RoundedCornerShape(6.dp)) else Modifier)
.combinedClickable(onClick = { onDay(day) }, onLongClick = { menuDay = day }) .combinedClickable(onClick = { onDay(day) }, onLongClick = { menuDay = day })
.padding(3.dp), .padding(3.dp),
) { ) {
Column { Column {
Text(day.substring(8).trimStart('0'), fontWeight = FontWeight.Bold, fontSize = 12.sp, color = if (otherMonth) Color(0xFF777777) else MaterialTheme.colorScheme.onBackground) Text(day.substring(8).trimStart('0'), fontWeight = FontWeight.Bold, fontSize = 12.sp, color = if (otherMonth) colors.onSurfaceVariant else MaterialTheme.colorScheme.onBackground)
val entries = events.map { e -> (if (e.start != null && e.singleDay) "${e.start} ${e.title}" else e.title) to { onEvent(e) } } + val entries = events.map { e -> (if (e.start != null && e.singleDay) "${e.start} ${e.title}" else e.title) to { onEvent(e) } } +
todos.map { t -> ((if (t.done) "☑ " else "☐ ") + (t.time?.let { "$it " } ?: "") + t.title) to { onTodo(t) } } todos.map { t -> ((if (t.done) "☑ " else "☐ ") + (t.time?.let { "$it " } ?: "") + t.title) to { onTodo(t) } }
entries.take(2).forEachIndexed { index, (text, click) -> entries.take(2).forEachIndexed { index, (text, click) ->
val isTodo = index >= events.size val isTodo = index >= events.size
Text( Text(
text, fontSize = 9.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, color = Color.White, text, fontSize = 9.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, color = colors.onPrimary,
modifier = Modifier.padding(top = 2.dp).fillMaxWidth() modifier = Modifier.padding(top = 2.dp).fillMaxWidth()
.background(if (isTodo) TodoBlue.copy(alpha = 0.35f) else Orange.copy(alpha = 0.5f), RoundedCornerShape(3.dp)) .background(if (isTodo) colors.secondary.copy(alpha = 0.35f) else colors.primary.copy(alpha = 0.5f), RoundedCornerShape(3.dp))
.clickable { click() }.padding(horizontal = 3.dp), .clickable { click() }.padding(horizontal = 3.dp),
) )
} }
if (entries.size > 2) Text("+${entries.size - 2}", fontSize = 9.sp, color = Muted) if (entries.size > 2) Text("+${entries.size - 2}", fontSize = 9.sp, color = colors.onSurfaceVariant)
} }
DropdownMenu(expanded = menuDay == day, onDismissRequest = { menuDay = null }) { DropdownMenu(expanded = menuDay == day, onDismissRequest = { menuDay = null }) {
DropdownMenuItem(text = { Text("Termin erstellen") }, onClick = { onNewEvent(day); menuDay = null }) DropdownMenuItem(text = { Text("Termin erstellen") }, onClick = { onNewEvent(day); menuDay = null })
@@ -272,21 +272,21 @@ private fun WeekView(
Column(Modifier.fillMaxSize()) { Column(Modifier.fillMaxSize()) {
Row(Modifier.fillMaxWidth().padding(top = 4.dp)) { Row(Modifier.fillMaxWidth().padding(top = 4.dp)) {
Text("KW ${Dates.isoWeek(days[0])}", Modifier.width(HOUR_LABEL_WIDTH), color = Muted, fontSize = 9.sp) Text("KW ${Dates.isoWeek(days[0])}", Modifier.width(HOUR_LABEL_WIDTH), color = colors.onSurfaceVariant, fontSize = 9.sp)
for (day in days) { for (day in days) {
val d = LocalDate.parse(day) val d = LocalDate.parse(day)
Text("${Dates.WEEKDAYS[d.dayOfWeek.value - 1]} ${d.dayOfMonth}.", Modifier.weight(1f), fontSize = 11.sp, Text("${Dates.WEEKDAYS[d.dayOfWeek.value - 1]} ${d.dayOfMonth}.", Modifier.weight(1f), fontSize = 11.sp,
color = if (day == today) Orange else Color(0xFFCCCCCC), fontWeight = if (day == today) FontWeight.Bold else null, color = if (day == today) colors.primary else colors.onBackground, fontWeight = if (day == today) FontWeight.Bold else null,
textAlign = androidx.compose.ui.text.style.TextAlign.Center) textAlign = androidx.compose.ui.text.style.TextAlign.Center)
} }
} }
// Ganztags-Zeile: Blöcke, darunter Todos ohne Uhrzeit. Tipp legt einen ganztägigen Termin an. // Ganztags-Zeile: Blöcke, darunter Todos ohne Uhrzeit. Tipp legt einen ganztägigen Termin an.
Row(Modifier.fillMaxWidth().padding(vertical = 2.dp)) { Row(Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
Text("ganz", Modifier.width(HOUR_LABEL_WIDTH), color = Muted, fontSize = 9.sp) Text("ganz", Modifier.width(HOUR_LABEL_WIDTH), color = colors.onSurfaceVariant, fontSize = 9.sp)
BoxWithConstraints(Modifier.weight(1f).height(maxOf(22.dp, ALLDAY_LANE * lanes + 4.dp))) { BoxWithConstraints(Modifier.weight(1f).height(maxOf(22.dp, ALLDAY_LANE * lanes + 4.dp))) {
val colWidth = maxWidth / 7 val colWidth = maxWidth / 7
Row(Modifier.fillMaxSize()) { Row(Modifier.fillMaxSize()) {
for (day in days) Box(Modifier.weight(1f).fillMaxSize().padding(1.dp).background(Color(0x0AFFFFFF), RoundedCornerShape(3.dp)).pointerInput(day) { for (day in days) Box(Modifier.weight(1f).fillMaxSize().padding(1.dp).background(colors.onBackground.copy(alpha = 0.04f), RoundedCornerShape(3.dp)).pointerInput(day) {
detectTapGestures(onTap = { onTap(day, null) }, onLongPress = { onLongPress(day, null, it) }) detectTapGestures(onTap = { onTap(day, null) }, onLongPress = { onLongPress(day, null, it) })
}) })
} }
@@ -294,8 +294,8 @@ private fun WeekView(
Box( Box(
Modifier.offset(x = colWidth * lane.from, y = ALLDAY_LANE * lane.lane) Modifier.offset(x = colWidth * lane.from, y = ALLDAY_LANE * lane.lane)
.width(colWidth * (lane.to - lane.from + 1)).height(ALLDAY_LANE - 2.dp).padding(horizontal = 1.dp) .width(colWidth * (lane.to - lane.from + 1)).height(ALLDAY_LANE - 2.dp).padding(horizontal = 1.dp)
.background(Orange.copy(alpha = 0.8f), RoundedCornerShape(3.dp)).clickable { onEvent(lane.event) }.padding(horizontal = 4.dp), .background(colors.primary.copy(alpha = 0.8f), RoundedCornerShape(3.dp)).clickable { onEvent(lane.event) }.padding(horizontal = 4.dp),
) { Text(lane.event.title, fontSize = 10.sp, color = Color.White, maxLines = 1, overflow = TextOverflow.Ellipsis) } ) { Text(lane.event.title, fontSize = 10.sp, color = colors.onPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis) }
} }
} }
} }
@@ -320,8 +320,8 @@ private fun WeekView(
}) { }) {
for (hour in 0 until 24) { for (hour in 0 until 24) {
Row(Modifier.fillMaxWidth().height(HOUR_HEIGHT)) { Row(Modifier.fillMaxWidth().height(HOUR_HEIGHT)) {
Text("${pad(hour)}:00", Modifier.width(HOUR_LABEL_WIDTH).padding(top = 2.dp), color = Muted, fontSize = 9.sp) Text("${pad(hour)}:00", Modifier.width(HOUR_LABEL_WIDTH).padding(top = 2.dp), color = colors.onSurfaceVariant, fontSize = 9.sp)
for (i in 0 until 7) Box(Modifier.weight(1f).fillMaxSize().padding(1.dp).background(Color(0x0AFFFFFF), RoundedCornerShape(3.dp))) for (i in 0 until 7) Box(Modifier.weight(1f).fillMaxSize().padding(1.dp).background(colors.onBackground.copy(alpha = 0.04f), RoundedCornerShape(3.dp)))
} }
} }
} }
@@ -334,13 +334,13 @@ private fun WeekView(
Box( Box(
Modifier.offset(x = HOUR_LABEL_WIDTH + colWidth * dayIndex + width * block.col, y = HOUR_HEIGHT * block.from / 60f) Modifier.offset(x = HOUR_LABEL_WIDTH + colWidth * dayIndex + width * block.col, y = HOUR_HEIGHT * block.from / 60f)
.width(width).height(HOUR_HEIGHT * (block.to - block.from) / 60f - 2.dp).padding(horizontal = 1.dp) .width(width).height(HOUR_HEIGHT * (block.to - block.from) / 60f - 2.dp).padding(horizontal = 1.dp)
.background(Orange.copy(alpha = 0.8f), RoundedCornerShape(4.dp)) .background(colors.primary.copy(alpha = 0.8f), RoundedCornerShape(4.dp))
.border(1.dp, Color(0x40FFFFFF), RoundedCornerShape(4.dp)) .border(1.dp, colors.onPrimary.copy(alpha = 0.25f), RoundedCornerShape(4.dp))
.clickable { onEvent(block.event) }.padding(horizontal = 3.dp, vertical = 1.dp), .clickable { onEvent(block.event) }.padding(horizontal = 3.dp, vertical = 1.dp),
) { ) {
Column { Column {
Text(if (day == block.event.date) block.event.start!! else "…", fontSize = 8.sp, color = Color(0xDDFFFFFF)) Text(if (day == block.event.date) block.event.start!! else "…", fontSize = 8.sp, color = colors.onPrimary.copy(alpha = 0.87f))
Text(block.event.title, fontSize = 10.sp, color = Color.White, fontWeight = FontWeight.Bold, overflow = TextOverflow.Clip) Text(block.event.title, fontSize = 10.sp, color = colors.onPrimary, fontWeight = FontWeight.Bold, overflow = TextOverflow.Clip)
} }
} }
} }
@@ -348,14 +348,14 @@ private fun WeekView(
Box( Box(
Modifier.offset(x = HOUR_LABEL_WIDTH + colWidth * dayIndex, y = HOUR_HEIGHT * Dates.timeToMinutes(todo.time!!) / 60f) Modifier.offset(x = HOUR_LABEL_WIDTH + colWidth * dayIndex, y = HOUR_HEIGHT * Dates.timeToMinutes(todo.time!!) / 60f)
.width(colWidth).height(16.dp).padding(horizontal = 1.dp) .width(colWidth).height(16.dp).padding(horizontal = 1.dp)
.background(TodoBlue.copy(alpha = 0.5f), RoundedCornerShape(3.dp)).clickable { onTodo(todo) }.padding(horizontal = 2.dp), .background(colors.secondary.copy(alpha = 0.5f), RoundedCornerShape(3.dp)).clickable { onTodo(todo) }.padding(horizontal = 2.dp),
) { Text((if (todo.done) "☑ " else "☐ ") + todo.title, fontSize = 9.sp, color = Color.White, maxLines = 1, overflow = TextOverflow.Ellipsis) } ) { Text((if (todo.done) "☑ " else "☐ ") + todo.title, fontSize = 9.sp, color = colors.onPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis) }
} }
} }
// Rote Linie an der aktuellen Uhrzeit in der Spalte von heute // Rote Linie an der aktuellen Uhrzeit in der Spalte von heute
if (todayIndex >= 0) { if (todayIndex >= 0) {
val now = LocalTime.now() val now = LocalTime.now()
Box(Modifier.offset(x = HOUR_LABEL_WIDTH + colWidth * todayIndex, y = HOUR_HEIGHT * (now.hour * 60 + now.minute) / 60f - 1.dp).width(colWidth).height(2.dp).background(NowRed)) Box(Modifier.offset(x = HOUR_LABEL_WIDTH + colWidth * todayIndex, y = HOUR_HEIGHT * (now.hour * 60 + now.minute) / 60f - 1.dp).width(colWidth).height(2.dp).background(colors.error))
} }
} }
} }
@@ -380,18 +380,18 @@ private fun DayView(store: Store, day: String, onEvent: (Event) -> Unit, onTodo:
val todos = store.todosOn(day) val todos = store.todosOn(day)
LazyColumn(Modifier.fillMaxSize().padding(horizontal = 8.dp)) { LazyColumn(Modifier.fillMaxSize().padding(horizontal = 8.dp)) {
item { SectionHeader("Termine", "+ Termin", onNewEvent) } item { SectionHeader("Termine", "+ Termin", onNewEvent) }
if (events.isEmpty()) item { Text("Keine Termine", color = Muted, modifier = Modifier.padding(8.dp)) } if (events.isEmpty()) item { Text("Keine Termine", color = colors.onSurfaceVariant, modifier = Modifier.padding(8.dp)) }
items(events, key = { it.id }) { event -> items(events, key = { it.id }) { event ->
Row( Row(
Modifier.fillMaxWidth().padding(vertical = 2.dp).background(Color(0x0FFFFFFF), RoundedCornerShape(6.dp)).clickable { onEvent(event) }.padding(10.dp), Modifier.fillMaxWidth().padding(vertical = 2.dp).background(colors.onBackground.copy(alpha = 0.06f), RoundedCornerShape(6.dp)).clickable { onEvent(event) }.padding(10.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
Text(timeRange(event), color = Orange, modifier = Modifier.width(110.dp)) Text(timeRange(event), color = colors.primary, modifier = Modifier.width(110.dp))
Text(event.title, fontWeight = FontWeight.Bold) Text(event.title, fontWeight = FontWeight.Bold)
} }
} }
item { SectionHeader("Todos", "+ Todo", onNewTodo) } item { SectionHeader("Todos", "+ Todo", onNewTodo) }
if (todos.isEmpty()) item { Text("Keine Todos", color = Muted, modifier = Modifier.padding(8.dp)) } if (todos.isEmpty()) item { Text("Keine Todos", color = colors.onSurfaceVariant, modifier = Modifier.padding(8.dp)) }
items(todos, key = { it.id }) { todo -> TodoRow(todo, onToggle = { store.toggleTodo(todo.id) }, onEdit = { onTodo(todo) }) } items(todos, key = { it.id }) { todo -> TodoRow(todo, onToggle = { store.toggleTodo(todo.id) }, onEdit = { onTodo(todo) }) }
} }
} }
@@ -399,7 +399,7 @@ private fun DayView(store: Store, day: String, onEvent: (Event) -> Unit, onTodo:
@Composable @Composable
private fun SectionHeader(title: String, buttonLabel: String, onClick: () -> Unit) { private fun SectionHeader(title: String, buttonLabel: String, onClick: () -> Unit) {
Row(Modifier.fillMaxWidth().padding(top = 8.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth().padding(top = 8.dp), verticalAlignment = Alignment.CenterVertically) {
Text(title, color = Muted, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) Text(title, color = colors.onSurfaceVariant, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
TextButton(onClick = onClick) { Text(buttonLabel) } TextButton(onClick = onClick) { Text(buttonLabel) }
} }
} }

View File

@@ -88,12 +88,12 @@ fun EventDialog(store: Store, event: Event, onDismiss: () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
TextInput("Titel", title) { title = it } TextInput("Titel", title) { title = it }
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Von", modifier = Modifier.width(28.dp), color = Muted) Text("Von", modifier = Modifier.width(28.dp), color = colors.onSurfaceVariant)
DateField(date, allowNone = false) { date = it ?: date } DateField(date, allowNone = false) { date = it ?: date }
TimeField(start) { start = it } TimeField(start) { start = it }
} }
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Bis", modifier = Modifier.width(28.dp), color = Muted) Text("Bis", modifier = Modifier.width(28.dp), color = colors.onSurfaceVariant)
DateField(endDate ?: date, allowNone = false) { endDate = it } DateField(endDate ?: date, allowNone = false) { endDate = it }
TimeField(end) { end = it } TimeField(end) { end = it }
} }

View File

@@ -97,7 +97,7 @@ fun NotesScreen(store: Store, modifier: Modifier = Modifier) {
collect(store.tree(), 0) collect(store.tree(), 0)
Box(modifier.fillMaxSize()) { Box(modifier.fillMaxSize()) {
if (rows.isEmpty()) Text("Keine Notizen", color = Muted, modifier = Modifier.padding(16.dp)) if (rows.isEmpty()) Text("Keine Notizen", color = colors.onSurfaceVariant, modifier = Modifier.padding(16.dp))
LazyColumn(Modifier.fillMaxSize()) { LazyColumn(Modifier.fillMaxSize()) {
items(rows, key = { (if (it.isFolder) "f:" else "n:") + it.path }) { row -> items(rows, key = { (if (it.isFolder) "f:" else "n:") + it.path }) { row ->
Box { Box {
@@ -170,7 +170,7 @@ private fun TreeRow(row: Row, onClick: () -> Unit, onLongClick: () -> Unit) {
) { ) {
if (row.isFolder) { if (row.isFolder) {
val icon = if (row.open) Icons.Filled.KeyboardArrowDown else Icons.AutoMirrored.Filled.KeyboardArrowRight 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)) Icon(icon, contentDescription = if (row.open) "aufgeklappt" else "zugeklappt", tint = colors.onSurfaceVariant, modifier = Modifier.padding(end = 10.dp))
} else { } else {
Spacer(Modifier.width(34.dp)) Spacer(Modifier.width(34.dp))
} }
@@ -211,11 +211,13 @@ fun NoteView(store: Store, id: String, modifier: Modifier = Modifier, onClose: (
} }
} }
val scheme = colors
val markdown = remember(scheme) { Markdown(scheme) }
Column(modifier.fillMaxSize()) { Column(modifier.fillMaxSize()) {
Row(Modifier.fillMaxWidth().padding(4.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth().padding(4.dp), verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { if (editing) stopEdit() else onClose() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") } IconButton(onClick = { if (editing) stopEdit() else onClose() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") }
Text(note?.name ?: "", style = MaterialTheme.typography.titleLarge, modifier = Modifier.weight(1f)) Text(note?.name ?: "", style = MaterialTheme.typography.titleLarge, modifier = Modifier.weight(1f))
if (editing) Text("Fertig", color = Orange, modifier = Modifier.clickable { stopEdit() }.padding(12.dp)) if (editing) Text("Fertig", color = colors.primary, modifier = Modifier.clickable { stopEdit() }.padding(12.dp))
} }
if (editing) { if (editing) {
EditorBar { change -> value = change(value) } EditorBar { change -> value = change(value) }
@@ -223,12 +225,12 @@ fun NoteView(store: Store, id: String, modifier: Modifier = Modifier, onClose: (
value = value, value = value,
onValueChange = { value = Editor.afterInput(value, it) }, onValueChange = { value = Editor.afterInput(value, it) },
textStyle = TextStyle(color = MaterialTheme.colorScheme.onBackground, fontFamily = FontFamily.Monospace, fontSize = MaterialTheme.typography.bodyMedium.fontSize), textStyle = TextStyle(color = MaterialTheme.colorScheme.onBackground, fontFamily = FontFamily.Monospace, fontSize = MaterialTheme.typography.bodyMedium.fontSize),
cursorBrush = SolidColor(Orange), cursorBrush = SolidColor(colors.primary),
modifier = Modifier.fillMaxSize().padding(16.dp).focusRequester(focus), modifier = Modifier.fillMaxSize().padding(16.dp).focusRequester(focus),
) )
} else { } else {
Text( Text(
Markdown.render(note?.content ?: ""), markdown.render(note?.content ?: ""),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).clickable { editing = true }.padding(16.dp), modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).clickable { editing = true }.padding(16.dp),
) )
} }
@@ -241,7 +243,7 @@ private fun EditorBar(apply: ((TextFieldValue) -> TextFieldValue) -> Unit) {
Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically) {
for (level in 1..6) { for (level in 1..6) {
Text( Text(
"H$level", color = Muted, fontWeight = FontWeight.Bold, "H$level", color = colors.onSurfaceVariant, fontWeight = FontWeight.Bold,
modifier = Modifier.clickable { apply { Editor.heading(it, level) } }.padding(horizontal = 5.dp, vertical = 12.dp), modifier = Modifier.clickable { apply { Editor.heading(it, level) } }.padding(horizontal = 5.dp, vertical = 12.dp),
) )
} }
@@ -255,5 +257,5 @@ private fun EditorBar(apply: ((TextFieldValue) -> TextFieldValue) -> Unit) {
@Composable @Composable
private fun BarIcon(icon: ImageVector, label: String, onClick: () -> Unit) { private fun BarIcon(icon: ImageVector, label: String, onClick: () -> Unit) {
IconButton(onClick = onClick, modifier = Modifier.size(40.dp)) { Icon(icon, contentDescription = label, tint = Muted) } IconButton(onClick = onClick, modifier = Modifier.size(40.dp)) { Icon(icon, contentDescription = label, tint = colors.onSurfaceVariant) }
} }

View File

@@ -2,10 +2,12 @@ package de.marha.planer.ui
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -19,9 +21,9 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.marha.planer.data.Sync import de.marha.planer.data.Sync
// Server-Zugang: URL und Token, wie sync.json auf dem Desktop. // Server-Zugang (URL und Token, wie sync.json auf dem Desktop) und Farbschema.
@Composable @Composable
fun SettingsScreen(sync: Sync, modifier: Modifier = Modifier) { fun SettingsScreen(sync: Sync, scheme: Scheme, onScheme: (Scheme) -> Unit, modifier: Modifier = Modifier) {
var url by remember { mutableStateOf(sync.url) } var url by remember { mutableStateOf(sync.url) }
var token by remember { mutableStateOf(sync.token) } var token by remember { mutableStateOf(sync.token) }
var saved by remember { mutableStateOf(false) } var saved by remember { mutableStateOf(false) }
@@ -34,11 +36,16 @@ fun SettingsScreen(sync: Sync, modifier: Modifier = Modifier) {
visualTransformation = PasswordVisualTransformation(), modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation(), modifier = Modifier.fillMaxWidth(),
) )
Button(onClick = { sync.configure(url, token); saved = true }) { Text("Speichern") } Button(onClick = { sync.configure(url, token); saved = true }) { Text("Speichern") }
if (saved) Text("Gespeichert. Abgleich läuft.", color = Muted) if (saved) Text("Gespeichert. Abgleich läuft.", color = colors.onSurfaceVariant)
sync.status?.let { Text("Letzter Fehler: $it", color = MaterialTheme.colorScheme.error) } sync.status?.let { Text("Letzter Fehler: $it", color = MaterialTheme.colorScheme.error) }
Text( Text(
"Ohne Zugang läuft die App rein lokal. Der Token steht auf dem Server in der .env und auf dem Desktop in sync.json.", "Ohne Zugang läuft die App rein lokal. Der Token steht auf dem Server in der .env und auf dem Desktop in sync.json.",
color = Muted, style = MaterialTheme.typography.bodySmall, color = colors.onSurfaceVariant, style = MaterialTheme.typography.bodySmall,
) )
Text("Farbschema", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(top = 12.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
for (s in Scheme.entries)
FilterChip(selected = s == scheme, onClick = { onScheme(s) }, label = { Text(s.label) })
}
} }
} }

View File

@@ -1,31 +1,52 @@
package de.marha.planer.ui package de.marha.planer.ui
import android.content.SharedPreferences
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
// Dunkel mit Orange, wie die Desktop-Extension // Farbschemata wie in Buchwerk: papier, dunkel, nacht, hell. Dunkel ist das bisherige Orange-Design.
val Orange = Color(0xFFE9672E) // Nacht: reines Schwarz (OLED-Pixel aus) und dunkles Rot, das die Augen im Dunkeln kaum reizt.
val Panel = Color(0xFF1C1C1C) enum class Scheme(val label: String) {
val Muted = Color(0xFF999999) Papier("papier"), Dunkel("dunkel"), Nacht("nacht"), Hell("hell");
val NowRed = Color(0xFFFF3B30)
val TodoBlue = Color(0xFF8AB4F8)
private val scheme = darkColorScheme( companion object {
primary = Orange, fun load(prefs: SharedPreferences) = entries.firstOrNull { it.name == prefs.getString("scheme", null) } ?: Dunkel
onPrimary = Color.White, }
secondary = TodoBlue, }
background = Color(0xFF121212),
onBackground = Color(0xFFE6E6E6), private val Orange = Color(0xFFE9672E)
surface = Panel,
onSurface = Color(0xFFE6E6E6), // Alle Farben stecken im Material-Schema: primary = Akzent, secondary = Todo-Blau, error = Rot,
surfaceVariant = Color(0xFF2A2A2A), // background = Hintergrund, surface = Panel, surfaceVariant = Karte, outline = Rand,
onSurfaceVariant = Color(0xFFBBBBBB), // onBackground = Text, onSurfaceVariant = gedämpfter Text.
outline = Color(0xFF444444), private fun scheme(
dark: Boolean, bg: Long, panel: Long, card: Long, border: Long, text: Long, dim: Long,
accent: Color, onAccent: Color, red: Long, blue: Long,
): ColorScheme = (if (dark) darkColorScheme() else lightColorScheme()).copy(
primary = accent, onPrimary = onAccent, secondary = Color(blue), error = Color(red),
background = Color(bg), onBackground = Color(text), surface = Color(panel), onSurface = Color(text),
surfaceVariant = Color(card), onSurfaceVariant = Color(dim), outline = Color(border),
surfaceContainer = Color(panel), surfaceContainerHigh = Color(card),
// Auswahl in Tabs, Chips und Schaltergruppen: Karte statt Material-Lila
secondaryContainer = Color(card), onSecondaryContainer = Color(text),
)
private val schemes = mapOf(
Scheme.Dunkel to scheme(true, 0xFF121212, 0xFF1C1C1C, 0xFF2A2A2A, 0xFF444444, 0xFFE6E6E6, 0xFF999999, Orange, Color.White, 0xFFFF6B6B, 0xFF8AB4F8),
Scheme.Hell to scheme(false, 0xFFF4F6F9, 0xFFFFFFFF, 0xFFEEF1F5, 0xFFD5DBE3, 0xFF1D2531, 0xFF5F6B7C, Orange, Color.White, 0xFFC43D3D, 0xFF1B6FD6),
Scheme.Papier to scheme(false, 0xFFEFE6D2, 0xFFF8F1DE, 0xFFEEE3C8, 0xFFD8CAA6, 0xFF43392A, 0xFF6E6142, Orange, Color.White, 0xFFA6442E, 0xFF2B5AA6),
// Nacht: kein Weiß, auch nicht auf dem Akzent
Scheme.Nacht to scheme(true, 0xFF000000, 0xFF000000, 0xFF0D0303, 0xFF2A0A0A, 0xFFB03535, 0xFF6B2222, Color(0xFF7A1C1C), Color(0xFFB03535), 0xFFC0392B, 0xFF9A4A22),
) )
@Composable @Composable
fun PlanerTheme(content: @Composable () -> Unit) { fun PlanerTheme(scheme: Scheme, content: @Composable () -> Unit) {
MaterialTheme(colorScheme = scheme, content = content) MaterialTheme(colorScheme = schemes.getValue(scheme), content = content)
} }
// Kurzform in den Screens: colors.primary statt MaterialTheme.colorScheme.primary
val colors: ColorScheme @Composable get() = MaterialTheme.colorScheme

View File

@@ -38,7 +38,7 @@ fun TodosScreen(store: Store, modifier: Modifier = Modifier) {
Box(modifier.fillMaxSize()) { Box(modifier.fillMaxSize()) {
if (todos.isEmpty()) { if (todos.isEmpty()) {
Text("Keine Todos", color = Muted, modifier = Modifier.padding(16.dp)) Text("Keine Todos", color = colors.onSurfaceVariant, modifier = Modifier.padding(16.dp))
} }
LazyColumn(Modifier.fillMaxSize()) { LazyColumn(Modifier.fillMaxSize()) {
items(todos, key = { it.id }) { todo -> items(todos, key = { it.id }) { todo ->
@@ -70,13 +70,13 @@ fun TodoRow(todo: Todo, onToggle: () -> Unit, onEdit: () -> Unit) {
Text( Text(
todo.title.lineSequence().first(), todo.title.lineSequence().first(),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
color = if (todo.done) Color(0xFF888888) else MaterialTheme.colorScheme.onBackground, color = if (todo.done) colors.onSurfaceVariant else MaterialTheme.colorScheme.onBackground,
textDecoration = if (todo.done) TextDecoration.LineThrough else null, textDecoration = if (todo.done) TextDecoration.LineThrough else null,
) )
val when_ = todoWhen(todo) val when_ = todoWhen(todo)
if (when_.isNotEmpty()) { if (when_.isNotEmpty()) {
val overdue = !todo.done && (todo.date ?: "") < Dates.today() val overdue = !todo.done && (todo.date ?: "") < Dates.today()
Text(when_, color = if (overdue) Color(0xFFFF6B6B) else Muted, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(end = 8.dp)) Text(when_, color = if (overdue) colors.error else colors.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(end = 8.dp))
} }
} }
} }