Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/PlaceCardScreen.kt +172 −140 Original line number Diff line number Diff line Loading @@ -64,6 +64,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import ch.poole.openinghoursparser.OpeningHoursParseException import ch.poole.openinghoursparser.OpeningHoursParser import ch.poole.openinghoursparser.Rule import ch.poole.openinghoursparser.WeekDayRange import earth.maps.cardinal.R.dimen import earth.maps.cardinal.R.drawable Loading Loading @@ -189,32 +190,22 @@ fun getOpeningHoursForNext7Days( return dayOpeningHours } fun ordinalInRange(ord: Int, start: Int, end: Int): Boolean { return ord >= start && ord <= end } fun weekdayRangeIncludesDay(range: WeekDayRange, day: Int): Boolean { if (range.startDay != null && range.startDay.ordinal == day) { return true } else if (range.startDay == null || range.endDay == null) { return false } if (range.endDay < range.startDay) { for (ord in 0..range.endDay.ordinal) { if (ord == day) { return true } } for (ord in range.startDay.ordinal..6) { if (ord == day) { return true } } return if (range.endDay < range.startDay) { ordinalInRange(day, 0, range.endDay.ordinal) || ordinalInRange(day, range.startDay.ordinal, 6) } else { for (ord in range.startDay.ordinal..range.endDay.ordinal) { if (ord == day) { return true ordinalInRange(day, range.startDay.ordinal, range.endDay.ordinal) } } } return false } @OptIn(ExperimentalTime::class) @Composable Loading Loading @@ -245,54 +236,8 @@ fun ExpandableOpeningHours( .padding(16.dp) ) { // Header with current status and expand/collapse icon Row( modifier = Modifier .fillMaxWidth() .clickable { expanded = !expanded }, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column( modifier = Modifier.weight(1f) ) { Text( text = stringResource(string.opening_hours_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium ) currentStatus?.let { status -> Row( modifier = Modifier.padding(top = 4.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = status.statusText, color = status.statusColor, style = MaterialTheme.typography.bodyMedium ) status.nextTimeText?.let { nextTime -> Spacer(modifier = Modifier.width(4.dp)) Text( text = nextTime, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } } Icon( painter = painterResource( if (expanded) drawable.ic_arrow_down else drawable.ic_arrow_down ), contentDescription = stringResource( if (expanded) string.content_description_collapse_opening_hours else string.content_description_expand_opening_hours ), modifier = Modifier.size(24.dp) ) OpeningHoursHeader(expanded, currentStatus) { expanded = it } // Expanded content with table Loading @@ -300,24 +245,7 @@ fun ExpandableOpeningHours( Spacer(modifier = Modifier.height(8.dp)) // Table header Row( modifier = Modifier.fillMaxWidth() ) { Text( text = "Day", modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurfaceVariant ) Text( text = "Hours", modifier = Modifier.weight(2f), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } OpeningHoursTableHeader() HorizontalDivider( modifier = Modifier.padding(vertical = 4.dp), Loading @@ -326,6 +254,21 @@ fun ExpandableOpeningHours( // Table rows for each day openingHoursData.forEach { dayHours -> OpeningHoursTableRow(dayHours) if (dayHours != openingHoursData.last()) { HorizontalDivider( color = MaterialTheme.colorScheme.outlineVariant ) } } } } } } @Composable private fun OpeningHoursTableRow(dayHours: DayOpeningHours) { Row( modifier = Modifier .fillMaxWidth() Loading @@ -351,19 +294,88 @@ fun ExpandableOpeningHours( color = MaterialTheme.colorScheme.onSurface ) } } if (dayHours != openingHoursData.last()) { HorizontalDivider( color = MaterialTheme.colorScheme.outlineVariant @Composable private fun OpeningHoursTableHeader() { Row( modifier = Modifier.fillMaxWidth() ) { Text( text = stringResource(string.opening_hours_day), modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurfaceVariant ) Text( text = stringResource(string.opening_hours_hours), modifier = Modifier.weight(2f), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @Composable private fun OpeningHoursHeader( expanded: Boolean, currentStatus: OpeningStatusDisplay?, onExpandChanged: (Boolean) -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() .clickable { onExpandChanged(!expanded) }, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column( modifier = Modifier.weight(1f) ) { Text( text = stringResource(string.opening_hours_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium ) currentStatus?.let { status -> Row( modifier = Modifier.padding(top = 4.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = status.statusText, color = status.statusColor, style = MaterialTheme.typography.bodyMedium ) status.nextTimeText?.let { nextTime -> Spacer(modifier = Modifier.width(4.dp)) Text( text = nextTime, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } } data class OpeningStatus( Icon( painter = painterResource( drawable.ic_arrow_down ), contentDescription = stringResource( if (expanded) string.content_description_collapse_opening_hours else string.content_description_expand_opening_hours ), modifier = Modifier.size(24.dp) ) } } data class OpeningStatusDisplay( val statusText: String, val statusColor: Color, val nextTimeText: String? Loading @@ -376,7 +388,7 @@ fun getCurrentOpeningStatus( now: LocalDateTime, timeZone: TimeZone, use24HourFormat: Boolean ): OpeningStatus? { ): OpeningStatusDisplay? { place.openingHours?.let { openingHours -> val today = now.dayOfWeek val parser = Loading @@ -391,41 +403,17 @@ fun getCurrentOpeningStatus( val timeOfDay = now.time val minutesFromMidnight = timeOfDay.minute + timeOfDay.hour * 60 var isOpen = false var openingTimeToday: Int? = null var closingTimeToday: Int? = null val openingStatus = processOpeningHoursRules(rules, today, minutesFromMidnight) for (rule in rules) { val days = rule.days val times = rule.times if (days == null || times == null) { continue } for (dayRule in days) { if (weekdayRangeIncludesDay(dayRule, today.ordinal)) { for (timeRule in times) { if (timeRule.start < minutesFromMidnight && timeRule.end > minutesFromMidnight) { isOpen = true if (closingTimeToday == null || timeRule.end < closingTimeToday) { closingTimeToday = timeRule.end } } else if (openingTimeToday == null || timeRule.start < openingTimeToday) { openingTimeToday = timeRule.start } } } } } val openingInstantToday = openingTimeToday?.let { openingTime -> val openingInstantToday = openingStatus.openingTimeToday?.let { openingTime -> now.date.atTime(0, 0).toInstant(timeZone = timeZone).plus(openingTime.minutes) } val closingInstantToday = closingTimeToday?.let { closingTime -> val closingInstantToday = openingStatus.closingTimeToday?.let { closingTime -> now.date.atTime(0, 0).toInstant(timeZone = timeZone).plus(closingTime.minutes) } return if (isOpen) { OpeningStatus( return if (openingStatus.isOpen) { OpeningStatusDisplay( statusText = stringResource(string.opening_hours_open), statusColor = Color.Green, nextTimeText = closingInstantToday?.let { closingInstant -> Loading @@ -436,7 +424,7 @@ fun getCurrentOpeningStatus( } ) } else { OpeningStatus( OpeningStatusDisplay( statusText = stringResource(string.opening_hours_closed), statusColor = Color.Red, nextTimeText = openingInstantToday?.let { openingInstant -> Loading @@ -451,6 +439,50 @@ fun getCurrentOpeningStatus( return null } data class OpeningStatus( val isOpen: Boolean, val closingTimeToday: Int?, val openingTimeToday: Int?, ) @Suppress("CognitiveComplexMethod") private fun processOpeningHoursRules( rules: List<Rule>, today: DayOfWeek, minutesFromMidnight: Int, ): OpeningStatus { var isOpen = false var closingTimeToday: Int? = null var openingTimeToday: Int? = null for (rule in rules) { val days = rule.days val times = rule.times if (days == null || times == null) { continue } for (dayRule in days) { if (weekdayRangeIncludesDay(dayRule, today.ordinal)) { for (timeRule in times) { if (timeRule.start < minutesFromMidnight && timeRule.end > minutesFromMidnight) { isOpen = true if (closingTimeToday == null || timeRule.end < closingTimeToday) { closingTimeToday = timeRule.end } } else if (openingTimeToday == null || timeRule.start < openingTimeToday) { openingTimeToday = timeRule.start } } } } } return OpeningStatus( isOpen, closingTimeToday, openingTimeToday, ) } @OptIn(ExperimentalTime::class) @Composable fun PlaceCardScreen( Loading cardinal-android/app/src/main/res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -284,4 +284,6 @@ <string name="day_sunday">Sunday</string> <string name="day_today">Today</string> <string name="opening_hours_hours">Hours</string> <string name="opening_hours_day">Day</string> </resources> Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/PlaceCardScreen.kt +172 −140 Original line number Diff line number Diff line Loading @@ -64,6 +64,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import ch.poole.openinghoursparser.OpeningHoursParseException import ch.poole.openinghoursparser.OpeningHoursParser import ch.poole.openinghoursparser.Rule import ch.poole.openinghoursparser.WeekDayRange import earth.maps.cardinal.R.dimen import earth.maps.cardinal.R.drawable Loading Loading @@ -189,32 +190,22 @@ fun getOpeningHoursForNext7Days( return dayOpeningHours } fun ordinalInRange(ord: Int, start: Int, end: Int): Boolean { return ord >= start && ord <= end } fun weekdayRangeIncludesDay(range: WeekDayRange, day: Int): Boolean { if (range.startDay != null && range.startDay.ordinal == day) { return true } else if (range.startDay == null || range.endDay == null) { return false } if (range.endDay < range.startDay) { for (ord in 0..range.endDay.ordinal) { if (ord == day) { return true } } for (ord in range.startDay.ordinal..6) { if (ord == day) { return true } } return if (range.endDay < range.startDay) { ordinalInRange(day, 0, range.endDay.ordinal) || ordinalInRange(day, range.startDay.ordinal, 6) } else { for (ord in range.startDay.ordinal..range.endDay.ordinal) { if (ord == day) { return true ordinalInRange(day, range.startDay.ordinal, range.endDay.ordinal) } } } return false } @OptIn(ExperimentalTime::class) @Composable Loading Loading @@ -245,54 +236,8 @@ fun ExpandableOpeningHours( .padding(16.dp) ) { // Header with current status and expand/collapse icon Row( modifier = Modifier .fillMaxWidth() .clickable { expanded = !expanded }, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column( modifier = Modifier.weight(1f) ) { Text( text = stringResource(string.opening_hours_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium ) currentStatus?.let { status -> Row( modifier = Modifier.padding(top = 4.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = status.statusText, color = status.statusColor, style = MaterialTheme.typography.bodyMedium ) status.nextTimeText?.let { nextTime -> Spacer(modifier = Modifier.width(4.dp)) Text( text = nextTime, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } } Icon( painter = painterResource( if (expanded) drawable.ic_arrow_down else drawable.ic_arrow_down ), contentDescription = stringResource( if (expanded) string.content_description_collapse_opening_hours else string.content_description_expand_opening_hours ), modifier = Modifier.size(24.dp) ) OpeningHoursHeader(expanded, currentStatus) { expanded = it } // Expanded content with table Loading @@ -300,24 +245,7 @@ fun ExpandableOpeningHours( Spacer(modifier = Modifier.height(8.dp)) // Table header Row( modifier = Modifier.fillMaxWidth() ) { Text( text = "Day", modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurfaceVariant ) Text( text = "Hours", modifier = Modifier.weight(2f), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } OpeningHoursTableHeader() HorizontalDivider( modifier = Modifier.padding(vertical = 4.dp), Loading @@ -326,6 +254,21 @@ fun ExpandableOpeningHours( // Table rows for each day openingHoursData.forEach { dayHours -> OpeningHoursTableRow(dayHours) if (dayHours != openingHoursData.last()) { HorizontalDivider( color = MaterialTheme.colorScheme.outlineVariant ) } } } } } } @Composable private fun OpeningHoursTableRow(dayHours: DayOpeningHours) { Row( modifier = Modifier .fillMaxWidth() Loading @@ -351,19 +294,88 @@ fun ExpandableOpeningHours( color = MaterialTheme.colorScheme.onSurface ) } } if (dayHours != openingHoursData.last()) { HorizontalDivider( color = MaterialTheme.colorScheme.outlineVariant @Composable private fun OpeningHoursTableHeader() { Row( modifier = Modifier.fillMaxWidth() ) { Text( text = stringResource(string.opening_hours_day), modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurfaceVariant ) Text( text = stringResource(string.opening_hours_hours), modifier = Modifier.weight(2f), style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @Composable private fun OpeningHoursHeader( expanded: Boolean, currentStatus: OpeningStatusDisplay?, onExpandChanged: (Boolean) -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() .clickable { onExpandChanged(!expanded) }, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column( modifier = Modifier.weight(1f) ) { Text( text = stringResource(string.opening_hours_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium ) currentStatus?.let { status -> Row( modifier = Modifier.padding(top = 4.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = status.statusText, color = status.statusColor, style = MaterialTheme.typography.bodyMedium ) status.nextTimeText?.let { nextTime -> Spacer(modifier = Modifier.width(4.dp)) Text( text = nextTime, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } } data class OpeningStatus( Icon( painter = painterResource( drawable.ic_arrow_down ), contentDescription = stringResource( if (expanded) string.content_description_collapse_opening_hours else string.content_description_expand_opening_hours ), modifier = Modifier.size(24.dp) ) } } data class OpeningStatusDisplay( val statusText: String, val statusColor: Color, val nextTimeText: String? Loading @@ -376,7 +388,7 @@ fun getCurrentOpeningStatus( now: LocalDateTime, timeZone: TimeZone, use24HourFormat: Boolean ): OpeningStatus? { ): OpeningStatusDisplay? { place.openingHours?.let { openingHours -> val today = now.dayOfWeek val parser = Loading @@ -391,41 +403,17 @@ fun getCurrentOpeningStatus( val timeOfDay = now.time val minutesFromMidnight = timeOfDay.minute + timeOfDay.hour * 60 var isOpen = false var openingTimeToday: Int? = null var closingTimeToday: Int? = null val openingStatus = processOpeningHoursRules(rules, today, minutesFromMidnight) for (rule in rules) { val days = rule.days val times = rule.times if (days == null || times == null) { continue } for (dayRule in days) { if (weekdayRangeIncludesDay(dayRule, today.ordinal)) { for (timeRule in times) { if (timeRule.start < minutesFromMidnight && timeRule.end > minutesFromMidnight) { isOpen = true if (closingTimeToday == null || timeRule.end < closingTimeToday) { closingTimeToday = timeRule.end } } else if (openingTimeToday == null || timeRule.start < openingTimeToday) { openingTimeToday = timeRule.start } } } } } val openingInstantToday = openingTimeToday?.let { openingTime -> val openingInstantToday = openingStatus.openingTimeToday?.let { openingTime -> now.date.atTime(0, 0).toInstant(timeZone = timeZone).plus(openingTime.minutes) } val closingInstantToday = closingTimeToday?.let { closingTime -> val closingInstantToday = openingStatus.closingTimeToday?.let { closingTime -> now.date.atTime(0, 0).toInstant(timeZone = timeZone).plus(closingTime.minutes) } return if (isOpen) { OpeningStatus( return if (openingStatus.isOpen) { OpeningStatusDisplay( statusText = stringResource(string.opening_hours_open), statusColor = Color.Green, nextTimeText = closingInstantToday?.let { closingInstant -> Loading @@ -436,7 +424,7 @@ fun getCurrentOpeningStatus( } ) } else { OpeningStatus( OpeningStatusDisplay( statusText = stringResource(string.opening_hours_closed), statusColor = Color.Red, nextTimeText = openingInstantToday?.let { openingInstant -> Loading @@ -451,6 +439,50 @@ fun getCurrentOpeningStatus( return null } data class OpeningStatus( val isOpen: Boolean, val closingTimeToday: Int?, val openingTimeToday: Int?, ) @Suppress("CognitiveComplexMethod") private fun processOpeningHoursRules( rules: List<Rule>, today: DayOfWeek, minutesFromMidnight: Int, ): OpeningStatus { var isOpen = false var closingTimeToday: Int? = null var openingTimeToday: Int? = null for (rule in rules) { val days = rule.days val times = rule.times if (days == null || times == null) { continue } for (dayRule in days) { if (weekdayRangeIncludesDay(dayRule, today.ordinal)) { for (timeRule in times) { if (timeRule.start < minutesFromMidnight && timeRule.end > minutesFromMidnight) { isOpen = true if (closingTimeToday == null || timeRule.end < closingTimeToday) { closingTimeToday = timeRule.end } } else if (openingTimeToday == null || timeRule.start < openingTimeToday) { openingTimeToday = timeRule.start } } } } } return OpeningStatus( isOpen, closingTimeToday, openingTimeToday, ) } @OptIn(ExperimentalTime::class) @Composable fun PlaceCardScreen( Loading
cardinal-android/app/src/main/res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -284,4 +284,6 @@ <string name="day_sunday">Sunday</string> <string name="day_today">Today</string> <string name="opening_hours_hours">Hours</string> <string name="opening_hours_day">Day</string> </resources>