Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ uses semantic versioning (`MAJOR.MINOR.PATCH`).
- **Announcements can be scheduled with a date picker.** Admin only. The start
and expiry wanted a hand-typed `2026-12-31T00:00:00Z`, where one typo meant
the server rejected the whole announcement. Both are now a date picker and a
time field, as is the expiry on an invite code.
time picker, as is the expiry on an invite code.

- **Announcements can be set to show on the login page.** Admin only. The
server has supported this for a long time and the setting simply wasn't on
Expand All @@ -32,6 +32,15 @@ uses semantic versioning (`MAJOR.MINOR.PATCH`).
screens down and left the search field scrolled off above its own results.
Searching also waits for a pause in typing now, instead of asking the server
again on every keystroke.
- **Times are picked from a clock instead of typed.** Front history entries
and admin schedules had separate hour and minute boxes with an AM/PM
toggle, and tapping one brought up the full keyboard. The time is now a
button next to the date that opens the standard time picker, which follows
the phone's 12/24 hour setting.
- **The front entry member list is shorter.** Adding or editing a front
entry listed every member before you could reach the times. It now shows
the first five plus anyone already picked, with "Show all" to expand.
Searching or filtering by group still shows every match.
- **A banner image for your system.** Settings > Profile gains a banner
beside the avatar, the same wide image a member can have, with the same
cropper. Needs a server running 1.6.0 or later.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import systems.lupine.sheaf.data.model.AnnouncementRead
import systems.lupine.sheaf.data.model.AnnouncementUpdate
import systems.lupine.sheaf.data.model.InviteCodeRead
import systems.lupine.sheaf.ui.components.ErrorBanner
import systems.lupine.sheaf.ui.components.TimeInputRow
import systems.lupine.sheaf.ui.components.TimePickerButton
import systems.lupine.sheaf.ui.components.datePickerDate
import systems.lupine.sheaf.ui.components.datePickerMillis
import systems.lupine.sheaf.ui.components.SectionHeader
Expand Down Expand Up @@ -700,7 +700,7 @@ private fun AnnouncementDialog(
}

/**
* An optional point in time, as a checkbox plus a date button and a time row.
* An optional point in time, as a checkbox plus a date button and a time button.
*
* Replaces a free-text field that wanted a hand-typed `2026-12-31T00:00:00Z`:
* one typo and the server rejected the whole announcement, and getting it right
Expand Down Expand Up @@ -766,15 +766,18 @@ private fun ScheduleField(
)
}
if (local != null) {
OutlinedButton(onClick = { showDatePicker = true }, modifier = Modifier.fillMaxWidth()) {
Text(local.toLocalDate().format(DateTimeFormatter.ofPattern("MMM d, yyyy")))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = { showDatePicker = true }, modifier = Modifier.weight(1f)) {
Text(local.toLocalDate().format(DateTimeFormatter.ofPattern("MMM d, yyyy")))
}
TimePickerButton(
time = local.toLocalTime(),
onTimeChange = { t ->
onChange(LocalDateTime.of(local.toLocalDate(), t).atZone(zone).toInstant())
},
modifier = Modifier.weight(1f),
)
}
TimeInputRow(
time = local.toLocalTime(),
onTimeChange = { time ->
onChange(LocalDateTime.of(local.toLocalDate(), time).atZone(zone).toInstant())
},
)
}
}
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package systems.lupine.sheaf.ui.components

import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker
import androidx.compose.material3.TimePickerDialog
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import java.time.LocalTime
import java.time.format.DateTimeFormatter

/**
* A button showing a time of day that opens the Material time picker.
*
* Shared so that every place in the app asking for a time of day asks for it
* the same way: front history entries, and the schedule on a server
* announcement. Pair it with a `DatePickerDialog` seeded through
* [datePickerMillis] to build a full timestamp.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePickerButton(time: LocalTime, onTimeChange: (LocalTime) -> Unit, modifier: Modifier = Modifier) {
var open by remember { mutableStateOf(false) }

if (open) {
val state = rememberTimePickerState(initialHour = time.hour, initialMinute = time.minute)
TimePickerDialog(
onDismissRequest = { open = false },
confirmButton = {
TextButton(onClick = {
onTimeChange(LocalTime.of(state.hour, state.minute))
open = false
}) { Text("OK") }
},
dismissButton = { TextButton(onClick = { open = false }) { Text("Cancel") } },
title = { Text("Select time") },
) { TimePicker(state = state) }
}

OutlinedButton(onClick = { open = true }, modifier = modifier) {
Text(time.format(DateTimeFormatter.ofPattern("h:mm a")))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ private fun FrontEntrySheet(
)
} else {
var memberQuery by remember { mutableStateOf("") }
var showAllMembers by remember { mutableStateOf(false) }
if (groups.isNotEmpty()) {
GroupFilterChips(
groups = groups,
Expand All @@ -818,7 +819,12 @@ private fun FrontEntrySheet(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
filteredMembers.forEach { member ->
val filtering = memberQuery.isNotBlank() || activeGroupId != null
val collapsed = !showAllMembers && !filtering && filteredMembers.size > 5
val visibleMembers = if (collapsed) {
filteredMembers.filterIndexed { i, m -> i < 5 || m.id in selectedIds }
} else filteredMembers
visibleMembers.forEach { member ->
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
Expand All @@ -835,6 +841,11 @@ private fun FrontEntrySheet(
Text(member.displayNameWithEmoji, style = MaterialTheme.typography.bodyMedium)
}
}
if (!filtering && filteredMembers.size > 5) {
TextButton(onClick = { showAllMembers = !showAllMembers }) {
Text(if (showAllMembers) "Show less" else "Show all (${filteredMembers.size})")
}
}
}
}

Expand All @@ -845,8 +856,8 @@ private fun FrontEntrySheet(
OutlinedButton(onClick = { showStartDatePicker = true }, modifier = Modifier.weight(1f)) {
Text(startDate.format(DateTimeFormatter.ofPattern("MMM d, yyyy")))
}
TimePickerButton(time = startTime, onTimeChange = { startTime = it }, modifier = Modifier.weight(1f))
}
TimeInputRow(time = startTime, onTimeChange = { startTime = it })

HorizontalDivider()

Expand All @@ -861,8 +872,8 @@ private fun FrontEntrySheet(
OutlinedButton(onClick = { showEndDatePicker = true }, modifier = Modifier.weight(1f)) {
Text(endDate.format(DateTimeFormatter.ofPattern("MMM d, yyyy")))
}
TimePickerButton(time = endTime, onTimeChange = { endTime = it }, modifier = Modifier.weight(1f))
}
TimeInputRow(time = endTime, onTimeChange = { endTime = it })
}

HorizontalDivider()
Expand Down
Loading