From 77634392ecc114f8447241ee74fdc53d4bc357c8 Mon Sep 17 00:00:00 2001 From: team3 Date: Fri, 9 Oct 2026 22:27:49 +0200 Subject: [PATCH] update --- .../kotlin/de/marha/planer/MainActivity.kt | 13 +++- .../de/marha/planer/ui/CalendarScreen.kt | 54 ++++++++--------- .../main/kotlin/de/marha/planer/ui/Dialogs.kt | 4 +- .../kotlin/de/marha/planer/ui/NotesScreen.kt | 16 ++--- .../de/marha/planer/ui/SettingsScreen.kt | 15 +++-- .../main/kotlin/de/marha/planer/ui/Theme.kt | 59 +++++++++++++------ .../kotlin/de/marha/planer/ui/TodosScreen.kt | 6 +- 7 files changed, 102 insertions(+), 65 deletions(-) diff --git a/android/app/src/main/kotlin/de/marha/planer/MainActivity.kt b/android/app/src/main/kotlin/de/marha/planer/MainActivity.kt index dbd738b..d7a95b0 100644 --- a/android/app/src/main/kotlin/de/marha/planer/MainActivity.kt +++ b/android/app/src/main/kotlin/de/marha/planer/MainActivity.kt @@ -18,6 +18,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue 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.setValue 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.NotesScreen import de.marha.planer.ui.PlanerTheme +import de.marha.planer.ui.Scheme import de.marha.planer.ui.SettingsScreen import de.marha.planer.ui.TodosScreen import java.io.File @@ -40,8 +43,12 @@ class MainActivity : ComponentActivity() { val dir = File(filesDir, "data") store = Store(dir) sync = Sync(store, getSharedPreferences("sync", Context.MODE_PRIVATE), dir, lifecycleScope) + val prefs = getSharedPreferences("settings", Context.MODE_PRIVATE) 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 -fun App(store: Store, sync: Sync) { +fun App(store: Store, sync: Sync, scheme: Scheme, onScheme: (Scheme) -> Unit) { // Ohne Server-Zugang zuerst die Einstellungen zeigen var tab by rememberSaveable { mutableIntStateOf(if (sync.active) 0 else 3) } Scaffold( @@ -87,7 +94,7 @@ fun App(store: Store, sync: Sync) { 0 -> CalendarScreen(store, modifier) 1 -> TodosScreen(store, modifier) 2 -> NotesScreen(store, modifier) - else -> SettingsScreen(sync, modifier) + else -> SettingsScreen(sync, scheme, onScheme, modifier) } } } diff --git a/android/app/src/main/kotlin/de/marha/planer/ui/CalendarScreen.kt b/android/app/src/main/kotlin/de/marha/planer/ui/CalendarScreen.kt index 8bbc64b..be29bbb 100644 --- a/android/app/src/main/kotlin/de/marha/planer/ui/CalendarScreen.kt +++ b/android/app/src/main/kotlin/de/marha/planer/ui/CalendarScreen.kt @@ -209,7 +209,7 @@ private fun MonthView( Column(Modifier.fillMaxSize().padding(horizontal = 4.dp)) { 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) { Row(Modifier.fillMaxWidth().weight(1f)) { @@ -219,25 +219,25 @@ private fun MonthView( val todos = store.todosOn(day) Box( Modifier.weight(1f).fillMaxSize().padding(1.dp) - .background(Color(0x0AFFFFFF), RoundedCornerShape(6.dp)) - .then(if (day == today) Modifier.border(1.dp, Orange, RoundedCornerShape(6.dp)) else Modifier) + .background(colors.onBackground.copy(alpha = 0.04f), RoundedCornerShape(6.dp)) + .then(if (day == today) Modifier.border(1.dp, colors.primary, RoundedCornerShape(6.dp)) else Modifier) .combinedClickable(onClick = { onDay(day) }, onLongClick = { menuDay = day }) .padding(3.dp), ) { 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) } } + todos.map { t -> ((if (t.done) "☑ " else "☐ ") + (t.time?.let { "$it " } ?: "") + t.title) to { onTodo(t) } } entries.take(2).forEachIndexed { index, (text, click) -> val isTodo = index >= events.size 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() - .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), ) } - 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 }) { DropdownMenuItem(text = { Text("Termin erstellen") }, onClick = { onNewEvent(day); menuDay = null }) @@ -272,21 +272,21 @@ private fun WeekView( Column(Modifier.fillMaxSize()) { 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) { val d = LocalDate.parse(day) 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) } } // Ganztags-Zeile: Blöcke, darunter Todos ohne Uhrzeit. Tipp legt einen ganztägigen Termin an. 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))) { val colWidth = maxWidth / 7 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) }) }) } @@ -294,8 +294,8 @@ private fun WeekView( Box( 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) - .background(Orange.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) } + .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 = colors.onPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis) } } } } @@ -320,8 +320,8 @@ private fun WeekView( }) { for (hour in 0 until 24) { Row(Modifier.fillMaxWidth().height(HOUR_HEIGHT)) { - Text("${pad(hour)}:00", Modifier.width(HOUR_LABEL_WIDTH).padding(top = 2.dp), color = Muted, fontSize = 9.sp) - for (i in 0 until 7) Box(Modifier.weight(1f).fillMaxSize().padding(1.dp).background(Color(0x0AFFFFFF), RoundedCornerShape(3.dp))) + 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(colors.onBackground.copy(alpha = 0.04f), RoundedCornerShape(3.dp))) } } } @@ -334,13 +334,13 @@ private fun WeekView( Box( 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) - .background(Orange.copy(alpha = 0.8f), RoundedCornerShape(4.dp)) - .border(1.dp, Color(0x40FFFFFF), RoundedCornerShape(4.dp)) + .background(colors.primary.copy(alpha = 0.8f), 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), ) { Column { - Text(if (day == block.event.date) block.event.start!! else "…", fontSize = 8.sp, color = Color(0xDDFFFFFF)) - Text(block.event.title, fontSize = 10.sp, color = Color.White, fontWeight = FontWeight.Bold, overflow = TextOverflow.Clip) + 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 = colors.onPrimary, fontWeight = FontWeight.Bold, overflow = TextOverflow.Clip) } } } @@ -348,14 +348,14 @@ private fun WeekView( Box( 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) - .background(TodoBlue.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) } + .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 = colors.onPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis) } } } // Rote Linie an der aktuellen Uhrzeit in der Spalte von heute if (todayIndex >= 0) { 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) LazyColumn(Modifier.fillMaxSize().padding(horizontal = 8.dp)) { 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 -> 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), ) { - 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) } } 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) }) } } } @@ -399,7 +399,7 @@ private fun DayView(store: Store, day: String, onEvent: (Event) -> Unit, onTodo: @Composable private fun SectionHeader(title: String, buttonLabel: String, onClick: () -> Unit) { 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) } } } diff --git a/android/app/src/main/kotlin/de/marha/planer/ui/Dialogs.kt b/android/app/src/main/kotlin/de/marha/planer/ui/Dialogs.kt index 936e728..dc44c8e 100644 --- a/android/app/src/main/kotlin/de/marha/planer/ui/Dialogs.kt +++ b/android/app/src/main/kotlin/de/marha/planer/ui/Dialogs.kt @@ -88,12 +88,12 @@ fun EventDialog(store: Store, event: Event, onDismiss: () -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { TextInput("Titel", title) { title = it } 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 } TimeField(start) { start = it } } 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 } TimeField(end) { end = it } } diff --git a/android/app/src/main/kotlin/de/marha/planer/ui/NotesScreen.kt b/android/app/src/main/kotlin/de/marha/planer/ui/NotesScreen.kt index 6e4a33e..780cc3e 100644 --- a/android/app/src/main/kotlin/de/marha/planer/ui/NotesScreen.kt +++ b/android/app/src/main/kotlin/de/marha/planer/ui/NotesScreen.kt @@ -97,7 +97,7 @@ fun NotesScreen(store: Store, modifier: Modifier = Modifier) { collect(store.tree(), 0) 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()) { items(rows, key = { (if (it.isFolder) "f:" else "n:") + it.path }) { row -> Box { @@ -170,7 +170,7 @@ private fun TreeRow(row: Row, onClick: () -> Unit, onLongClick: () -> Unit) { ) { 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)) + Icon(icon, contentDescription = if (row.open) "aufgeklappt" else "zugeklappt", tint = colors.onSurfaceVariant, modifier = Modifier.padding(end = 10.dp)) } else { 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()) { Row(Modifier.fillMaxWidth().padding(4.dp), verticalAlignment = Alignment.CenterVertically) { 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)) - 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) { EditorBar { change -> value = change(value) } @@ -223,12 +225,12 @@ fun NoteView(store: Store, id: String, modifier: Modifier = Modifier, onClose: ( 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), + cursorBrush = SolidColor(colors.primary), modifier = Modifier.fillMaxSize().padding(16.dp).focusRequester(focus), ) } else { Text( - Markdown.render(note?.content ?: ""), + markdown.render(note?.content ?: ""), 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) { for (level in 1..6) { 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), ) } @@ -255,5 +257,5 @@ private fun EditorBar(apply: ((TextFieldValue) -> TextFieldValue) -> Unit) { @Composable 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) } } diff --git a/android/app/src/main/kotlin/de/marha/planer/ui/SettingsScreen.kt b/android/app/src/main/kotlin/de/marha/planer/ui/SettingsScreen.kt index 3f06ee1..7d8ae63 100644 --- a/android/app/src/main/kotlin/de/marha/planer/ui/SettingsScreen.kt +++ b/android/app/src/main/kotlin/de/marha/planer/ui/SettingsScreen.kt @@ -2,10 +2,12 @@ package de.marha.planer.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button +import androidx.compose.material3.FilterChip import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -19,9 +21,9 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp 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 -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 token by remember { mutableStateOf(sync.token) } var saved by remember { mutableStateOf(false) } @@ -34,11 +36,16 @@ fun SettingsScreen(sync: Sync, modifier: Modifier = Modifier) { visualTransformation = PasswordVisualTransformation(), modifier = Modifier.fillMaxWidth(), ) 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) } 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.", - 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) }) + } } } diff --git a/android/app/src/main/kotlin/de/marha/planer/ui/Theme.kt b/android/app/src/main/kotlin/de/marha/planer/ui/Theme.kt index 6774c94..85c7fba 100644 --- a/android/app/src/main/kotlin/de/marha/planer/ui/Theme.kt +++ b/android/app/src/main/kotlin/de/marha/planer/ui/Theme.kt @@ -1,31 +1,52 @@ package de.marha.planer.ui +import android.content.SharedPreferences +import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -// Dunkel mit Orange, wie die Desktop-Extension -val Orange = Color(0xFFE9672E) -val Panel = Color(0xFF1C1C1C) -val Muted = Color(0xFF999999) -val NowRed = Color(0xFFFF3B30) -val TodoBlue = Color(0xFF8AB4F8) +// Farbschemata wie in Buchwerk: papier, dunkel, nacht, hell. Dunkel ist das bisherige Orange-Design. +// Nacht: reines Schwarz (OLED-Pixel aus) und dunkles Rot, das die Augen im Dunkeln kaum reizt. +enum class Scheme(val label: String) { + Papier("papier"), Dunkel("dunkel"), Nacht("nacht"), Hell("hell"); -private val scheme = darkColorScheme( - primary = Orange, - onPrimary = Color.White, - secondary = TodoBlue, - background = Color(0xFF121212), - onBackground = Color(0xFFE6E6E6), - surface = Panel, - onSurface = Color(0xFFE6E6E6), - surfaceVariant = Color(0xFF2A2A2A), - onSurfaceVariant = Color(0xFFBBBBBB), - outline = Color(0xFF444444), + companion object { + fun load(prefs: SharedPreferences) = entries.firstOrNull { it.name == prefs.getString("scheme", null) } ?: Dunkel + } +} + +private val Orange = Color(0xFFE9672E) + +// Alle Farben stecken im Material-Schema: primary = Akzent, secondary = Todo-Blau, error = Rot, +// background = Hintergrund, surface = Panel, surfaceVariant = Karte, outline = Rand, +// onBackground = Text, onSurfaceVariant = gedämpfter Text. +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 -fun PlanerTheme(content: @Composable () -> Unit) { - MaterialTheme(colorScheme = scheme, content = content) +fun PlanerTheme(scheme: Scheme, content: @Composable () -> Unit) { + MaterialTheme(colorScheme = schemes.getValue(scheme), content = content) } + +// Kurzform in den Screens: colors.primary statt MaterialTheme.colorScheme.primary +val colors: ColorScheme @Composable get() = MaterialTheme.colorScheme diff --git a/android/app/src/main/kotlin/de/marha/planer/ui/TodosScreen.kt b/android/app/src/main/kotlin/de/marha/planer/ui/TodosScreen.kt index 7d72b00..20e0159 100644 --- a/android/app/src/main/kotlin/de/marha/planer/ui/TodosScreen.kt +++ b/android/app/src/main/kotlin/de/marha/planer/ui/TodosScreen.kt @@ -38,7 +38,7 @@ fun TodosScreen(store: Store, modifier: Modifier = Modifier) { Box(modifier.fillMaxSize()) { 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()) { items(todos, key = { it.id }) { todo -> @@ -70,13 +70,13 @@ fun TodoRow(todo: Todo, onToggle: () -> Unit, onEdit: () -> Unit) { Text( todo.title.lineSequence().first(), 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, ) val when_ = todoWhen(todo) if (when_.isNotEmpty()) { 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)) } } }