Compare commits
1 Commits
release/v2
...
5c141fc9a9
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c141fc9a9 |
@@ -24,7 +24,7 @@ jobs:
|
|||||||
# directly drops the docker-in-docker requirement entirely.
|
# directly drops the docker-in-docker requirement entirely.
|
||||||
# Full tag pinned; Renovate's github-actions manager keeps it bumped.
|
# Full tag pinned; Renovate's github-actions manager keeps it bumped.
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/renovatebot/renovate:43.232.0
|
image: ghcr.io/renovatebot/renovate:44.31.0
|
||||||
steps:
|
steps:
|
||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
run: renovate
|
run: renovate
|
||||||
|
|||||||
@@ -7,13 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Month-view event titles no longer end in an ellipsis. The "…" took the width
|
|
||||||
of a couple of characters and told you nothing you couldn't already see, so
|
|
||||||
the title now simply runs to the edge of its chip — a few more letters per
|
|
||||||
cell, which is often the difference between two events you can tell apart and
|
|
||||||
two you can't ([#164]).
|
|
||||||
|
|
||||||
## [2.19.1] — 2026-08-11
|
## [2.19.1] — 2026-08-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -1421,4 +1414,3 @@ automatically, with zero telemetry and no internet permission.
|
|||||||
[#123]: https://codeberg.org/jlmakiola/calendula/issues/123
|
[#123]: https://codeberg.org/jlmakiola/calendula/issues/123
|
||||||
[#163]: https://codeberg.org/jlmakiola/calendula/issues/163
|
[#163]: https://codeberg.org/jlmakiola/calendula/issues/163
|
||||||
[#173]: https://codeberg.org/jlmakiola/calendula/issues/173
|
[#173]: https://codeberg.org/jlmakiola/calendula/issues/173
|
||||||
[#164]: https://codeberg.org/jlmakiola/calendula/issues/164
|
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ fun AgendaScreen(
|
|||||||
AgendaTopBar(
|
AgendaTopBar(
|
||||||
selectedView = selectedView,
|
selectedView = selectedView,
|
||||||
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
||||||
quickSwitchViews = quickSwitchViews,
|
|
||||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||||
onOpenSearch = onOpenSearch,
|
onOpenSearch = onOpenSearch,
|
||||||
showTodayButton = todayInToolbar,
|
showTodayButton = todayInToolbar,
|
||||||
@@ -410,7 +409,6 @@ private fun AgendaEmpty(modifier: Modifier = Modifier) {
|
|||||||
private fun AgendaTopBar(
|
private fun AgendaTopBar(
|
||||||
selectedView: CalendarView,
|
selectedView: CalendarView,
|
||||||
onCycleView: () -> Unit,
|
onCycleView: () -> Unit,
|
||||||
quickSwitchViews: List<CalendarView>,
|
|
||||||
onOpenDrawer: () -> Unit,
|
onOpenDrawer: () -> Unit,
|
||||||
onOpenSearch: () -> Unit,
|
onOpenSearch: () -> Unit,
|
||||||
showTodayButton: Boolean,
|
showTodayButton: Boolean,
|
||||||
@@ -442,7 +440,6 @@ private fun AgendaTopBar(
|
|||||||
}
|
}
|
||||||
ViewSwitcherPill(
|
ViewSwitcherPill(
|
||||||
current = selectedView,
|
current = selectedView,
|
||||||
cycle = quickSwitchViews,
|
|
||||||
onCycle = onCycleView,
|
onCycle = onCycleView,
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -57,8 +57,7 @@ fun CalendarView.next(available: List<CalendarView> = IMPLEMENTED_VIEWS): Calend
|
|||||||
* implemented view — the settings screen reorders the whole set — while [cycle]
|
* implemented view — the settings screen reorders the whole set — while [cycle]
|
||||||
* is the subset the pill actually steps through, in [order]. The navigation
|
* is the subset the pill actually steps through, in [order]. The navigation
|
||||||
* drawer keeps its own separate order and always lists every view, so a view
|
* drawer keeps its own separate order and always lists every view, so a view
|
||||||
* disabled here stays reachable there — including when [cycle] is emptied and
|
* disabled here stays reachable there.
|
||||||
* the pill disappears altogether.
|
|
||||||
*/
|
*/
|
||||||
data class QuickSwitchConfig(
|
data class QuickSwitchConfig(
|
||||||
val order: List<CalendarView>,
|
val order: List<CalendarView>,
|
||||||
@@ -68,15 +67,14 @@ data class QuickSwitchConfig(
|
|||||||
val cycle: List<CalendarView> get() = order.filter { it in enabled }
|
val cycle: List<CalendarView> get() = order.filter { it in enabled }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* Fewest views that keep the switch meaningful. A single target is not a
|
|
||||||
* switch, so below this the pill is hidden rather than special-cased (#150);
|
|
||||||
* the drawer still reaches every view.
|
|
||||||
*/
|
|
||||||
const val MIN_CYCLE = 2
|
|
||||||
|
|
||||||
/** All views, in default order, all enabled. */
|
/** All views, in default order, all enabled. */
|
||||||
val Default = QuickSwitchConfig(IMPLEMENTED_VIEWS, IMPLEMENTED_VIEWS.toSet())
|
val Default = QuickSwitchConfig(IMPLEMENTED_VIEWS, IMPLEMENTED_VIEWS.toSet())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fewest views that keep the switch meaningful — a "switch" needs at
|
||||||
|
* least two targets, so the settings screen blocks disabling below this.
|
||||||
|
*/
|
||||||
|
const val MIN_ENABLED = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
|
||||||
|
|
||||||
/** How an event chip's title should overflow: what to pass to `Text`. */
|
|
||||||
data class EventTitleOverflow(val overflow: TextOverflow, val softWrap: Boolean)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overflow for an event chip's title (#164). Chips are narrow enough that the
|
|
||||||
* "…" costs a couple of readable characters, so the title runs to the chip's
|
|
||||||
* edge and clips mid-glyph instead.
|
|
||||||
*
|
|
||||||
* Two cases keep the ellipsis:
|
|
||||||
*
|
|
||||||
* - **[rtl].** With `softWrap` off Compose lays the line out at its full
|
|
||||||
* intrinsic width and clips to the node's left edge, which in RTL is the *end*
|
|
||||||
* of the string — an Arabic title would lose its beginning. The ellipsis
|
|
||||||
* truncates at the logical end in both directions.
|
|
||||||
* - **More than one line** ([singleLine] false). Wrapping needs `softWrap` on,
|
|
||||||
* and clipping with it on breaks the last line at the last whole word — less
|
|
||||||
* title than the ellipsis showed, not more.
|
|
||||||
*/
|
|
||||||
fun eventTitleOverflowFor(rtl: Boolean, singleLine: Boolean): EventTitleOverflow =
|
|
||||||
if (rtl || !singleLine) {
|
|
||||||
EventTitleOverflow(TextOverflow.Ellipsis, softWrap = true)
|
|
||||||
} else {
|
|
||||||
EventTitleOverflow(TextOverflow.Clip, softWrap = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** [eventTitleOverflowFor] against the current layout direction. */
|
|
||||||
@Composable
|
|
||||||
fun eventTitleOverflow(singleLine: Boolean = true): EventTitleOverflow =
|
|
||||||
eventTitleOverflowFor(
|
|
||||||
rtl = LocalLayoutDirection.current == LayoutDirection.Rtl,
|
|
||||||
singleLine = singleLine,
|
|
||||||
)
|
|
||||||
@@ -464,13 +464,11 @@ fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier =
|
|||||||
.padding(horizontal = 4.dp, vertical = 2.dp),
|
.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
val titleOverflow = eventTitleOverflow()
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = titleOverflow.overflow,
|
overflow = TextOverflow.Ellipsis,
|
||||||
softWrap = titleOverflow.softWrap,
|
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = 0.85f),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -10,19 +10,13 @@ import androidx.compose.ui.res.stringResource
|
|||||||
/**
|
/**
|
||||||
* Top-bar pill that shows the current view and cycles to the next one on tap
|
* Top-bar pill that shows the current view and cycles to the next one on tap
|
||||||
* (spec M1: Month → Week → Day → Month, restricted to [IMPLEMENTED_VIEWS]).
|
* (spec M1: Month → Week → Day → Month, restricted to [IMPLEMENTED_VIEWS]).
|
||||||
*
|
|
||||||
* Renders nothing when [cycle] holds fewer than [QuickSwitchConfig.MIN_CYCLE]
|
|
||||||
* views (#150), so it drops straight into an app bar's `actions` slot without a
|
|
||||||
* wrapping condition — the same way [TodayAction] handles being turned off.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ViewSwitcherPill(
|
fun ViewSwitcherPill(
|
||||||
current: CalendarView,
|
current: CalendarView,
|
||||||
cycle: List<CalendarView>,
|
|
||||||
onCycle: () -> Unit,
|
onCycle: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (cycle.size < QuickSwitchConfig.MIN_CYCLE) return
|
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
onClick = onCycle,
|
onClick = onCycle,
|
||||||
shape = MaterialTheme.shapes.large,
|
shape = MaterialTheme.shapes.large,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.customActions
|
import androidx.compose.ui.semantics.customActions
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -93,7 +94,6 @@ import de.jeanlucmakiola.calendula.ui.common.TimelineDrop
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.beginsOn
|
import de.jeanlucmakiola.calendula.ui.common.beginsOn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed
|
import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventMoveAction
|
import de.jeanlucmakiola.calendula.ui.common.eventMoveAction
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventTitleOverflow
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberEventDragSource
|
import de.jeanlucmakiola.calendula.ui.common.rememberEventDragSource
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
|
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
|
||||||
import de.jeanlucmakiola.calendula.ui.common.startInstant
|
import de.jeanlucmakiola.calendula.ui.common.startInstant
|
||||||
@@ -243,7 +243,6 @@ fun DayScreen(
|
|||||||
currentYear = currentYear,
|
currentYear = currentYear,
|
||||||
selectedView = selectedView,
|
selectedView = selectedView,
|
||||||
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
||||||
quickSwitchViews = quickSwitchViews,
|
|
||||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||||
onOpenSearch = onOpenSearch,
|
onOpenSearch = onOpenSearch,
|
||||||
onJumpToDate = jumpToDate,
|
onJumpToDate = jumpToDate,
|
||||||
@@ -412,7 +411,6 @@ private fun DayTopBar(
|
|||||||
currentYear: Int,
|
currentYear: Int,
|
||||||
selectedView: CalendarView,
|
selectedView: CalendarView,
|
||||||
onCycleView: () -> Unit,
|
onCycleView: () -> Unit,
|
||||||
quickSwitchViews: List<CalendarView>,
|
|
||||||
onOpenDrawer: () -> Unit,
|
onOpenDrawer: () -> Unit,
|
||||||
onOpenSearch: () -> Unit,
|
onOpenSearch: () -> Unit,
|
||||||
onJumpToDate: (LocalDate) -> Unit,
|
onJumpToDate: (LocalDate) -> Unit,
|
||||||
@@ -447,7 +445,6 @@ private fun DayTopBar(
|
|||||||
}
|
}
|
||||||
ViewSwitcherPill(
|
ViewSwitcherPill(
|
||||||
current = selectedView,
|
current = selectedView,
|
||||||
cycle = quickSwitchViews,
|
|
||||||
onCycle = onCycleView,
|
onCycle = onCycleView,
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
)
|
)
|
||||||
@@ -522,13 +519,11 @@ private fun AllDayBar(
|
|||||||
.semantics { contentDescription = title },
|
.semantics { contentDescription = title },
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
val titleOverflow = eventTitleOverflow()
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = titleOverflow.overflow,
|
overflow = TextOverflow.Ellipsis,
|
||||||
softWrap = titleOverflow.softWrap,
|
|
||||||
color = eventInk(fill),
|
color = eventInk(fill),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -771,13 +766,11 @@ private fun EventBlock(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
if (showTitle) {
|
if (showTitle) {
|
||||||
val titleOverflow = eventTitleOverflow(singleLine = showTime)
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = if (showTime) 1 else 2,
|
maxLines = if (showTime) 1 else 2,
|
||||||
overflow = titleOverflow.overflow,
|
overflow = TextOverflow.Ellipsis,
|
||||||
softWrap = titleOverflow.softWrap,
|
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = 0.85f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberDragSurface
|
import de.jeanlucmakiola.calendula.ui.common.rememberDragSurface
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventMoveAction
|
import de.jeanlucmakiola.calendula.ui.common.eventMoveAction
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventTitleOverflow
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MoveTarget
|
import de.jeanlucmakiola.calendula.ui.common.MoveTarget
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MoveRequest
|
import de.jeanlucmakiola.calendula.ui.common.MoveRequest
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
||||||
@@ -387,7 +386,6 @@ fun MonthScreen(
|
|||||||
titleDate = LocalDate(titleMonth.year, titleMonth.month, 1),
|
titleDate = LocalDate(titleMonth.year, titleMonth.month, 1),
|
||||||
selectedView = selectedView,
|
selectedView = selectedView,
|
||||||
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
||||||
quickSwitchViews = quickSwitchViews,
|
|
||||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||||
onOpenSearch = onOpenSearch,
|
onOpenSearch = onOpenSearch,
|
||||||
onJumpToDate = jumpToDate,
|
onJumpToDate = jumpToDate,
|
||||||
@@ -652,7 +650,6 @@ private fun MonthTopBar(
|
|||||||
titleDate: LocalDate,
|
titleDate: LocalDate,
|
||||||
selectedView: CalendarView,
|
selectedView: CalendarView,
|
||||||
onCycleView: () -> Unit,
|
onCycleView: () -> Unit,
|
||||||
quickSwitchViews: List<CalendarView>,
|
|
||||||
onOpenDrawer: () -> Unit,
|
onOpenDrawer: () -> Unit,
|
||||||
onOpenSearch: () -> Unit,
|
onOpenSearch: () -> Unit,
|
||||||
onJumpToDate: (LocalDate) -> Unit,
|
onJumpToDate: (LocalDate) -> Unit,
|
||||||
@@ -686,7 +683,6 @@ private fun MonthTopBar(
|
|||||||
}
|
}
|
||||||
ViewSwitcherPill(
|
ViewSwitcherPill(
|
||||||
current = selectedView,
|
current = selectedView,
|
||||||
cycle = quickSwitchViews,
|
|
||||||
onCycle = onCycleView,
|
onCycle = onCycleView,
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
)
|
)
|
||||||
@@ -2356,13 +2352,11 @@ private fun MonthBar(
|
|||||||
},
|
},
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
val titleOverflow = eventTitleOverflow()
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = titleOverflow.overflow,
|
overflow = TextOverflow.Ellipsis,
|
||||||
softWrap = titleOverflow.softWrap,
|
|
||||||
color = eventInk(fill),
|
color = eventInk(fill),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -591,15 +591,15 @@ class SettingsViewModel @Inject constructor(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or disable [view] in the quick-switch cycle, via an atomic
|
* Enable or disable [view] in the quick-switch cycle, via an atomic
|
||||||
* read-modify-write so a concurrent reorder can't clobber it. Any number of
|
* read-modify-write so a concurrent reorder can't clobber it. The
|
||||||
* views may be turned off; below two the pill hides itself (#150).
|
* MIN_ENABLED floor is re-checked inside the transform, because the screen's
|
||||||
|
* own guard reads an async-echoed snapshot.
|
||||||
*/
|
*/
|
||||||
fun setQuickSwitchViewEnabled(view: CalendarView, enabled: Boolean) {
|
fun setQuickSwitchViewEnabled(view: CalendarView, enabled: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
prefs.updateQuickSwitch { config ->
|
prefs.updateQuickSwitch { config ->
|
||||||
config.copy(
|
val next = if (enabled) config.enabled + view else config.enabled - view
|
||||||
enabled = if (enabled) config.enabled + view else config.enabled - view,
|
if (next.size < QuickSwitchConfig.MIN_ENABLED) config else config.copy(enabled = next)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||||
import de.jeanlucmakiola.calendula.ui.common.PickerDescription
|
import de.jeanlucmakiola.calendula.ui.common.PickerDescription
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
|
||||||
import de.jeanlucmakiola.calendula.ui.common.TimelineScale
|
import de.jeanlucmakiola.calendula.ui.common.TimelineScale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel
|
import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel
|
||||||
import de.jeanlucmakiola.calendula.ui.common.descriptionRes
|
import de.jeanlucmakiola.calendula.ui.common.descriptionRes
|
||||||
@@ -54,8 +55,8 @@ import java.time.format.TextStyle as JavaTextStyle
|
|||||||
* it belongs to, plus the two cross-view ordering lists (#24, #69).
|
* it belongs to, plus the two cross-view ordering lists (#24, #69).
|
||||||
*
|
*
|
||||||
* The quick-switch cycle and the drawer list are independent orders — a view
|
* The quick-switch cycle and the drawer list are independent orders — a view
|
||||||
* off in the cycle is still reachable from the drawer, including when the cycle
|
* off in the cycle is still reachable from the drawer. The cycle needs at least
|
||||||
* is emptied and the pill disappears (#150).
|
* [QuickSwitchConfig.MIN_ENABLED] targets.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ViewsScreen(
|
internal fun ViewsScreen(
|
||||||
@@ -220,6 +221,8 @@ internal fun ViewsScreen(
|
|||||||
SectionHeader(stringResource(R.string.settings_quick_switch_header))
|
SectionHeader(stringResource(R.string.settings_quick_switch_header))
|
||||||
SettingsHint(stringResource(R.string.settings_quick_switch_hint))
|
SettingsHint(stringResource(R.string.settings_quick_switch_hint))
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
// Turning a view off is blocked once only the minimum remain enabled.
|
||||||
|
val canDisable = config.enabled.size > QuickSwitchConfig.MIN_ENABLED
|
||||||
ReorderableColumn(
|
ReorderableColumn(
|
||||||
items = config.order,
|
items = config.order,
|
||||||
keyOf = { it },
|
keyOf = { it },
|
||||||
@@ -235,6 +238,8 @@ internal fun ViewsScreen(
|
|||||||
trailing = {
|
trailing = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
|
// Keep the last two on: with fewer, the pill can't switch.
|
||||||
|
enabled = !checked || canDisable,
|
||||||
onCheckedChange = { on -> viewModel.setQuickSwitchViewEnabled(view, on) },
|
onCheckedChange = { on -> viewModel.setQuickSwitchViewEnabled(view, on) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import androidx.compose.ui.semantics.contentDescription
|
|||||||
import androidx.compose.ui.semantics.customActions
|
import androidx.compose.ui.semantics.customActions
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -101,7 +102,6 @@ import de.jeanlucmakiola.calendula.ui.common.TimelineDrop
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.beginsOn
|
import de.jeanlucmakiola.calendula.ui.common.beginsOn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed
|
import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventMoveAction
|
import de.jeanlucmakiola.calendula.ui.common.eventMoveAction
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventTitleOverflow
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberEventDragSource
|
import de.jeanlucmakiola.calendula.ui.common.rememberEventDragSource
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
|
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
|
||||||
import de.jeanlucmakiola.calendula.ui.common.startInstant
|
import de.jeanlucmakiola.calendula.ui.common.startInstant
|
||||||
@@ -266,7 +266,6 @@ fun WeekScreen(
|
|||||||
currentYear = currentYear,
|
currentYear = currentYear,
|
||||||
selectedView = selectedView,
|
selectedView = selectedView,
|
||||||
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
||||||
quickSwitchViews = quickSwitchViews,
|
|
||||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||||
onOpenSearch = onOpenSearch,
|
onOpenSearch = onOpenSearch,
|
||||||
onJumpToDate = jumpToDate,
|
onJumpToDate = jumpToDate,
|
||||||
@@ -447,7 +446,6 @@ private fun WeekTopBar(
|
|||||||
currentYear: Int,
|
currentYear: Int,
|
||||||
selectedView: CalendarView,
|
selectedView: CalendarView,
|
||||||
onCycleView: () -> Unit,
|
onCycleView: () -> Unit,
|
||||||
quickSwitchViews: List<CalendarView>,
|
|
||||||
onOpenDrawer: () -> Unit,
|
onOpenDrawer: () -> Unit,
|
||||||
onOpenSearch: () -> Unit,
|
onOpenSearch: () -> Unit,
|
||||||
onJumpToDate: (LocalDate) -> Unit,
|
onJumpToDate: (LocalDate) -> Unit,
|
||||||
@@ -482,7 +480,6 @@ private fun WeekTopBar(
|
|||||||
}
|
}
|
||||||
ViewSwitcherPill(
|
ViewSwitcherPill(
|
||||||
current = selectedView,
|
current = selectedView,
|
||||||
cycle = quickSwitchViews,
|
|
||||||
onCycle = onCycleView,
|
onCycle = onCycleView,
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
)
|
)
|
||||||
@@ -659,13 +656,11 @@ private fun AllDayBar(
|
|||||||
.semantics { contentDescription = title },
|
.semantics { contentDescription = title },
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
val titleOverflow = eventTitleOverflow()
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = titleOverflow.overflow,
|
overflow = TextOverflow.Ellipsis,
|
||||||
softWrap = titleOverflow.softWrap,
|
|
||||||
color = eventInk(fill),
|
color = eventInk(fill),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -939,13 +934,11 @@ private fun EventBlock(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
if (showTitle) {
|
if (showTitle) {
|
||||||
val titleOverflow = eventTitleOverflow(singleLine = titleMaxLines == 1)
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = titleMaxLines,
|
maxLines = titleMaxLines,
|
||||||
overflow = titleOverflow.overflow,
|
overflow = TextOverflow.Ellipsis,
|
||||||
softWrap = titleOverflow.softWrap,
|
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = 0.85f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -496,7 +496,7 @@
|
|||||||
<string name="month_split_expand">Show the whole month</string>
|
<string name="month_split_expand">Show the whole month</string>
|
||||||
<string name="month_split_collapse">Show the day\'s events</string>
|
<string name="month_split_collapse">Show the day\'s events</string>
|
||||||
<string name="settings_quick_switch_header">Quick-switch button</string>
|
<string name="settings_quick_switch_header">Quick-switch button</string>
|
||||||
<string name="settings_quick_switch_hint">Choose which views the top-right button cycles through, and drag to reorder them. With fewer than two views turned on the button is hidden. Turned-off views stay reachable from the navigation menu.</string>
|
<string name="settings_quick_switch_hint">Choose which views the top-right button cycles through, and drag to reorder them. Turned-off views stay reachable from the navigation menu.</string>
|
||||||
<string name="settings_drawer_order_header">Navigation menu</string>
|
<string name="settings_drawer_order_header">Navigation menu</string>
|
||||||
<string name="settings_drawer_order_hint">Drag to reorder the views listed in the navigation menu.</string>
|
<string name="settings_drawer_order_hint">Drag to reorder the views listed in the navigation menu.</string>
|
||||||
<string name="reorder_drag_handle">Drag to reorder</string>
|
<string name="reorder_drag_handle">Drag to reorder</string>
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
|
||||||
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
class EventTitleOverflowTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a single LTR line clips, so the title runs to the chip's edge`() {
|
|
||||||
val result = eventTitleOverflowFor(rtl = false, singleLine = true)
|
|
||||||
assertThat(result.overflow).isEqualTo(TextOverflow.Clip)
|
|
||||||
// Load-bearing: with softWrap on, a clipped line breaks at the last
|
|
||||||
// whole word and shows less title than the ellipsis did (#164).
|
|
||||||
assertThat(result.softWrap).isFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `RTL keeps the ellipsis, which truncates at the logical end`() {
|
|
||||||
// Clipping with softWrap off cuts at the node's left edge, which in RTL
|
|
||||||
// is the end of the string — the title would lose its beginning.
|
|
||||||
val result = eventTitleOverflowFor(rtl = true, singleLine = true)
|
|
||||||
assertThat(result.overflow).isEqualTo(TextOverflow.Ellipsis)
|
|
||||||
assertThat(result.softWrap).isTrue()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a multi-line block keeps the ellipsis, since wrapping needs softWrap`() {
|
|
||||||
val result = eventTitleOverflowFor(rtl = false, singleLine = false)
|
|
||||||
assertThat(result.overflow).isEqualTo(TextOverflow.Ellipsis)
|
|
||||||
assertThat(result.softWrap).isTrue()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `multi-line in RTL keeps the ellipsis too`() {
|
|
||||||
val result = eventTitleOverflowFor(rtl = true, singleLine = false)
|
|
||||||
assertThat(result.overflow).isEqualTo(TextOverflow.Ellipsis)
|
|
||||||
assertThat(result.softWrap).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -120,30 +120,4 @@ class ViewBackStackTest {
|
|||||||
)
|
)
|
||||||
assertThat(config.cycle).containsExactly(CalendarView.Agenda, CalendarView.Month).inOrder()
|
assertThat(config.cycle).containsExactly(CalendarView.Agenda, CalendarView.Month).inOrder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a cycle can be emptied down to no views at all`() {
|
|
||||||
// #150: the settings screen no longer holds a floor, so every view may go.
|
|
||||||
val config = QuickSwitchConfig(order = IMPLEMENTED_VIEWS, enabled = emptySet())
|
|
||||||
assertThat(config.cycle).isEmpty()
|
|
||||||
assertThat(config.cycle.size).isLessThan(QuickSwitchConfig.MIN_CYCLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `one enabled view is below the cycle minimum, so the pill hides`() {
|
|
||||||
// A single target is not a switch — it hides rather than becoming a
|
|
||||||
// jump-to-one-view button, which would be dead once you were there.
|
|
||||||
val config = QuickSwitchConfig(order = IMPLEMENTED_VIEWS, enabled = setOf(CalendarView.Day))
|
|
||||||
assertThat(config.cycle).containsExactly(CalendarView.Day)
|
|
||||||
assertThat(config.cycle.size).isLessThan(QuickSwitchConfig.MIN_CYCLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `two enabled views are enough to show the pill`() {
|
|
||||||
val config = QuickSwitchConfig(
|
|
||||||
order = IMPLEMENTED_VIEWS,
|
|
||||||
enabled = setOf(CalendarView.Day, CalendarView.Month),
|
|
||||||
)
|
|
||||||
assertThat(config.cycle.size).isAtLeast(QuickSwitchConfig.MIN_CYCLE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user