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 554039f55ad7f844d041bbb4b45739ec6f168459
parent a021cca297cfb867eabb204d3a25fb7abe762519
Author: MTRNord <MTRNord@users.noreply.github.com>
Date:   Fri,  1 Aug 2025 21:08:39 +0200

Add all the docs!

Diffstat:
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt | 46+++++++++++++++++++++++++++++++++++++++++++---
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/NordicCalendarApp.kt | 37+++++++++++++++++++++++++++++++++++++
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/background/CalendarSyncWorker.kt | 29++++++++++++++++++++++++++---
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/components/AppScaffold.kt | 25+++++++++++++++++++++++++
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/components/DateRangeHeader.kt | 14+++++++++++++-
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/components/SidebarDrawer.kt | 12+++++++++++-
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/notifications/NotificationHelper.kt | 47+++++++++++++++++++++++++++++++++++++----------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/notifications/NotificationReceiver.kt | 13++++++++++++-
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/onboarding/Onboarding.kt | 19+++++++++++++++++++
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt | 26++++++++++++++++++++++++++
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/DayView.kt | 61++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt | 31+++++++++++++++++++++++++++----
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/MonthView.kt | 31+++++++++++++++++++++++++------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/SettingsView.kt | 7+++++++
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/WeekView.kt | 8++++++++
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/util/CalendarData.kt | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/util/OnboardingPrefs.kt | 23++++++++++++++++++++++-
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt | 120++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt | 40+++++++++++++++++++++++++++++++++++++++-
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SettingsViewModel.kt | 6++++++
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt | 16++++++++++++++++
Aapp/src/main/res/drawable/outline_home_pin_24.xml | 5+++++
23 files changed, 771 insertions(+), 58 deletions(-)

diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt @@ -51,6 +51,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import androidx.navigation.navDeepLink import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState import dagger.hilt.android.AndroidEntryPoint @@ -64,16 +65,35 @@ import space.midnightthoughts.nordiccalendar.screens.SettingsView import space.midnightthoughts.nordiccalendar.ui.theme.NordicCalendarTheme import space.midnightthoughts.nordiccalendar.util.OnboardingPrefs +/** + * Destinations for the app's navigation. + */ sealed class Destinations(val route: String) { object Intro : Destinations("intro") - object Calendar : Destinations("calendar?tab={tab}") // Route mit optionalem Tab-Argument + + /** + * Calendar destination with optional parameters for tab and date. + * @param tab The index of the selected tab (default is 0). + * @param date The date to display in the calendar (optional). + */ + object Calendar : Destinations("calendar?tab={tab}") object Settings : Destinations("settings") object About : Destinations("about") object EventDetails : Destinations("eventDetails/{eventId}") } +/** + * MainActivity is the entry point of the Nordic Calendar app. + * It sets up the navigation and UI components, including onboarding and calendar views. + */ @AndroidEntryPoint class MainActivity : ComponentActivity() { + /** + * onCreate is called when the activity is created. + * It initializes the UI and sets up navigation. + * + * @param savedInstanceState The saved instance state bundle. + */ @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -166,7 +186,9 @@ class MainActivity : ComponentActivity() { } ), deepLinks = listOf( - androidx.navigation.NavDeepLink("nordiccalendar://eventdetails/{eventId}") + navDeepLink { + uriPattern = "nordiccalendar://eventdetails/{eventId}" + } ) ) { backStackEntry -> EventDetailsView( @@ -192,7 +214,13 @@ class MainActivity : ComponentActivity() { } -// Pass Navigation Actions: Create a function to handle navigation and pass it to screens. +/** + * IntroScreen displays the onboarding screens for the app. + * It allows users to navigate through the onboarding items and finish the onboarding process. + * + * @param navController The NavHostController for navigation actions. + * @param onFinish Optional callback when the user finishes the onboarding. + */ @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun IntroScreen(navController: NavHostController, onFinish: (() -> Unit)? = null) { @@ -293,6 +321,12 @@ fun IntroScreen(navController: NavHostController, onFinish: (() -> Unit)? = null } } +/** + * CalendarView displays the main calendar screen with a floating action button to add events. + * It uses the CalendarScreen composable to render the calendar UI. + * + * @param navController The NavHostController for navigation actions. + */ @Composable fun CalendarView( navController: NavHostController @@ -326,6 +360,12 @@ fun CalendarView( } } +/** + * AboutView displays information about the app and its version. + * It provides a simple text view with the app name and version. + * + * @param navController The NavHostController for navigation actions. + */ @Composable fun AboutView(navController: NavHostController) { diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/NordicCalendarApp.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/NordicCalendarApp.kt @@ -18,19 +18,44 @@ import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Inject +/** + * NordicCalendarApp is the main application class for the Nordic Calendar app. + * It initializes Hilt for dependency injection, sets up WorkManager for periodic calendar synchronization, + * and registers a content observer for calendar changes. + * + * @property workerFactory The HiltWorkerFactory for creating Hilt-enabled workers. + * @property calendarRepository The repository for managing calendar data and operations. + */ @HiltAndroidApp class NordicCalendarApp : Application(), Configuration.Provider { + /** + * HiltWorkerFactory for creating workers with Hilt dependencies. + * Injected by Hilt. + */ @Inject lateinit var workerFactory: HiltWorkerFactory + /** + * CalendarRepository for managing calendar data and operations. + * Injected by Hilt. + */ @Inject lateinit var calendarRepository: CalendarRepository + /** + * Provides the WorkManager configuration with the HiltWorkerFactory. + * This is used to create workers that can inject dependencies via Hilt. + */ override val workManagerConfiguration: Configuration get() = Configuration.Builder() .setWorkerFactory(workerFactory) .build() + /** + * Called when the application is created. + * This method sets up the periodic calendar synchronization work and registers a content observer + * for calendar changes. + */ override fun onCreate() { super.onCreate() // WorkManager für periodische Kalender-Synchronisation einrichten @@ -55,6 +80,10 @@ class NordicCalendarApp : Application(), Configuration.Provider { calendarRepository.registerCalendarContentObserver(this) } + /** + * Called when the application is terminated. + * This method unregisters the calendar content observer to avoid memory leaks. + */ override fun onTerminate() { super.onTerminate() // CalendarContentObserver abmelden, um Speicherlecks zu vermeiden @@ -63,6 +92,14 @@ class NordicCalendarApp : Application(), Configuration.Provider { } } +/** + * Returns the current application locale based on the Android version. + * For Android Tiramisu (API 33) and above, it uses LocaleManager to get the application locales. + * For earlier versions, it falls back to Locale.getDefault(). + * + * @param context The application context. + * @return The current application locale. + */ fun getCurrentAppLocale(context: Context): Locale { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val localeManager = context.getSystemService(LocaleManager::class.java) diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/background/CalendarSyncWorker.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/background/CalendarSyncWorker.kt @@ -1,3 +1,14 @@ +// CalendarSyncWorker.kt +// +// This file defines the CalendarSyncWorker class, a background worker for synchronizing calendar events and reminders in the NordicCalendar app. It uses Hilt for dependency injection and interacts with the CalendarRepository to fetch calendars, retrieve events, and manage reminders. +// +// Classes: +// CalendarSyncWorker: A Worker subclass that performs calendar synchronization tasks in the background. It fetches the current and next day's events from selected calendars and updates reminders accordingly. +// +// Interfaces: +// RepositoryEntryPoint: Hilt entry point interface to provide CalendarRepository dependency. +// + package space.midnightthoughts.nordiccalendar.background import android.content.Context @@ -18,27 +29,39 @@ class CalendarSyncWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted workerParams: WorkerParameters ) : Worker(appContext, workerParams) { + /** + * Hilt entry point interface to provide CalendarRepository dependency + */ @EntryPoint @InstallIn(SingletonComponent::class) interface RepositoryEntryPoint { fun calendarRepository(): CalendarRepository } + /** + * Performs the background calendar synchronization work. + * + * Steps: + * 1. Retrieves the CalendarRepository via Hilt entry point. + * 2. Sets the time range for event retrieval (now to 24h later). + * 3. Fetches all calendars and filters for selected ones. + * 4. Retrieves events for selected calendars within the time range. + * 5. Cancels existing reminders and schedules new ones for these events. + * + * @return Result of the work (success or failure) + */ override fun doWork(): Result { Log.d("CalendarSyncWorker", "Starting calendar sync work") val repo = EntryPointAccessors.fromApplication( applicationContext, RepositoryEntryPoint::class.java ).calendarRepository() - // Zeitraum: jetzt bis 24h später val now = System.currentTimeMillis() val tomorrow = now + 24 * 60 * 60 * 1000 repo.setTimeRange(now, tomorrow) - // Events für diesen Zeitraum holen val calendars = repo.getCalendars(applicationContext) val selectedIds = calendars.filter { it.selected }.map { it.id } val events = repo.getEventsForCalendars(applicationContext, selectedIds, now, tomorrow) - // Reminder-Benachrichtigungen vorher löschen und neu setzen repo.cancelRemindersForEvents(applicationContext, events) repo.scheduleRemindersForEvents(applicationContext, events) Log.d("CalendarSyncWorker", "Calendar sync work completed, ${events.size} events processed") diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/components/AppScaffold.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/components/AppScaffold.kt @@ -27,6 +27,19 @@ import androidx.navigation.NavController import kotlinx.coroutines.launch import space.midnightthoughts.nordiccalendar.R +/** + * AppScaffoldContent is a composable function that provides the main scaffold structure for the app, + * including a top app bar, optional back button, menu button, floating action button, and content area. + * + * @param title The title to display in the top app bar. + * @param isBackButtonVisible Whether the back button should be visible. + * @param navController The NavController for navigation actions. + * @param onBackClick Optional callback for back button click. + * @param floatingActionButton Optional composable for a floating action button. + * @param content The main content composable, receives a Modifier. + * @param onMenuClick Optional callback for menu button click. + * @param actions Optional composable for additional actions in the top app bar. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AppScaffoldContent( @@ -83,6 +96,18 @@ private fun AppScaffoldContent( } } +/** + * AppScaffold is a composable function that provides the main scaffold and navigation drawer structure for the app. + * It automatically determines whether to show a navigation drawer or a back button based on the current destination. + * + * @param title The title to display in the top app bar (optional). + * @param selectedDestination The current navigation destination, used to determine drawer visibility. + * @param navController The NavController for navigation actions. + * @param floatingActionButton Optional composable for a floating action button. + * @param onBackClick Optional callback for back button click. + * @param actions Optional composable for additional actions in the top app bar. + * @param content The main content composable, receives a Modifier. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppScaffold( diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/components/DateRangeHeader.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/components/DateRangeHeader.kt @@ -31,7 +31,19 @@ import java.time.format.DateTimeFormatter import java.util.Calendar import java.util.Date - +/** + * DateRangeHeader is a composable function that displays a header with the current date range + * (month, week, or day) and navigation controls for moving to the previous/next period or jumping to today. + * + * The displayed range and navigation logic depend on the selectedTab: + * 0 = month view, 1 = week view, 2 = day view. + * + * @param selectedTab The currently selected tab (0=month, 1=week, 2=day). + * @param calendarViewModel The CalendarViewModel providing start and end millis for the range. + * @param onPrev Callback for navigating to the previous period. + * @param onNext Callback for navigating to the next period. + * @param onToday Callback for jumping to today. + */ @Composable fun DateRangeHeader( selectedTab: Int, diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/components/SidebarDrawer.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/components/SidebarDrawer.kt @@ -1,5 +1,6 @@ package space.midnightthoughts.nordiccalendar.components +import Destinations import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -26,10 +27,19 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination import kotlinx.coroutines.launch -import space.midnightthoughts.nordiccalendar.Destinations import space.midnightthoughts.nordiccalendar.R import space.midnightthoughts.nordiccalendar.viewmodels.SidebarDrawerViewModel +/** + * SidebarDrawer is a composable function that displays the app's navigation drawer. + * It provides navigation items for main destinations (calendar, settings, about) and a list of selectable calendars. + * + * The user can navigate between destinations and select which calendars are active. + * + * @param navController The NavController for navigation actions. + * @param selectedDestination The currently selected navigation destination. + * @param drawerState The DrawerState controlling the drawer's open/close state. + */ @Composable fun SidebarDrawer( navController: NavController, diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt @@ -27,23 +27,73 @@ import space.midnightthoughts.nordiccalendar.util.Reminder import javax.inject.Inject import javax.inject.Singleton +/** + * CalendarRepository is a singleton class responsible for managing calendar data, events, and reminders. + * It provides flows for calendars and events, manages the selected time range, and handles synchronization + * with the Android calendar provider. It also manages scheduling and canceling reminders for events. + * + * @constructor Injects the application context for accessing system services and content providers. + */ @Singleton class CalendarRepository @Inject constructor(@ApplicationContext context: Context) { + /** + * Holds the calendar data utility instance. + */ private val calendarData = CalendarData() + /** + * StateFlow holding the list of all calendars. + */ private val _calendarsFlow = MutableStateFlow<List<Calendar>>(emptyList()) + + /** + * Public read-only StateFlow for observing calendar list changes. + */ val calendarsFlow: StateFlow<List<Calendar>> = _calendarsFlow.asStateFlow() + + /** + * Coroutine scope for repository background operations. + */ private val repoScope = CoroutineScope(Dispatchers.IO) + /** + * StateFlow holding the start time in milliseconds for the current event range. + */ private val _startMillis = MutableStateFlow(System.currentTimeMillis()) + + /** + * Public read-only StateFlow for observing the start time. + */ val startMillis: StateFlow<Long> = _startMillis.asStateFlow() + + /** + * StateFlow holding the end time in milliseconds for the current event range. + */ private val _endMillis = MutableStateFlow(System.currentTimeMillis()) + + /** + * Public read-only StateFlow for observing the end time. + */ val endMillis: StateFlow<Long> = _endMillis.asStateFlow() + + /** + * StateFlow holding the list of events for the selected calendars and time range. + */ private val _eventsFlow = MutableStateFlow<List<Event>>(emptyList()) + + /** + * Public read-only StateFlow for observing event list changes. + */ val eventsFlow: StateFlow<List<Event>> = _eventsFlow.asStateFlow() + /** + * ContentObserver for monitoring changes in the calendar provider. + */ private var calendarContentObserver: ContentObserver? = null + /** + * Initializes the repository by loading calendars and setting up event synchronization. + */ init { loadCalendarsToFlow(context) repoScope.launch { @@ -56,6 +106,10 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex } } + /** + * Loads all calendars from the system and updates the calendars flow. + * @param context The application context. + */ private fun loadCalendarsToFlow(context: Context) { repoScope.launch { val calendars = getCalendars(context) @@ -63,6 +117,11 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex } } + /** + * Retrieves all calendars from the system and marks them as selected based on preferences. + * @param context The application context. + * @return List of Calendar objects. + */ fun getCalendars(context: Context): List<Calendar> { val contentResolver = context.contentResolver val prefs = context.getSharedPreferences("calendar_prefs", Context.MODE_PRIVATE) @@ -75,6 +134,12 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex } } + /** + * Sets the selected state of a calendar and updates the preferences. + * @param context The application context. + * @param calendarId The ID of the calendar to update. + * @param selected The new selected state. + */ fun setCalendarSelected(context: Context, calendarId: Long, selected: Boolean) { val prefs = context.getSharedPreferences("calendar_prefs", Context.MODE_PRIVATE) val selectedIds = @@ -88,11 +153,24 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex loadCalendarsToFlow(context) } + /** + * Sets the time range for event retrieval. + * @param startMillis The start time in milliseconds. + * @param endMillis The end time in milliseconds. + */ fun setTimeRange(startMillis: Long, endMillis: Long) { _startMillis.value = startMillis _endMillis.value = endMillis } + /** + * Retrieves events for the specified calendars and time range. + * @param context The application context. + * @param calendarIds The list of calendar IDs. + * @param startMillis The start time in milliseconds. + * @param endMillis The end time in milliseconds. + * @return List of Event objects. + */ fun getEventsForCalendars( context: Context, calendarIds: List<Long>, @@ -106,10 +184,20 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex endMillis ) + /** + * Retrieves an event by its ID. + * @param context The application context. + * @param eventId The ID of the event to retrieve. + * @return The Event object, or null if not found. + */ fun getEventById(context: Context, eventId: Long): Event? { return calendarData.getEventById(context.contentResolver, eventId) } + /** + * Refreshes the events flow by reloading events for the selected calendars. + * @param context The application context. + */ fun refreshEvents(context: Context) { repoScope.launch { val calendars = getCalendars(context) @@ -119,7 +207,12 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex } } - // Liefert für eine Liste von Events alle Reminder (Vorlaufzeiten) als Map<EventId, List<Reminder>> + /** + * Returns a map of event IDs to their corresponding reminders (lead times). + * @param context The application context. + * @param events The list of events to retrieve reminders for. + * @return Map<EventId, List<Reminder>> containing the reminders for each event. + */ fun getRemindersForEvents( context: Context, events: List<Event> @@ -135,7 +228,9 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex } /** - * Löscht alle Reminder-Alarme für die übergebenen Events. + * Cancels all reminder alarms for the given events. + * @param context The application context. + * @param events The list of events to cancel reminders for. */ fun cancelRemindersForEvents(context: Context, events: List<Event>) { Log.d( @@ -167,9 +262,11 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex } /** - * Setzt für alle Events mit Reminder einen Alarm, der eine Benachrichtigung auslöst. - * Sollte nach jedem Laden der Events aufgerufen werden. - * Vorher werden alle bestehenden Reminder-Alarme für diese Events entfernt. + * Schedules reminder alarms for events with reminders. + * Should be called after loading events. + * Existing reminder alarms for these events are removed first. + * @param context The application context. + * @param events The list of events to schedule reminders for. */ fun scheduleRemindersForEvents(context: Context, events: List<Event>) { Log.d( @@ -243,8 +340,12 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex } } + /** + * Registers a ContentObserver to listen for calendar provider changes. + * @param context The application context. + */ fun registerCalendarContentObserver(context: Context) { - if (calendarContentObserver != null) return // Nur einmal registrieren + if (calendarContentObserver != null) return // Only register once val handler = Handler(Looper.getMainLooper()) calendarContentObserver = object : ContentObserver(handler) { override fun onChange(selfChange: Boolean) { @@ -270,6 +371,10 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex ) } + /** + * Unregisters the ContentObserver for calendar provider changes. + * @param context The application context. + */ fun unregisterCalendarContentObserver(context: Context) { calendarContentObserver?.let { context.contentResolver.unregisterContentObserver(it) diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/notifications/NotificationHelper.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/notifications/NotificationHelper.kt @@ -15,11 +15,38 @@ import androidx.core.content.ContextCompat import androidx.core.net.toUri import space.midnightthoughts.nordiccalendar.R +/** + * NotificationHelper is a utility object for managing and displaying event notifications. + * It handles notification channel creation, permission checks, and building notifications + * for calendar events with reminders. + */ object NotificationHelper { + /** + * Notification channel ID for calendar event reminders. + */ private const val CHANNEL_ID = "calendar_event_reminders" - private const val CHANNEL_NAME = "Kalender Erinnerungen" - private const val CHANNEL_DESC = "Benachrichtigungen für Kalender-Events mit Erinnerung" + /** + * Notification channel name (displayed to the user). + */ + private const val CHANNEL_NAME = "Calendar Reminders" + + /** + * Notification channel description (displayed to the user). + */ + private const val CHANNEL_DESC = "Notifications for calendar events with reminders" + + /** + * Shows a notification for a calendar event reminder. + * + * @param context The application context. + * @param eventId The ID of the event. + * @param eventTitle The title of the event. + * @param eventDescription The description of the event (optional). + * @param eventTime The start time of the event in milliseconds. + * @param eventEndTime The end time of the event in milliseconds. + * @param eventLocation The location of the event. + */ fun showEventNotification( context: Context, eventId: Long, @@ -34,7 +61,7 @@ object NotificationHelper { "showEventNotification: eventId=$eventId, eventTitle=$eventTitle, eventTime=$eventTime, eventDescription=$eventDescription, eventEndTime=$eventEndTime, eventLocation=$eventLocation" ) createNotificationChannel(context) - // Prüfe Berechtigung für Benachrichtigungen (ab Android 13) + // Check notification permission (Android 13+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission( context, @@ -45,11 +72,11 @@ object NotificationHelper { "NotificationHelper", "POST_NOTIFICATIONS permission not granted, notification not shown" ) - // Keine Berechtigung, Notification nicht anzeigen + // No permission, do not show notification return } } - // Deep Link Intent für EventDetails + // Deep link intent for EventDetails val deepLinkUri = "nordiccalendar://eventdetails/$eventId".toUri() val intent = Intent(Intent.ACTION_VIEW, deepLinkUri).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK @@ -68,13 +95,13 @@ object NotificationHelper { val timeFormat = android.text.format.DateFormat.getTimeFormat(context) val dateFormat = android.text.format.DateFormat.getDateFormat(context) val timeText = if (sameDay) { - "${timeFormat.format(startCal.time)} — ${timeFormat.format(endCal.time)} ${eventDescription}" + "${timeFormat.format(startCal.time)} — ${timeFormat.format(endCal.time)} $eventDescription" } else { "${dateFormat.format(startCal.time)} ${timeFormat.format(startCal.time)} — ${ dateFormat.format( endCal.time ) - } ${timeFormat.format(endCal.time)} ${eventDescription}" + } ${timeFormat.format(endCal.time)} $eventDescription" } val builder = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.outline_calendar_clock_24) @@ -86,7 +113,7 @@ object NotificationHelper { .setAutoCancel(true) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setColor(ContextCompat.getColor(context, R.color.teal_700)) - // Action für Link oder Adresse + // Action for link or address val location = eventLocation if (android.util.Patterns.WEB_URL.matcher(location).matches()) { val linkIntent = Intent(Intent.ACTION_VIEW, location.toUri()) @@ -96,7 +123,7 @@ object NotificationHelper { linkIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) - builder.addAction(0, "Öffnen", linkPendingIntent) + builder.addAction(0, "Open", linkPendingIntent) } else if (location.isNotBlank()) { val mapsIntent = Intent( Intent.ACTION_VIEW, @@ -111,7 +138,7 @@ object NotificationHelper { mapsIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) - builder.addAction(0, "Route anzeigen", mapsPendingIntent) + builder.addAction(0, "Show route", mapsPendingIntent) } try { diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/notifications/NotificationReceiver.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/notifications/NotificationReceiver.kt @@ -5,10 +5,21 @@ import android.content.Context import android.content.Intent import android.util.Log +/** + * NotificationReceiver is a BroadcastReceiver that receives alarm broadcasts for calendar event reminders + * and triggers the display of a notification using NotificationHelper. + */ class NotificationReceiver : BroadcastReceiver() { + /** + * Called when the BroadcastReceiver receives an Intent broadcast. + * Extracts event details from the intent and shows a notification. + * + * @param context The application context. + * @param intent The received Intent containing event details. + */ override fun onReceive(context: Context, intent: Intent) { val eventId = intent.getLongExtra("eventId", -1) - val eventTitle = intent.getStringExtra("eventTitle") ?: "Kalenderereignis" + val eventTitle = intent.getStringExtra("eventTitle") ?: "Calendar event" val eventDescription = intent.getStringExtra("eventDescription") val eventTime = intent.getLongExtra("eventTime", 0L) val eventEndTime = intent.getLongExtra("eventEndTime", 0L) diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/onboarding/Onboarding.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/onboarding/Onboarding.kt @@ -26,6 +26,15 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState import space.midnightthoughts.nordiccalendar.R +/** + * Data class representing a single onboarding page. + * + * @property imageRes Optional image resource to display. + * @property titleRes String resource for the title. + * @property descriptionRes String resource for the description. + * @property permissionRequest List of permissions to request on this page. + * @property showPermissionRequest Whether to show the permission request UI. + */ data class OnBoardModel( val imageRes: Int? = null, val titleRes: Int, @@ -34,6 +43,9 @@ data class OnBoardModel( val showPermissionRequest: Boolean = false ) +/** + * List of onboarding data models, each representing a page in the onboarding flow. + */ val onBoardingData = listOf( // Explain the purpose of the app OnBoardModel( @@ -54,6 +66,13 @@ val onBoardingData = listOf( ), ) +/** + * Composable function that displays a single onboarding page. + * Handles permission requests if required by the page. + * + * @param page The OnBoardModel representing the current onboarding page. + * @param hasPermissions MutableState indicating if all required permissions are granted. + */ @OptIn(ExperimentalPermissionsApi::class) @Composable fun OnBoardItem(page: OnBoardModel, hasPermissions: MutableState<Boolean>) { diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt @@ -32,6 +32,14 @@ import space.midnightthoughts.nordiccalendar.R import space.midnightthoughts.nordiccalendar.components.DateRangeHeader import space.midnightthoughts.nordiccalendar.viewmodels.CalendarViewModel +/** + * CalendarScreen is the main composable for displaying the calendar view. + * It provides a tabbed interface for switching between month, week, and day views, + * supports pull-to-refresh, and displays the current date range and events. + * + * @param modifier Modifier for styling and layout. + * @param navController NavController for navigation actions. + */ @SuppressLint("UnusedBoxWithConstraintsScope") @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -39,17 +47,35 @@ fun CalendarScreen( modifier: Modifier = Modifier, navController: NavController, ) { + /** + * ViewModel for calendar data and state. + */ val calendarViewModel: CalendarViewModel = hiltViewModel() + + /** + * State holding the list of events for the current view. + */ val events = remember(calendarViewModel) { calendarViewModel.events }.collectAsState(initial = emptyList()) + + /** + * State indicating whether a refresh is in progress. + */ val isRefreshing = remember(calendarViewModel) { calendarViewModel.isRefreshing }.collectAsState(initial = false) + + /** + * State holding the currently selected tab (0=month, 1=week, 2=day). + */ val selectedTab = remember(calendarViewModel) { calendarViewModel.selectedTab }.collectAsState(initial = 0) + /** + * State for pull-to-refresh gesture. + */ val pullToRefreshState = rememberPullToRefreshState() PullToRefreshBox( state = pullToRefreshState, diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/DayView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/DayView.kt @@ -60,6 +60,13 @@ import java.time.format.DateTimeFormatter import java.util.Date import java.util.PriorityQueue +/** + * Assigns columns to events so that overlapping events are displayed side by side. + * Each event is assigned a column index and the total number of columns for its overlap group. + * + * @param events List of events to assign columns to. + * @return List of Triple<Event, columnIndex, maxColumns> for layout. + */ private fun assignColumns(events: List<Event>): List<Triple<Event, Int, Int>> { data class ActiveEvent(val endTime: Long, val col: Int) @@ -70,7 +77,7 @@ private fun assignColumns(events: List<Event>): List<Triple<Event, Int, Int>> { var maxColumns = 0 for (event in sorted) { - // Entferne abgelaufene Events und gib deren Spalten frei + // Remove expired events and free their columns while (active.isNotEmpty() && active.peek()?.endTime!! <= event.startTime) { freeColumns.add(active.poll()?.col) } @@ -82,6 +89,14 @@ private fun assignColumns(events: List<Event>): List<Triple<Event, Int, Int>> { return result } +/** + * Displays the hour grid for a day, with time labels and horizontal dividers for each hour. + * + * @param dayStart Start time of the day in milliseconds. + * @param hourHeightDp Height of each hour row in dp. + * @param timeColumnWidth Width of the time label column in dp. + * @param hourFormat DateTimeFormatter for the hour labels. + */ @Composable private fun HourLines( dayStart: Long, @@ -111,6 +126,15 @@ private fun HourLines( } } +/** + * Displays a red bar indicating the current time ("now") on the day view. + * The bar is only shown if the current time is within the visible range. + * + * @param nowOffsetY Vertical offset in pixels for the bar position. + * @param hourHeightPx Height of one hour in pixels. + * @param now Current time in milliseconds. + * @param hourFormat Formatter for displaying the time label. + */ @Composable private fun NowBar( nowOffsetY: Float, @@ -167,6 +191,14 @@ private fun NowBar( } } +/** + * Main composable for displaying the day view of the calendar. + * Shows a scrollable list of hours, events, and a bar for the current time. + * + * @param modifier Modifier for styling and layout. + * @param navController NavController for navigation actions. + * @param calendarViewModel ViewModel providing calendar data and state. + */ @SuppressLint("UnusedBoxWithConstraintsScope") @Composable fun DayView( @@ -213,7 +245,7 @@ fun DayView( ) { val maxWidthPx = with(density) { maxWidth.toPx() } - // Stundenraster + // Hour grid HourLines( dayStart = dayStart.value, hourHeightDp = hourHeightDp, @@ -221,7 +253,7 @@ fun DayView( hourFormat = hourFormat ) - // Events (wie gehabt, optimiert) + // Events (optimized as before) val eventColumns = remember(events.value, maxWidthPx) { assignColumns(events.value) } events.value.forEach { event -> val triple = @@ -241,10 +273,14 @@ fun DayView( val minCardHeightDp = 60.dp val isCompact = with(density) { eventHeightPx.toDp() } < minCardHeightDp + // Whether the event starts before the visible day (no top corners) val noTopCorners = event.startTime < dayStart.value + // Whether the event ends after the visible day (no bottom corners) val noBottomCorners = event.endTime > dayEnd.value + // Show start time as midnight if event starts at the beginning of the day val showStartTimeAsMidnight = shownStart == dayStart.value + // Show end time as midnight if event ends at the end of the day val showEndTimeAsMidnight = shownEnd == dayEnd.value key(event.eventId, event.calendar.id) { @@ -277,7 +313,7 @@ fun DayView( } } - // Jetzt-Linie (optimiert als eigene Composable) + // Now bar (optimized as its own composable) NowBar( nowOffsetY = nowOffsetY, hourHeightPx = hourHeightPx, @@ -288,7 +324,22 @@ fun DayView( } } - +/** + * Card composable for displaying a single event in the day view. + * Handles compact and expanded layouts, corner rounding, and click actions. + * + * @param event The event to display. + * @param isCompact Whether to use a compact layout (for short events). + * @param modifier Modifier for styling and layout. + * @param onClick Optional callback for click actions. + * @param index Index of the event (for accessibility semantics). + * @param noBottomCorners If true, bottom corners are not rounded. + * @param noTopCorners If true, top corners are not rounded. + * @param showStartTimeAsMidnight If true, show start time as "00:00". + * @param showEndTimeAsMidnight If true, show end time as "24:00". + * @param eventStartOverride Optional override for the event start time. + * @param eventEndOverride Optional override for the event end time. + */ @Composable private fun EventCard( event: Event, diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics @@ -52,6 +53,8 @@ import dev.sargunv.maplibrecompose.compose.source.rememberGeoJsonSource import dev.sargunv.maplibrecompose.core.BaseStyle import dev.sargunv.maplibrecompose.core.CameraPosition import dev.sargunv.maplibrecompose.core.source.GeoJsonData +import dev.sargunv.maplibrecompose.expressions.dsl.const +import dev.sargunv.maplibrecompose.expressions.dsl.image import io.github.dellisd.spatialk.geojson.Feature import io.github.dellisd.spatialk.geojson.Point import io.github.dellisd.spatialk.geojson.Position @@ -68,6 +71,13 @@ import java.util.Calendar import java.util.Date import kotlin.time.toKotlinDuration +/** + * EventDetailsView displays the details of a calendar event, including title, time, duration, + * calendar, organizer, location (with map if available), and description. Handles navigation and back actions. + * + * @param backStackEntry The NavBackStackEntry for navigation arguments. + * @param navController The NavHostController for navigation actions. + */ @Composable fun EventDetailsView( backStackEntry: NavBackStackEntry, @@ -134,7 +144,7 @@ fun EventDetailsView( val tabArg = backStackEntry.arguments?.getInt("tab") - // Back-Handling: Wenn Tab-Argument vorhanden, gezielt zurück navigieren + // Back handling: If tab argument is present, navigate back to the specific tab val handleBack: () -> Unit = { if (tabArg != null) { navController.navigate("calendar?tab=$tabArg") { @@ -154,7 +164,7 @@ fun EventDetailsView( ), selectedDestination = "eventDetails", navController = navController, - onBackClick = handleBack // <-- Back-Logik an AppScaffold übergeben + onBackClick = handleBack // <-- Pass back logic to AppScaffold ) { innerPadding -> SelectionContainer(modifier = innerPadding) { Column( @@ -382,6 +392,13 @@ fun EventDetailsView( } } +/** + * LocationMap displays a map centered on the given coordinate and bounding box, with a marker. + * Clicking the map opens the location in a routing app. + * + * @param coordinate The Position (latitude/longitude) to center the map and place the marker. + * @param boundingBox Optional BoundingBox to adjust the map zoom and center. + */ @Composable fun LocationMap(coordinate: Position, boundingBox: BoundingBox?) { val shape = RoundedCornerShape(16.dp) @@ -437,6 +454,11 @@ fun LocationMap(coordinate: Position, boundingBox: BoundingBox?) { } } +/** + * MapContent adds a marker to the map at the given coordinate. + * + * @param coordinate The Position (latitude/longitude) for the marker. + */ @Composable fun MapContent( coordinate: Position, @@ -446,12 +468,13 @@ fun MapContent( Feature(Point(coordinates = coordinate)) ) ) + val markerIcon = painterResource(R.drawable.outline_home_pin_24) SymbolLayer( id = "marker", source = marker, - //iconImage = "marker_icon", // Ensure you have a marker icon in your resources - //iconSize = 1.0f, + iconImage = image(markerIcon), + iconSize = const(1f) ) } diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/MonthView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/MonthView.kt @@ -41,6 +41,15 @@ import java.time.ZoneId import java.time.format.TextStyle import java.util.Locale +/** + * MonthView displays a monthly calendar grid with days and events. + * It highlights the current day, shows a grid of days for the month, and displays up to three events per day as compact chips. + * If there are more than three events, a "+N more" chip is shown. + * + * @param modifier Modifier for styling and layout. + * @param navController NavController for navigation actions. + * @param calendarViewModel ViewModel providing calendar data and state. + */ @SuppressLint("UnusedBoxWithConstraintsScope") @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -55,7 +64,7 @@ fun MonthView( val zoneId = ZoneId.systemDefault() val firstDay = Instant.ofEpochMilli(startMillis).atZone(zoneId).toLocalDate() val daysInMonth = firstDay.lengthOfMonth() - // Wochenstart auf Montag (1=Montag, 7=Sonntag) + // Week starts on Monday (1=Monday, 7=Sunday) val firstDayOfWeek = 1 val firstOfMonth = firstDay.withDayOfMonth(1) val firstOfMonthDayOfWeek = (firstOfMonth.dayOfWeek.value - firstDayOfWeek + 7) % 7 @@ -68,7 +77,7 @@ fun MonthView( Instant.ofEpochMilli(event.startTime).atZone(zoneId).toLocalDate() } Column(modifier) { - // Wochentagsnamen als Grid-Zeile + // Weekday names as grid row Row(Modifier.fillMaxWidth()) { val locale = Locale.getDefault() for (i in 0..6) { @@ -85,7 +94,7 @@ fun MonthView( } } } - // Monatsraster mit Linien (nur innere Linien) + // Month grid with lines (only inner lines) LazyVerticalGrid( columns = GridCells.Fixed(7), modifier = Modifier.fillMaxWidth(), @@ -106,7 +115,7 @@ fun MonthView( .fillMaxSize(), verticalArrangement = Arrangement.Top ) { - // Tag-Nummer + // Day number Box( modifier = Modifier .size(32.dp) @@ -133,7 +142,7 @@ fun MonthView( ) } Spacer(Modifier.size(2.dp)) - // Events als kompakte Chips + // Events as compact chips val maxEvents = 3 dayEvents.take(maxEvents).forEach { event -> CompactChip( @@ -152,7 +161,7 @@ fun MonthView( } if (dayEvents.size > maxEvents) { CompactChip( - text = "+${dayEvents.size - maxEvents} mehr", + text = "+${dayEvents.size - maxEvents} more", backgroundColor = Color.Transparent, textColor = MaterialTheme.colorScheme.primary, center = true, @@ -172,6 +181,16 @@ fun MonthView( } } +/** + * CompactChip displays a small, rounded chip for an event or a "+N more" indicator in the month view. + * + * @param text The text to display inside the chip. + * @param backgroundColor The background color of the chip. + * @param textColor The text color. + * @param modifier Modifier for styling and layout. + * @param borderColor Optional border color for the chip. + * @param center If true, centers the text inside the chip. + */ @Composable fun CompactChip( text: String, diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/SettingsView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/SettingsView.kt @@ -17,6 +17,12 @@ import space.midnightthoughts.nordiccalendar.R import space.midnightthoughts.nordiccalendar.components.AppScaffold import space.midnightthoughts.nordiccalendar.viewmodels.SettingsViewModel +/** + * SettingsView displays the settings screen for the app, allowing the user to configure preferences. + * Uses a preference library to show editable fields, such as the Nominatim URL for geocoding. + * + * @param navController The NavHostController for navigation actions. + */ @Composable fun SettingsView(navController: NavHostController) { val viewModel: SettingsViewModel = hiltViewModel() @@ -34,6 +40,7 @@ fun SettingsView(navController: NavHostController) { horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(8.dp) ) { + // Text field preference for the Nominatim geocoding service URL textFieldPreference( key = "nominatim_url", defaultValue = "https://nominatim.openstreetmap.org/search", diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/WeekView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/WeekView.kt @@ -14,6 +14,14 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import space.midnightthoughts.nordiccalendar.viewmodels.CalendarViewModel +/** + * WeekView displays a list of events for the current week. + * Each event is shown as a clickable text item that navigates to the event details view. + * + * @param modifier Modifier for styling and layout. + * @param navController NavController for navigation actions. + * @param calendarViewModel ViewModel providing calendar data and state. + */ @Composable fun WeekView( modifier: Modifier = Modifier, diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/util/CalendarData.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/util/CalendarData.kt @@ -6,6 +6,18 @@ import android.net.Uri import android.provider.CalendarContract import android.util.Log +/** + * Data class representing a calendar. + * + * @property id The unique ID of the calendar. + * @property name The display name of the calendar. + * @property color The color of the calendar. + * @property accountName The account name associated with the calendar. + * @property accountType The account type associated with the calendar. + * @property syncEvents Whether the calendar is set to sync events. + * @property visible Whether the calendar is visible. + * @property selected Whether the calendar is selected (default: true). + */ data class Calendar( val id: Long, val name: String, @@ -18,12 +30,38 @@ data class Calendar( val selected: Boolean = true ) +/** + * Data class representing an event in the calendar. + * + * @property title The title of the event. + * @property description A description of the event. + * @property location The location of the event. + * @property eventColor The color of the event. + * @property status The status of the event (e.g., confirmed, tentative, canceled). + * @property selfAttendeeStatus The attendee status of the self (e.g., accepted, declined). + * @property duration The duration of the event. + * @property eventTimezone The timezone of the event. + * @property eventEndTimezone The timezone of the event's end time. + * @property allDay Whether the event lasts all day. + * @property accessLevel The access level for the event. + * @property availability The availability status of the event. + * @property hasAlarm Whether the event has an alarm. + * @property eventId The unique ID of the event. + * @property startTime The start time of the event in milliseconds. + * @property endTime The end time of the event in milliseconds. + * @property calendarId The ID of the calendar that the event belongs to. + * @property organizer The organizer of the event. + * @property attendees A list of attendees for the event. + * @property calendar A reference to the calendar this event belongs to. + */ data class Event( val title: String, val description: String?, val location: String?, val eventColor: Long, - /// One of STATUS_TENTATIVE, STATUS_CONFIRMED, STATUS_CANCELED + /** + * One of STATUS_TENTATIVE, STATUS_CONFIRMED, STATUS_CANCELED + */ val status: Int, val selfAttendeeStatus: Int, val duration: String?, @@ -39,16 +77,29 @@ data class Event( val calendarId: Long, val organizer: String?, val attendees: List<String> = emptyList(), - // Reference to the calendar this event belongs to + /** + * Reference to the calendar this event belongs to + */ val calendar: Calendar ) -// Reminder-Datenklasse +/** + * Data class representing a reminder for an event. + * + * @property minutes The number of minutes before the event when the reminder should trigger. + * @property method The method used for the reminder (e.g., alert, email). + */ data class Reminder(val minutes: Int, val method: Int) -// Helper for the android calendar provider/CalendarContract +/** + * Helper class for interacting with the Android calendar provider/CalendarContract. + * Provides methods for querying calendars, events, and reminders. + */ class CalendarData { + /** + * Projection array for querying event instances from the calendar provider. + */ val INSTANCE_PROJECTION = arrayOf( CalendarContract.Instances.CALENDAR_ID, CalendarContract.Instances.TITLE, @@ -88,7 +139,9 @@ class CalendarData { val PROJECTION_END_INDEX = 16 val PROJECTION_ORGANIZER_INDEX = 17 - // Get available calendars from the Android Calendar Provider + /** + * Get available calendars from the Android Calendar Provider + */ fun getCalendars(contentResolver: ContentResolver): List<Calendar> { val calendars = mutableListOf<Calendar>() @@ -148,6 +201,15 @@ class CalendarData { return calendars } + /** + * Get events for a specific calendar within a given time range. + * + * @param contentResolver The content resolver to access the calendar provider. + * @param calendarId The ID of the calendar to fetch events from. + * @param startMillis The start of the time range in milliseconds (default: start of the week). + * @param endMillis The end of the time range in milliseconds (default: end of the week). + * @return A list of events occurring in the specified calendar and time range. + */ fun getEventsForCalendar( contentResolver: ContentResolver, calendarId: Long, @@ -240,6 +302,15 @@ class CalendarData { return events } + /** + * Get events for multiple calendars within a given time range. + * + * @param contentResolver The content resolver to access the calendar provider. + * @param calendarIds A list of calendar IDs to fetch events from. + * @param startMillis The start of the time range in milliseconds (default: start of the week). + * @param endMillis The end of the time range in milliseconds (default: end of the week). + * @return A list of events occurring in the specified calendars and time range. + */ fun getEventsForCalendars( contentResolver: ContentResolver, calendarIds: List<Long>, @@ -297,6 +368,13 @@ class CalendarData { } } + /** + * Get a specific event by its ID. + * + * @param contentResolver The content resolver to access the calendar provider. + * @param eventId The ID of the event to fetch. + * @return The event with the specified ID, or null if not found. + */ fun getEventById(contentResolver: ContentResolver, eventId: Long): Event? { val selection = "Instances.event_id = ?" val selectionArgs = arrayOf(eventId.toString()) @@ -353,7 +431,13 @@ class CalendarData { return event } - // Liefert alle Reminder (Vorlaufzeiten in Minuten) für ein Event + /** + * Get all reminders (in minutes) for an event. + * + * @param contentResolver The content resolver to access the calendar provider. + * @param eventId The ID of the event to fetch reminders for. + * @return A list of reminders for the specified event. + */ fun getRemindersForEvent(contentResolver: ContentResolver, eventId: Long): List<Reminder> { val reminders = mutableListOf<Reminder>() val projection = arrayOf( diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/util/OnboardingPrefs.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/util/OnboardingPrefs.kt @@ -4,11 +4,22 @@ import android.content.Context import android.util.Log import androidx.core.content.edit +/** + * OnboardingPrefs is a utility object for managing onboarding state in shared preferences. + * It tracks whether onboarding has been completed and for which app version. + */ object OnboardingPrefs { private const val PREFS_NAME = "onboarding_prefs" private const val KEY_ONBOARDING_DONE = "onboarding_done" private const val KEY_ONBOARDING_VERSION = "onboarding_version" + /** + * Checks if onboarding is needed for the current app version. + * + * @param context The application context. + * @param currentVersion The current app version string. + * @return True if onboarding should be shown, false otherwise. + */ fun isOnboardingNeeded(context: Context, currentVersion: String): Boolean { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val done = prefs.getBoolean(KEY_ONBOARDING_DONE, false) @@ -20,6 +31,12 @@ object OnboardingPrefs { return !done || savedVersion != currentVersion } + /** + * Marks onboarding as completed for the given app version. + * + * @param context The application context. + * @param currentVersion The current app version string. + */ fun setOnboardingDone(context: Context, currentVersion: String) { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) prefs.edit { @@ -28,9 +45,13 @@ object OnboardingPrefs { } } + /** + * Resets onboarding state (for testing or re-showing onboarding). + * + * @param context The application context. + */ fun resetOnboarding(context: Context) { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) prefs.edit { clear() } } } - diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt @@ -17,26 +17,66 @@ import space.midnightthoughts.nordiccalendar.data.CalendarRepository import java.time.LocalDate import javax.inject.Inject +/** + * CalendarViewModel is the main ViewModel for managing calendar state, event data, and navigation logic. + * It handles tab selection, date range management, event refreshing, and reminder scheduling. + * + * @property context The application context injected by Hilt. + * @property repository The CalendarRepository for accessing calendar and event data. + * @property events StateFlow of the current list of events. + * @property startMillis StateFlow of the current start time in milliseconds. + * @property endMillis StateFlow of the current end time in milliseconds. + * @property isRefreshing StateFlow indicating if a refresh is in progress. + * @property selectedTab StateFlow of the currently selected tab (0=month, 1=week, 2=day). + */ @HiltViewModel class CalendarViewModel @Inject constructor( @param:ApplicationContext private val context: Context, private val repository: CalendarRepository, savedStateHandle: SavedStateHandle ) : ViewModel() { + /** + * StateFlow of the current list of events. + */ val events = repository.eventsFlow.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList() ) + + /** + * StateFlow of the current start time in milliseconds. + */ val startMillis = repository.startMillis + + /** + * StateFlow of the current end time in milliseconds. + */ val endMillis = repository.endMillis + /** + * StateFlow indicating if a refresh is in progress. + */ val isRefreshing = MutableStateFlow(false) + + /** + * Backing property for the currently selected tab (0=month, 1=week, 2=day). + */ private val _selectedTab = MutableStateFlow(0) + + /** + * StateFlow of the currently selected tab (0=month, 1=week, 2=day). + */ val selectedTab: StateFlow<Int> = _selectedTab.asStateFlow() + /** + * Backing property for the date argument (if provided via navigation). + */ private val _dateArg = MutableStateFlow<String?>(null) + /** + * Initializes the ViewModel, sets up tab and date from navigation arguments, and starts reminder scheduling. + */ init { val tab = savedStateHandle.get<Int?>("tab") if (tab != null) { @@ -47,7 +87,7 @@ class CalendarViewModel @Inject constructor( setDayFromString(dateArg) _dateArg.value = dateArg } - // Reminder-Benachrichtigungen immer aktuell halten (auch beim Init) + // Always keep reminder notifications up to date (also on init) viewModelScope.launch { repository.eventsFlow.collect { events -> Log.d("CalendarViewModel", "Scheduling reminders for ${events.size} events") @@ -56,6 +96,12 @@ class CalendarViewModel @Inject constructor( } } + /** + * Sets the currently selected tab (0=month, 1=week, 2=day) and updates the time range accordingly. + * If a date argument is present, it is used instead of the default range. + * + * @param tab The tab index to select. + */ fun setTab(tab: Int) { _selectedTab.value = tab if (_dateArg.value == null) { @@ -65,10 +111,16 @@ class CalendarViewModel @Inject constructor( } } + /** + * Returns the default start time in milliseconds for the given tab (month, week, or day). + * + * @param tab The tab index (0=month, 1=week, 2=day). + * @return The start time in milliseconds. + */ private fun getDefaultStartMillis(tab: Int): Long { val cal = java.util.Calendar.getInstance() when (tab) { - 0 -> { // Monat + 0 -> { // Month cal.set(java.util.Calendar.DAY_OF_MONTH, 1) cal.set(java.util.Calendar.HOUR_OF_DAY, 0) cal.set(java.util.Calendar.MINUTE, 0) @@ -76,7 +128,7 @@ class CalendarViewModel @Inject constructor( return cal.timeInMillis } - 1 -> { // Woche + 1 -> { // Week cal.set(java.util.Calendar.DAY_OF_WEEK, cal.firstDayOfWeek) cal.set(java.util.Calendar.HOUR_OF_DAY, 0) cal.set(java.util.Calendar.MINUTE, 0) @@ -84,7 +136,7 @@ class CalendarViewModel @Inject constructor( return cal.timeInMillis } - 2 -> { // Tag + 2 -> { // Day cal.set(java.util.Calendar.HOUR_OF_DAY, 0) cal.set(java.util.Calendar.MINUTE, 0) cal.set(java.util.Calendar.SECOND, 0) @@ -95,10 +147,16 @@ class CalendarViewModel @Inject constructor( } } + /** + * Returns the default end time in milliseconds for the given tab (month, week, or day). + * + * @param tab The tab index (0=month, 1=week, 2=day). + * @return The end time in milliseconds. + */ private fun getDefaultEndMillis(tab: Int): Long { val cal = java.util.Calendar.getInstance() when (tab) { - 0 -> { // Monat + 0 -> { // Month cal.set( java.util.Calendar.DAY_OF_MONTH, cal.getActualMaximum(java.util.Calendar.DAY_OF_MONTH) @@ -109,7 +167,7 @@ class CalendarViewModel @Inject constructor( return cal.timeInMillis } - 1 -> { // Woche + 1 -> { // Week cal.set(java.util.Calendar.DAY_OF_WEEK, cal.firstDayOfWeek + 6) cal.set(java.util.Calendar.HOUR_OF_DAY, 23) cal.set(java.util.Calendar.MINUTE, 59) @@ -117,7 +175,7 @@ class CalendarViewModel @Inject constructor( return cal.timeInMillis } - 2 -> { // Tag + 2 -> { // Day cal.set(java.util.Calendar.HOUR_OF_DAY, 23) cal.set(java.util.Calendar.MINUTE, 59) cal.set(java.util.Calendar.SECOND, 59) @@ -128,7 +186,10 @@ class CalendarViewModel @Inject constructor( } } - // Die folgenden Methoden ändern nur noch den Zeitraum im Repository + // The following methods only change the time period in the repository + /** + * Advances the day view to the next day and updates the time range. + */ fun nextDay() { if (_selectedTab.value != 2) return val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis.value } @@ -141,6 +202,9 @@ class CalendarViewModel @Inject constructor( repository.setTimeRange(start, end) } + /** + * Moves the day view to the previous day and updates the time range. + */ fun prevDay() { if (_selectedTab.value != 2) return val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis.value } @@ -153,6 +217,9 @@ class CalendarViewModel @Inject constructor( repository.setTimeRange(start, end) } + /** + * Advances the week view to the next week and updates the time range. + */ fun nextWeek() { if (_selectedTab.value != 1) return val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis.value } @@ -166,6 +233,9 @@ class CalendarViewModel @Inject constructor( repository.setTimeRange(start, end) } + /** + * Moves the week view to the previous week and updates the time range. + */ fun prevWeek() { if (_selectedTab.value != 1) return val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis.value } @@ -179,6 +249,9 @@ class CalendarViewModel @Inject constructor( repository.setTimeRange(start, end) } + /** + * Advances the month view to the next month and updates the time range. + */ fun nextMonth() { if (_selectedTab.value != 0) return val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis.value } @@ -196,6 +269,9 @@ class CalendarViewModel @Inject constructor( repository.setTimeRange(start, end) } + /** + * Moves the month view to the previous month and updates the time range. + */ fun prevMonth() { if (_selectedTab.value != 0) return val cal = java.util.Calendar.getInstance().apply { timeInMillis = startMillis.value } @@ -213,6 +289,9 @@ class CalendarViewModel @Inject constructor( repository.setTimeRange(start, end) } + /** + * Sets the week view to the current week and updates the time range. + */ fun setTodayWeek() { if (_selectedTab.value != 1) return val cal = java.util.Calendar.getInstance() @@ -231,6 +310,9 @@ class CalendarViewModel @Inject constructor( repository.setTimeRange(start, end) } + /** + * Sets the month view to the current month and updates the time range. + */ fun setTodayMonth() { if (_selectedTab.value != 0) return val cal = java.util.Calendar.getInstance() @@ -252,6 +334,9 @@ class CalendarViewModel @Inject constructor( repository.setTimeRange(start, end) } + /** + * Sets the day view to the current day and updates the time range. + */ fun setTodayDay() { if (_selectedTab.value != 2) return val cal = java.util.Calendar.getInstance() @@ -268,15 +353,24 @@ class CalendarViewModel @Inject constructor( repository.setTimeRange(start, end) } + /** + * Refreshes the events from the repository and updates reminders. + * Sets the isRefreshing state to true during the operation. + */ fun refreshEvents() { isRefreshing.value = true repository.refreshEvents(context) - // Nach dem Aktualisieren der Events Reminder setzen + // After updating the events, set reminders val events = repository.eventsFlow.value repository.scheduleRemindersForEvents(context, events) isRefreshing.value = false } + /** + * Sets the current day for the day view tab (tab 2) and updates the time range accordingly. + * + * @param date The LocalDate to set as the current day. + */ fun setDay(date: LocalDate) { if (_selectedTab.value != 2) { setTab(2) @@ -296,13 +390,19 @@ class CalendarViewModel @Inject constructor( repository.setTimeRange(start, end) } + /** + * Parses a date string and sets the current day for the day view tab. + * If the string is invalid, the error is ignored. + * + * @param dateString The date string to parse (in ISO format). + */ fun setDayFromString(dateString: String?) { if (dateString == null) return try { val date = LocalDate.parse(dateString) setDay(date) } catch (_: Exception) { - // Ignoriere ungültiges Datum + // Ignore invalid date } } } diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt @@ -39,21 +39,44 @@ data class NominatimResult( data class BoundingBox(val south: Double, val north: Double, val west: Double, val east: Double) +/** + * EventDetailsViewModel is the ViewModel responsible for providing event details, location resolution, + * and bounding box data for the event details screen. It fetches event data, resolves locations using Nominatim, + * and exposes state flows for UI observation. + * + * @property context The application context injected by Hilt. + * @property repository The CalendarRepository for accessing event and calendar data. + * @property event StateFlow holding the current event details. + * @property locationPosition StateFlow holding the resolved geographic position for the event location. + * @property locationBoundingBox StateFlow holding the bounding box for the resolved location. + */ @HiltViewModel class EventDetailsViewModel @Inject constructor( @param:ApplicationContext private val context: Context, private val repository: CalendarRepository, savedStateHandle: SavedStateHandle ) : ViewModel() { + /** + * StateFlow holding the current event details. + */ private val _event = MutableStateFlow<Event?>(null) val event: StateFlow<Event?> = _event.asStateFlow() + /** + * StateFlow holding the resolved geographic position for the event location. + */ private val _locationPosition = MutableStateFlow<Position?>(null) val locationPosition: StateFlow<Position?> = _locationPosition.asStateFlow() + /** + * StateFlow holding the bounding box for the resolved location. + */ private val _locationBoundingBox = MutableStateFlow<BoundingBox?>(null) val locationBoundingBox: StateFlow<BoundingBox?> = _locationBoundingBox.asStateFlow() + /** + * Ktor HTTP client for making network requests to Nominatim. + */ private val ktorClient = HttpClient(Android) { install(ContentNegotiation) { json( @@ -73,13 +96,16 @@ class EventDetailsViewModel @Inject constructor( install(Logging) { logger = object : Logger { override fun log(message: String) { - android.util.Log.d("Ktor", message) + Log.d("Ktor", message) } } level = LogLevel.INFO } } + /** + * Initializes the ViewModel by loading the event details if an eventId is provided in the navigation arguments. + */ init { val eventId = savedStateHandle.get<Long>("eventId") if (eventId != null) { @@ -87,6 +113,11 @@ class EventDetailsViewModel @Inject constructor( } } + /** + * Loads the event details for the given eventId from the repository and updates the event state. + * + * @param eventId The ID of the event to load. + */ private fun loadEvent(eventId: Long) { viewModelScope.launch { val event = repository.getEventById(context, eventId) @@ -94,6 +125,13 @@ class EventDetailsViewModel @Inject constructor( } } + /** + * Resolves the geographic location for a given address using the Nominatim geocoding service. + * Updates the locationPosition and locationBoundingBox state flows with the result. + * + * @param address The address string to resolve. + * @param locale The locale/language to use for the geocoding request (default: "de"). + */ fun resolveLocation(address: String, locale: String = "de") { viewModelScope.launch { try { diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SettingsViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SettingsViewModel.kt @@ -6,6 +6,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject +/** + * SettingsViewModel is the ViewModel responsible for managing settings-related data and operations. + * It currently does not hold any specific state or logic but serves as a placeholder for future settings management. + * + * @property context The application context injected by Hilt. + */ @HiltViewModel class SettingsViewModel @Inject constructor( @param:ApplicationContext private val context: Context diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt @@ -12,17 +12,33 @@ import space.midnightthoughts.nordiccalendar.data.CalendarRepository import space.midnightthoughts.nordiccalendar.util.Calendar import javax.inject.Inject +/** + * SidebarDrawerViewModel is the ViewModel responsible for managing the state of the sidebar drawer, + * including the list of calendars and their selection state. + * + * @property context The application context injected by Hilt. + * @property repository The CalendarRepository for accessing calendar data. + */ @HiltViewModel class SidebarDrawerViewModel @Inject constructor( @param:ApplicationContext private val context: Context, private val repository: CalendarRepository ) : ViewModel() { + /** + * Flow that emits the list of calendars, sharing the state with a timeout of 5000 milliseconds. + * This allows the UI to observe changes in the calendar list and update accordingly. + */ val calendars = repository.calendarsFlow.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList() ) + /** + * Toggles the selection state of a calendar. + * + * @param calendar The calendar to toggle. + */ fun toggleCalendar(calendar: Calendar) { viewModelScope.launch { repository.setCalendarSelected( diff --git a/app/src/main/res/drawable/outline_home_pin_24.xml b/app/src/main/res/drawable/outline_home_pin_24.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M360,520L440,520L440,410L520,410L520,520L600,520L600,330L480,250L360,330L360,520ZM480,774Q602,662 661,570.5Q720,479 720,408Q720,299 650.5,229.5Q581,160 480,160Q379,160 309.5,229.5Q240,299 240,408Q240,479 299,570.5Q358,662 480,774ZM480,880Q319,743 239.5,625.5Q160,508 160,408Q160,258 256.5,169Q353,80 480,80Q607,80 703.5,169Q800,258 800,408Q800,508 720.5,625.5Q641,743 480,880ZM480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Z"/> + +</vector>