update
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,12 +120,15 @@ private fun assignLanes(items: List<Lane>): 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<Event?>(null) }
|
||||
var todoDialog by remember { mutableStateOf<Todo?>(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<String>,
|
||||
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<String, String> {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
android/app/src/main/kotlin/de/marha/planer/ui/Settings.kt
Normal file
18
android/app/src/main/kotlin/de/marha/planer/ui/Settings.kt
Normal file
@@ -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() }
|
||||
}
|
||||
@@ -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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user