nordiccalendar

A simple Material 3 native calendar app fitting my personal requirements
git clone git://archive.git.mtrnord.blog/MTRNord/nordiccalendar.git
Log | Files | Refs | README | LICENSE

commit ad97419f8755143761e5cba288d573390005cf25
parent a0cef3250a2ab790da571cb1f34d15d9e3d80305
Author: MTRNord <MTRNord@users.noreply.github.com>
Date:   Mon, 28 Jul 2025 09:59:14 +0200

improve day view

Diffstat:
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt | 11++++++++++-
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/components/AppScaffold.kt | 7+++++--
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 216 insertions(+), 16 deletions(-)

diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt @@ -137,7 +137,16 @@ class MainActivity : ComponentActivity() { modifier = innerPadding, isRefreshing = isRefreshing, onRefresh = { calendarViewModel.updateEvents() }, - navController = navController + navController = navController, + viewModel = calendarViewModel, + startMillis = calendarViewModel.run { + javaClass.getDeclaredField("startMillis") + .apply { isAccessible = true }.get(this) as Long + }, + endMillis = calendarViewModel.run { + javaClass.getDeclaredField("endMillis") + .apply { isAccessible = true }.get(this) as Long + } ) } } diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/components/AppScaffold.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/components/AppScaffold.kt @@ -2,7 +2,7 @@ package space.midnightthoughts.nordiccalendar.components import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api @@ -66,7 +66,10 @@ fun AppScaffold( navigationIcon = { if (onBackClick != null) { IconButton(onClick = onBackClick) { - Icon(Icons.Filled.ArrowBack, contentDescription = "Zurück") + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Zurück" + ) } } else { IconButton(onClick = { scope.launch { drawerState.open() } }) { diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt @@ -4,10 +4,12 @@ import android.annotation.SuppressLint import android.util.Log import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -18,10 +20,15 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab import androidx.compose.material3.TabRow @@ -39,17 +46,20 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.navigation.NavController import kotlinx.coroutines.delay import kotlinx.coroutines.launch import space.midnightthoughts.nordiccalendar.util.Event +import space.midnightthoughts.nordiccalendar.viewmodels.CalendarViewModel import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date import java.util.Locale + @SuppressLint("UnusedBoxWithConstraintsScope") @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -60,7 +70,10 @@ fun CalendarScreen( modifier: Modifier = Modifier, isRefreshing: Boolean, onRefresh: (() -> Unit), - navController: NavController + navController: NavController, + viewModel: CalendarViewModel, // <-- ViewModel als Parameter + startMillis: Long, + endMillis: Long ) { Log.d( "CalendarScreen", @@ -191,35 +204,57 @@ fun CalendarScreen( (maxWidthPx - with(density) { timeColumnWidth.toPx() }) / maxColumns val offsetX = (col * columnWidthPx).toInt() + with(density) { timeColumnWidth.toPx() }.toInt() + val minCardHeightDp = 60.dp + val isCompact = with(density) { eventHeightPx.toDp() } < minCardHeightDp Card( modifier = Modifier .width(with(density) { columnWidthPx.toDp() }) + .defaultMinSize(minHeight = 24.dp) .padding(end = 8.dp) .offset { IntOffset(offsetX, offsetY.toInt()) } .height(with(density) { eventHeightPx.toDp() }) .clickable { navController.navigate("eventDetails/${event.id}") }, - colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + colors = if (isCompact) { + CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.primary + ) + } else CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), border = BorderStroke( 1.dp, color = MaterialTheme.colorScheme.outline ) ) { - Column(modifier = Modifier.padding(12.dp)) { - Text(event.title, style = MaterialTheme.typography.bodyLarge) - Spacer(modifier = Modifier.height(4.dp)) + Column( + modifier = Modifier + .padding( + horizontal = 12.dp, + vertical = if (isCompact) 4.dp else 12.dp, + ) + .fillMaxSize(), + verticalArrangement = if (isCompact) Arrangement.Center else Arrangement.Top, + ) { Text( - hourFormat.format(Date(event.startTime)) + " - " + hourFormat.format( - Date(event.endTime) - ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary + event.title, + overflow = TextOverflow.Ellipsis, + style = if (isCompact) MaterialTheme.typography.bodySmall else MaterialTheme.typography.bodyLarge ) - Spacer(modifier = Modifier.height(16.dp)) - if (!event.description.isNullOrBlank()) { + if (!isCompact) { + Spacer(modifier = Modifier.height(4.dp)) Text( - event.description, + hourFormat.format(Date(event.startTime)) + " - " + hourFormat.format( + Date(event.endTime) + ), style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary ) + Spacer(modifier = Modifier.height(8.dp)) + if (!event.description.isNullOrBlank()) { + Text( + event.description, + style = MaterialTheme.typography.bodyMedium, + overflow = TextOverflow.Ellipsis + ) + } } } } @@ -268,6 +303,55 @@ fun CalendarScreen( } } } + // Zeitraum-Navigation und Titel + val dateFormat = when (selectedTab) { + 0, 1 -> SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()) + 2 -> SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()) + else -> SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()) + } + val rangeText = when (selectedTab) { + 0, 1 -> dateFormat.format(Date(startMillis)) + " – " + dateFormat.format( + Date( + endMillis + ) + ) + + 2 -> dateFormat.format(Date(startMillis)) + else -> "" + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + IconButton(onClick = { + when (selectedTab) { + 0 -> viewModel.prevMonth() + 1 -> viewModel.prevWeek() + 2 -> viewModel.prevDay() + } + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Vorheriger Zeitraum" + ) + } + Text(rangeText, style = MaterialTheme.typography.titleMedium) + IconButton(onClick = { + when (selectedTab) { + 0 -> viewModel.nextMonth() + 1 -> viewModel.nextWeek() + 2 -> viewModel.nextDay() + } + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = "Nächster Zeitraum" + ) + } + } } } } diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt @@ -151,4 +151,108 @@ class CalendarViewModel(app: Application) : AndroidViewModel(app) { isRefreshing.value = false } } + + fun nextDay() { + if (_selectedTab.value != 2) return + val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis } + cal.add(java.util.Calendar.DAY_OF_MONTH, 1) + startMillis = cal.timeInMillis + cal.set(java.util.Calendar.HOUR_OF_DAY, 23) + cal.set(java.util.Calendar.MINUTE, 59) + cal.set(java.util.Calendar.SECOND, 59) + endMillis = cal.timeInMillis + updateEventsCustom() + } + + fun prevDay() { + if (_selectedTab.value != 2) return + val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis } + cal.add(java.util.Calendar.DAY_OF_MONTH, -1) + startMillis = cal.timeInMillis + cal.set(java.util.Calendar.HOUR_OF_DAY, 23) + cal.set(java.util.Calendar.MINUTE, 59) + cal.set(java.util.Calendar.SECOND, 59) + endMillis = cal.timeInMillis + updateEventsCustom() + } + + fun nextWeek() { + if (_selectedTab.value != 1) return + val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis } + cal.add(java.util.Calendar.WEEK_OF_YEAR, 1) + startMillis = cal.timeInMillis + cal.add(java.util.Calendar.DAY_OF_WEEK, 6) + cal.set(java.util.Calendar.HOUR_OF_DAY, 23) + cal.set(java.util.Calendar.MINUTE, 59) + cal.set(java.util.Calendar.SECOND, 59) + endMillis = cal.timeInMillis + updateEventsCustom() + } + + fun prevWeek() { + if (_selectedTab.value != 1) return + val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis } + cal.add(java.util.Calendar.WEEK_OF_YEAR, -1) + startMillis = cal.timeInMillis + cal.add(java.util.Calendar.DAY_OF_WEEK, 6) + cal.set(java.util.Calendar.HOUR_OF_DAY, 23) + cal.set(java.util.Calendar.MINUTE, 59) + cal.set(java.util.Calendar.SECOND, 59) + endMillis = cal.timeInMillis + updateEventsCustom() + } + + fun nextMonth() { + if (_selectedTab.value != 0) return + val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis } + cal.add(java.util.Calendar.MONTH, 1) + cal.set(java.util.Calendar.DAY_OF_MONTH, 1) + startMillis = cal.timeInMillis + cal.set( + java.util.Calendar.DAY_OF_MONTH, + cal.getActualMaximum(java.util.Calendar.DAY_OF_MONTH) + ) + cal.set(java.util.Calendar.HOUR_OF_DAY, 23) + cal.set(java.util.Calendar.MINUTE, 59) + cal.set(java.util.Calendar.SECOND, 59) + endMillis = cal.timeInMillis + updateEventsCustom() + } + + fun prevMonth() { + if (_selectedTab.value != 0) return + val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis } + cal.add(java.util.Calendar.MONTH, -1) + cal.set(java.util.Calendar.DAY_OF_MONTH, 1) + startMillis = cal.timeInMillis + cal.set( + java.util.Calendar.DAY_OF_MONTH, + cal.getActualMaximum(java.util.Calendar.DAY_OF_MONTH) + ) + cal.set(java.util.Calendar.HOUR_OF_DAY, 23) + cal.set(java.util.Calendar.MINUTE, 59) + cal.set(java.util.Calendar.SECOND, 59) + endMillis = cal.timeInMillis + updateEventsCustom() + } + + private fun updateEventsCustom() { + isRefreshing.value = true + val tab = _selectedTab.value + Log.d( + "CalendarViewModel", + "Updating events for selected calendars: ${_selectedCalendars.value.map { it.name }}" + ) + viewModelScope.launch { + val selectedIds = _selectedCalendars.value.map { it.id } + Log.d("CalendarViewModel", "Selected calendar IDs: $selectedIds") + _events.value = calendarData.getEventsForCalendars( + contentResolver, + selectedIds, + startMillis, + endMillis + ) + isRefreshing.value = false + } + } }