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 d7a95b0..94f464b 100644 --- a/android/app/src/main/kotlin/de/marha/planer/MainActivity.kt +++ b/android/app/src/main/kotlin/de/marha/planer/MainActivity.kt @@ -18,8 +18,6 @@ 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 @@ -29,7 +27,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.Settings import de.marha.planer.ui.SettingsScreen import de.marha.planer.ui.TodosScreen import java.io.File @@ -43,12 +41,9 @@ 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) + val settings = Settings(getSharedPreferences("settings", Context.MODE_PRIVATE)) setContent { - var scheme by remember { mutableStateOf(Scheme.load(prefs)) } - PlanerTheme(scheme) { - App(store, sync, scheme) { scheme = it; prefs.edit().putString("scheme", it.name).apply() } - } + PlanerTheme(settings.scheme) { App(store, sync, settings) } } } @@ -72,7 +67,7 @@ private val tabs = listOf( ) @Composable -fun App(store: Store, sync: Sync, scheme: Scheme, onScheme: (Scheme) -> Unit) { +fun App(store: Store, sync: Sync, settings: Settings) { // Ohne Server-Zugang zuerst die Einstellungen zeigen var tab by rememberSaveable { mutableIntStateOf(if (sync.active) 0 else 3) } Scaffold( @@ -91,10 +86,10 @@ fun App(store: Store, sync: Sync, scheme: Scheme, onScheme: (Scheme) -> Unit) { ) { padding -> val modifier = Modifier.padding(padding) when (tab) { - 0 -> CalendarScreen(store, modifier) + 0 -> CalendarScreen(store, settings.calendarView, modifier) 1 -> TodosScreen(store, modifier) 2 -> NotesScreen(store, modifier) - else -> SettingsScreen(sync, scheme, onScheme, modifier) + else -> SettingsScreen(sync, settings, 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 be29bbb..134e4d0 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 @@ -120,12 +120,15 @@ private fun assignLanes(items: List): Int { return laneEnds.size } -// Kalender: Monat, Woche, Tag. Tipp bearbeitet oder legt an, langes Drücken öffnet das Menü. +// Ansichten in der Reihenfolge der Schalter. "days4": vier Tage ab dem gewählten Tag. +val CALENDAR_VIEWS = listOf("day" to "Tag", "days4" to "4 Tage", "week" to "Woche", "month" to "Monat") + +// Kalender: Tag, 4 Tage, Woche, Monat. Tipp bearbeitet oder legt an, langes Drücken öffnet das Menü. @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CalendarScreen(store: Store, modifier: Modifier = Modifier) { +fun CalendarScreen(store: Store, startView: String, modifier: Modifier = Modifier) { store.version.intValue - var view by rememberSaveable { mutableStateOf("month") } + var view by rememberSaveable { mutableStateOf(startView) } var cursor by rememberSaveable { mutableStateOf(Dates.today()) } var eventDialog by remember { mutableStateOf(null) } var todoDialog by remember { mutableStateOf(null) } @@ -149,6 +152,7 @@ fun CalendarScreen(store: Store, modifier: Modifier = Modifier) { cursor = when (view) { "month" -> LocalDate.parse(cursor).withDayOfMonth(1).plusMonths(n.toLong()).toString() "week" -> Dates.addDays(cursor, 7L * n) + "days4" -> Dates.addDays(cursor, 4L * n) else -> Dates.addDays(cursor, n.toLong()) } } @@ -157,6 +161,7 @@ fun CalendarScreen(store: Store, modifier: Modifier = Modifier) { val title = when (view) { "month" -> Dates.formatMonth(date.year, date.monthValue) "week" -> Dates.weekDays(cursor).let { "${Dates.formatDayMonth(it[0])} – ${Dates.formatDateInput(it[6])}" } + "days4" -> "${Dates.formatDayMonth(cursor)} – ${Dates.formatDateInput(Dates.addDays(cursor, 3))}" else -> Dates.formatDate(cursor) } @@ -170,13 +175,14 @@ fun CalendarScreen(store: Store, modifier: Modifier = Modifier) { TextButton(onClick = { cursor = Dates.today() }) { Text("Heute") } } SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth().padding(horizontal = 12.dp)) { - listOf("month" to "Monat", "week" to "Woche", "day" to "Tag").forEachIndexed { index, (key, label) -> - SegmentedButton(selected = view == key, onClick = { view = key }, shape = SegmentedButtonDefaults.itemShape(index, 3)) { Text(label) } + CALENDAR_VIEWS.forEachIndexed { index, (key, label) -> + SegmentedButton(selected = view == key, onClick = { view = key }, shape = SegmentedButtonDefaults.itemShape(index, CALENDAR_VIEWS.size)) { Text(label) } } } when (view) { "month" -> MonthView(store, cursor, onDay = { cursor = it; view = "day" }, onEvent = { eventDialog = it }, onTodo = { todoDialog = it }, onNewEvent = { newEvent(it, null) }, onNewTodo = { newTodo(it, null) }) - "week" -> WeekView(store, cursor, onEvent = { eventDialog = it }, onTodo = { todoDialog = it }, onTap = { day, time -> newEvent(day, time) }, onLongPress = { day, time, pos -> openMenu(day, time, pos) }) + "week" -> WeekView(store, Dates.weekDays(cursor), onEvent = { eventDialog = it }, onTodo = { todoDialog = it }, onTap = { day, time -> newEvent(day, time) }, onLongPress = { day, time, pos -> openMenu(day, time, pos) }) + "days4" -> WeekView(store, (0L..3L).map { Dates.addDays(cursor, it) }, onEvent = { eventDialog = it }, onTodo = { todoDialog = it }, onTap = { day, time -> newEvent(day, time) }, onLongPress = { day, time, pos -> openMenu(day, time, pos) }) else -> DayView(store, cursor, onEvent = { eventDialog = it }, onTodo = { todoDialog = it }, onNewEvent = { newEvent(cursor, null) }, onNewTodo = { newTodo(cursor, null) }) } } @@ -254,12 +260,11 @@ private fun MonthView( @Composable private fun WeekView( - store: Store, cursor: String, + store: Store, days: List, onEvent: (Event) -> Unit, onTodo: (Todo) -> Unit, onTap: (String, String?) -> Unit, onLongPress: (String, String?, Offset) -> Unit, ) { store.version.intValue // neu zeichnen, sobald sich Daten ändern (Strong Skipping überspringt sonst) - val days = Dates.weekDays(cursor) val today = Dates.today() val todayIndex = days.indexOf(today) val density = LocalDensity.current @@ -267,7 +272,7 @@ private fun WeekView( // Ganztägige Termine als Blöcke über ihre Tage val allDay = days.flatMap { store.eventsOn(it) }.filter { it.start == null }.distinctBy { it.id } - .map { e -> Lane(e, days.indexOf(e.date).let { if (it < 0) 0 else it }, days.indexOf(e.endDateOrDate).let { if (it < 0) 6 else it }) } + .map { e -> Lane(e, days.indexOf(e.date).let { if (it < 0) 0 else it }, days.indexOf(e.endDateOrDate).let { if (it < 0) days.size - 1 else it }) } val lanes = assignLanes(allDay) Column(Modifier.fillMaxSize()) { @@ -284,7 +289,7 @@ private fun WeekView( Row(Modifier.fillMaxWidth().padding(vertical = 2.dp)) { 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 + val colWidth = maxWidth / days.size Row(Modifier.fillMaxSize()) { 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) }) @@ -302,13 +307,13 @@ private fun WeekView( // Stundenraster mit Terminblöcken, roter Linie und Todos mit Uhrzeit Box(Modifier.weight(1f).verticalScroll(scroll)) { BoxWithConstraints(Modifier.fillMaxWidth().height(HOUR_HEIGHT * 24)) { - val colWidth = (maxWidth - HOUR_LABEL_WIDTH) / 7 + val colWidth = (maxWidth - HOUR_LABEL_WIDTH) / days.size var gridOrigin by remember { mutableStateOf(Offset.Zero) } val hourPx = with(density) { HOUR_HEIGHT.toPx() } val colPx = with(density) { colWidth.toPx() } val labelPx = with(density) { HOUR_LABEL_WIDTH.toPx() } fun cellAt(offset: Offset): Pair { - val dayIndex = ((offset.x - labelPx) / colPx).toInt().coerceIn(0, 6) + val dayIndex = ((offset.x - labelPx) / colPx).toInt().coerceIn(0, days.size - 1) val hour = (offset.y / hourPx).toInt().coerceIn(0, 23) return days[dayIndex] to "${pad(hour)}:00" } @@ -321,7 +326,7 @@ 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 = 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))) + for (i in days.indices) Box(Modifier.weight(1f).fillMaxSize().padding(1.dp).background(colors.onBackground.copy(alpha = 0.04f), RoundedCornerShape(3.dp))) } } } diff --git a/android/app/src/main/kotlin/de/marha/planer/ui/Settings.kt b/android/app/src/main/kotlin/de/marha/planer/ui/Settings.kt new file mode 100644 index 0000000..95386b1 --- /dev/null +++ b/android/app/src/main/kotlin/de/marha/planer/ui/Settings.kt @@ -0,0 +1,18 @@ +package de.marha.planer.ui + +import android.content.SharedPreferences +import androidx.compose.runtime.mutableStateOf + +// Einstellungen der App, in SharedPreferences gemerkt. Compose-State: eine Änderung zeichnet neu. +class Settings(private val prefs: SharedPreferences) { + private val _scheme = mutableStateOf(Scheme.load(prefs)) + var scheme: Scheme + get() = _scheme.value + set(value) { _scheme.value = value; prefs.edit().putString("scheme", value.name).apply() } + + // Ansicht, mit der der Kalender startet: Schlüssel aus CALENDAR_VIEWS + private val _calendarView = mutableStateOf(prefs.getString("calendarView", null) ?: "days4") + var calendarView: String + get() = _calendarView.value + set(value) { _calendarView.value = value; prefs.edit().putString("calendarView", value).apply() } +} 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 7d8ae63..1469084 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 @@ -21,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) und Farbschema. +// Server-Zugang (URL und Token, wie sync.json auf dem Desktop), Farbschema und Kalender-Startansicht. @Composable -fun SettingsScreen(sync: Sync, scheme: Scheme, onScheme: (Scheme) -> Unit, modifier: Modifier = Modifier) { +fun SettingsScreen(sync: Sync, settings: Settings, modifier: Modifier = Modifier) { var url by remember { mutableStateOf(sync.url) } var token by remember { mutableStateOf(sync.token) } var saved by remember { mutableStateOf(false) } @@ -45,7 +45,12 @@ fun SettingsScreen(sync: Sync, scheme: Scheme, onScheme: (Scheme) -> Unit, modif 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) }) + FilterChip(selected = s == settings.scheme, onClick = { settings.scheme = s }, label = { Text(s.label) }) + } + Text("Kalender startet mit", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(top = 12.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + for ((key, label) in CALENDAR_VIEWS) + FilterChip(selected = key == settings.calendarView, onClick = { settings.calendarView = key }, label = { Text(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 85c7fba..913a0f7 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 @@ -33,6 +33,8 @@ private fun scheme( surfaceContainer = Color(panel), surfaceContainerHigh = Color(card), // Auswahl in Tabs, Chips und Schaltergruppen: Karte statt Material-Lila secondaryContainer = Color(card), onSecondaryContainer = Color(text), + // Plus-Knopf (FloatingActionButton) nimmt primaryContainer + primaryContainer = accent, onPrimaryContainer = onAccent, ) private val schemes = mapOf( diff --git a/desktop@marha.de/calendar.js b/desktop@marha.de/calendar.js index f8458e0..9084508 100644 --- a/desktop@marha.de/calendar.js +++ b/desktop@marha.de/calendar.js @@ -5,6 +5,7 @@ import Pango from 'gi://Pango'; import St from 'gi://St'; import * as D from './dates.js'; +import {readJson, writeJson} from './store.js'; import {editTodo, todoClick, todoRow, todoTitle} from './todos.js'; import {button, contextMenu, dropdown, openDatePicker, openTimePicker, Popover, scrollBox, textButton} from './widgets.js'; @@ -160,7 +161,9 @@ class CalendarPanel extends St.BoxLayout { _init(store) { super._init({vertical: true, style_class: 'desktop-panel calendar', reactive: true, x_expand: true, y_expand: false}); this._store = store; - this._view = 'month'; + // Die zuletzt gewählte Ansicht bleibt über Neustarts erhalten + this._settingsPath = GLib.build_filenamev([store.dir, 'settings.json']); + this._view = readJson(this._settingsPath, {}).calendarView ?? 'month'; this._cursor = D.today(); this._drag = null; this._slots = []; @@ -185,7 +188,7 @@ class CalendarPanel extends St.BoxLayout { header.add_child(new Clutter.Actor({x_expand: true})); this._viewButtons = {}; - for (const [view, text] of [['month', 'Monat'], ['week', 'Woche'], ['day', 'Tag']]) { + for (const [view, text] of [['day', 'Tag'], ['week', 'Woche'], ['month', 'Monat']]) { const b = button(text, () => this._go(this._cursor, view), 'desktop-button view-button'); this._viewButtons[view] = b; header.add_child(b); @@ -195,6 +198,8 @@ class CalendarPanel extends St.BoxLayout { _go(date, view = this._view) { this._cursor = date; + if (view !== this._view) + writeJson(this._settingsPath, {...readJson(this._settingsPath, {}), calendarView: view}); this._view = view; this._render(); }