Compare commits

..

4 Commits

Author SHA1 Message Date
Jean-Luc Makiola
9dc83ce15b Extend onboarding into a wizard (#163) (#169)
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/169
2026-08-10 17:46:51 +02:00
Jean-Luc Makiola
342af07e09 Drop deprecated edge-to-edge theme attributes (#123) (#168)
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/168
2026-08-08 20:26:40 +02:00
Jean-Luc Makiola
007e8ab8f6 Add actions to search results (#80) (#167)
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/167
2026-08-08 19:36:22 +02:00
Jean-Luc Makiola
4e38866dfd Drag an event to reschedule it (#135)
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/135
2026-08-02 20:36:09 +02:00
64 changed files with 58 additions and 916 deletions

View File

@@ -7,98 +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
### Added
- **Drag to reschedule can be turned off.** Settings → Views → *Drag to
reschedule*, on by default. Off, no event can be picked up in the month, week
or day view — for anyone who kept moving appointments by accident while
scrolling ([#173]).
- An early Brazilian Portuguese translation. Calendula has started speaking
Brazilian Portuguese, contributed as a community translation through
[Calendula's Weblate](https://weblate.dev.jeanlucmakiola.de/projects/calendula/).
About a third of the app is covered, so the rest still shows in English — you
can pick it under Settings → Language or in Android's per-app language
settings. Thanks to
[bkrz](https://weblate.dev.jeanlucmakiola.de/user/bkrz/) for getting it
started; help finishing it is very welcome.
### Fixed
- **The language picker forgot which language was picked.** Android hands the
applied language back with its script filled in — Simplified Chinese goes in
as `zh-CN` and comes out as `zh-Hans-CN` — so after a restart no entry in
Settings → Language showed as selected, and the row above it named the
language the long way round. Only languages that imply a script were
affected.
## [2.19.0] — 2026-08-10
### Added
- **Drag an event to reschedule it.** Press and hold an event in the month,
week or day view and drop it on another time or day — the only way to move an
appointment used to be opening it and editing its start and end by hand. The
block follows your finger, the timeline scrolls when you carry it to an edge,
and the event's length is preserved: a 90-minute meeting dropped on 09:00 ends
at 10:30. In week and day view the drop snaps to 15-minute steps; in month
view it moves the event to the day you dropped it on and leaves the time
alone. A confirmation names the new slot and offers **Undo** wherever the move
can be taken back in one step. Events you can't edit — read-only calendars, and
birthdays and anniversaries mirrored from your contacts — can't be picked up.
Rescheduling isn't pointer-only: a screen reader gets a **Move…** action on
every event that can be moved ([#68]).
- Dragging a **recurring** event asks what it should apply to, the same *this
event / this and following / all events* choice a save does. Where moving the
whole series would change which weekday it falls on, the rule is rewritten to
match — a weekly Monday event dropped on a Wednesday becomes a weekly
Wednesday event. Where it can't be rewritten safely, the prompt says so and
offers to move just the one occurrence, rather than quietly leaving the
series behind ([#68]).
- **Delete several search results at once.** Long-press a hit in search to enter
selection mode, tap the rest (or **Select all**), and delete the lot in one
action — finding and removing, say, every leftover event with the same title
no longer means opening them one at a time. Recurring hits in the batch ask
once for the whole selection instead of once each, results on read-only
calendars can't be selected, and the deletion is confirmed before it runs
([#80]).
### Changed
- **First launch is now a short wizard** instead of a single permission screen.
It counts its steps, each one can be stepped back out of, and beyond granting
calendar access it offers two things worth deciding once: turning on automatic
backup — chosen inline, folder picker and all, since events that live only on
the phone are lost with it — and choosing which view Calendula opens on, with
a live preview of the month, week, day and agenda layouts rather than four
names in a list. Both are skippable, everything chosen is changeable in
Settings afterwards, and the backup step is offered only where nothing
writable is being synced anywhere. Existing installs are not re-onboarded —
revoking and re-granting the calendar permission later does not restart the
wizard ([#163]).
- The one-time notice about calendars switched off for this device is now a step
in the flow rather than a dialog thrown over the app on first open.
### Fixed
- Removed the deprecated `statusBarColor` / `navigationBarColor` theme
attributes the Play Console flags on Android 15 and up, where they no longer
do anything. The launch backdrop now picks its status-bar icons from the
light/dark resource instead, so they stay legible over it either way
([#123]).
## [2.18.1] — 2026-08-06
### Fixed
- Tapping an empty slot in week or day view creates the event at the hour you
tapped, after the timeline has been zoomed. Pinching the day taller or shorter
— or changing **Hour height** in Settings — left the tap still being measured
against the old spacing, so the new event landed at some other hour entirely
([#148]).
## [2.18.0] — 2026-07-31 ## [2.18.0] — 2026-07-31
### Added ### Added
@@ -1415,10 +1323,3 @@ automatically, with zero telemetry and no internet permission.
[#57]: https://codeberg.org/jlmakiola/calendula/issues/57 [#57]: https://codeberg.org/jlmakiola/calendula/issues/57
[#56]: https://codeberg.org/jlmakiola/calendula/issues/56 [#56]: https://codeberg.org/jlmakiola/calendula/issues/56
[#114]: https://codeberg.org/jlmakiola/calendula/issues/114 [#114]: https://codeberg.org/jlmakiola/calendula/issues/114
[#148]: https://codeberg.org/jlmakiola/calendula/issues/148
[#68]: https://codeberg.org/jlmakiola/calendula/issues/68
[#80]: https://codeberg.org/jlmakiola/calendula/issues/80
[#123]: https://codeberg.org/jlmakiola/calendula/issues/123
[#163]: https://codeberg.org/jlmakiola/calendula/issues/163
[#173]: https://codeberg.org/jlmakiola/calendula/issues/173
[#164]: https://codeberg.org/jlmakiola/calendula/issues/164

View File

@@ -28,8 +28,8 @@ android {
// which builds this version and then creates the matching vX.Y.Z tag + // which builds this version and then creates the matching vX.Y.Z tag +
// release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 + // release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 +
// PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md. // PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md.
versionCode = 21901 versionCode = 21800
versionName = "2.19.1" versionName = "2.18.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -301,19 +301,6 @@ class SettingsPrefs @Inject constructor(
store.edit { it[TODAY_BUTTON_IN_TOOLBAR_KEY] = enabled } store.edit { it[TODAY_BUTTON_IN_TOOLBAR_KEY] = enabled }
} }
/**
* Whether events can be dragged to another slot in the calendar views (#68,
* #173). Defaults to ON. Off means no block registers a drag gesture at all;
* rescheduling then goes through the edit form.
*/
val dragToReschedule: Flow<Boolean> = store.data.map { prefs ->
prefs[DRAG_TO_RESCHEDULE_KEY] ?: true
}
suspend fun setDragToReschedule(enabled: Boolean) {
store.edit { it[DRAG_TO_RESCHEDULE_KEY] = enabled }
}
/** /**
* How far ahead the in-app Agenda screen shows events (v2.11). Defaults to * How far ahead the in-app Agenda screen shows events (v2.11). Defaults to
* [AgendaRange.Month] — a month of upcoming events. Independent of the * [AgendaRange.Month] — a month of upcoming events. Independent of the
@@ -958,7 +945,6 @@ class SettingsPrefs @Inject constructor(
internal val MONTH_VIEW_STYLE_KEY = stringPreferencesKey("month_view_style") internal val MONTH_VIEW_STYLE_KEY = stringPreferencesKey("month_view_style")
internal val TIMELINE_SCALE_KEY = stringPreferencesKey("timeline_scale") internal val TIMELINE_SCALE_KEY = stringPreferencesKey("timeline_scale")
internal val TODAY_BUTTON_IN_TOOLBAR_KEY = booleanPreferencesKey("today_button_in_toolbar") internal val TODAY_BUTTON_IN_TOOLBAR_KEY = booleanPreferencesKey("today_button_in_toolbar")
internal val DRAG_TO_RESCHEDULE_KEY = booleanPreferencesKey("drag_to_reschedule")
internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view") internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view")
internal val QUICK_SWITCH_VIEWS_KEY = stringPreferencesKey("quick_switch_views") internal val QUICK_SWITCH_VIEWS_KEY = stringPreferencesKey("quick_switch_views")
internal val DRAWER_VIEW_ORDER_KEY = stringPreferencesKey("drawer_view_order") internal val DRAWER_VIEW_ORDER_KEY = stringPreferencesKey("drawer_view_order")

View File

@@ -312,13 +312,9 @@ fun CalendarHost(
heldEditKey = key heldEditKey = key
editKey = key editKey = key
} }
// Off by preference (#173) switches off the gesture, not the scope: blocks val moveScope = remember(movableCalendarIds, reschedule) {
// register no drag, while the edit form and its TalkBack action stay put.
val dragToReschedule = viewModel.dragToReschedule.collectAsStateWithLifecycle().value
val moveScope = remember(movableCalendarIds, reschedule, dragToReschedule) {
EventMoveScope( EventMoveScope(
movableCalendarIds = movableCalendarIds, movableCalendarIds = movableCalendarIds,
dragEnabled = dragToReschedule,
move = reschedule::move, move = reschedule::move,
inFlight = reschedule.inFlight, inFlight = reschedule.inFlight,
undoStarted = reschedule.undoStarted, undoStarted = reschedule.undoStarted,

View File

@@ -53,18 +53,4 @@ class CalendarHostViewModel @Inject constructor(
started = SharingStarted.WhileSubscribed(5_000L), started = SharingStarted.WhileSubscribed(5_000L),
initialValue = false, initialValue = false,
) )
/**
* Whether events may be dragged to another slot to reschedule them (#68, #173).
* Off until the stored value arrives, even though the preference itself defaults
* on: the wrong guess costs an enabled user a gesture for the few frames a cold
* start takes to read DataStore, but re-arms the one thing a disabled user
* switched off to be rid of.
*/
val dragToReschedule: StateFlow<Boolean> = prefs.dragToReschedule
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000L),
initialValue = false,
)
} }

View File

@@ -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),
) )

View File

@@ -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
} }
} }

View File

@@ -29,12 +29,6 @@ class EventMoveScope(
* the repository writes whatever it is handed — so this gate is load-bearing. * the repository writes whatever it is handed — so this gate is load-bearing.
*/ */
val movableCalendarIds: Set<Long>, val movableCalendarIds: Set<Long>,
/**
* Whether the *gesture* is on (#173). Off gates the drag alone: [edit] and the
* TalkBack action below stay, since invoking a menu item can't cause the
* accidental drags the setting exists to stop.
*/
val dragEnabled: Boolean,
/** False when the drop was refused outright, so nothing will be written. */ /** False when the drop was refused outright, so nothing will be written. */
val move: (MoveRequest) -> Boolean, val move: (MoveRequest) -> Boolean,
/** /**
@@ -97,17 +91,6 @@ fun ghostAlpha(lifted: Boolean): Float = animateFloatAsState(
label = "ghost-alpha", label = "ghost-alpha",
).value ).value
/**
* Whether [event] can be picked up and dragged — its calendar allows moving and
* the gesture is switched on. Not the same question as [eventMoveAction], which
* survives the setting.
*/
@Composable
fun eventDragAllowed(event: EventInstance): Boolean {
val move = LocalEventMove.current ?: return false
return move.dragEnabled && move.allows(event)
}
/** /**
* A TalkBack action that opens [event] in the edit form, so rescheduling isn't * A TalkBack action that opens [event] in the edit form, so rescheduling isn't
* pointer-only. Null when this event can't be moved. * pointer-only. Null when this event can't be moved.

View File

@@ -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,
)

View File

@@ -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(

View File

@@ -76,18 +76,6 @@ fun TimelineScale.hourHeight(viewportHeight: Dp): Dp = when (this) {
.coerceAtLeast(fillHourHeight(viewportHeight)) .coerceAtLeast(fillHourHeight(viewportHeight))
} }
/**
* The minute of the day a tap [offsetY] px down a timeline column means, snapped
* to the hour it landed in.
*
* [hourPx] must be the hour height the column is drawing at *now* — a tap
* detector that captured it when it was installed maps taps to a pre-pinch grid
* (#148). A zero or negative height (a viewport measured at nothing) has no grid
* to read, so it answers midnight rather than dividing by it.
*/
fun tappedMinuteOfDay(offsetY: Float, hourPx: Float): Int =
if (hourPx <= 0f) 0 else (offsetY / hourPx).toInt().coerceIn(0, 23) * 60
/** /**
* Shortest an event block may render, as a fraction of an hour. Blocks keep a * Shortest an event block may render, as a fraction of an hour. Blocks keep a
* floor so a 15-minute event stays tappable, but the floor scales with the hour * floor so a 15-minute event stays tappable, but the floor scales with the hour

View File

@@ -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,

View File

@@ -46,7 +46,6 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -65,6 +64,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
@@ -91,9 +91,7 @@ import de.jeanlucmakiola.calendula.ui.common.TimelineDragController
import de.jeanlucmakiola.calendula.ui.common.TimelineDragOverlay import de.jeanlucmakiola.calendula.ui.common.TimelineDragOverlay
import de.jeanlucmakiola.calendula.ui.common.TimelineDrop 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.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
@@ -119,7 +117,6 @@ import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH
import de.jeanlucmakiola.calendula.ui.common.HourGutter import de.jeanlucmakiola.calendula.ui.common.HourGutter
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
import de.jeanlucmakiola.calendula.ui.common.tappedMinuteOfDay
import de.jeanlucmakiola.calendula.ui.week.TimedBlock import de.jeanlucmakiola.calendula.ui.week.TimedBlock
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -243,7 +240,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 +408,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 +442,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 +516,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),
) )
} }
@@ -631,12 +623,6 @@ private fun DayColumnCard(
LaunchedEffect(blocks, dragController.settling) { LaunchedEffect(blocks, dragController.settling) {
dragController.noteGrid(date, blocks) dragController.noteGrid(date, blocks)
} }
// The tap detector outlives the composition that installed it — a pinch or a
// Settings change moves the hour height without restarting it — so it reads
// the height and the callback through state handles instead of capturing
// them, or taps land on the scale the column had before the zoom (#148).
val currentHourPx = rememberUpdatedState(hourPx)
val currentOnCreateAt = rememberUpdatedState(onCreateAt)
Card( Card(
// Plain rectangular column — the soft corners come from the outer // Plain rectangular column — the soft corners come from the outer
// rounded scroll viewport, so inner rounding would look odd at the edges. // rounded scroll viewport, so inner rounding would look odd at the edges.
@@ -657,10 +643,8 @@ private fun DayColumnCard(
// only fires on the column background. Snaps to the tapped hour. // only fires on the column background. Snaps to the tapped hour.
.pointerInput(date) { .pointerInput(date) {
detectTapGestures { offset -> detectTapGestures { offset ->
currentOnCreateAt.value( val hour = (offset.y / hourPx).toInt().coerceIn(0, 23)
date, onCreateAt(date, hour * 60)
tappedMinuteOfDay(offset.y, currentHourPx.value),
)
} }
}, },
) { ) {
@@ -743,7 +727,7 @@ private fun EventBlock(
val moveAction = eventMoveAction(block.event) val moveAction = eventMoveAction(block.event)
// A block clipped at the top continues from the previous day: its top edge // A block clipped at the top continues from the previous day: its top edge
// is midnight, not the event's start, so dragging it would invent a time. // is midnight, not the event's start, so dragging it would invent a time.
val draggable = eventDragAllowed(block.event) && block.beginsOn(date, zone) val draggable = moveAction != null && block.beginsOn(date, zone)
val dragModifier = rememberEventDragSource( val dragModifier = rememberEventDragSource(
enabled = draggable, enabled = draggable,
key = block.event.instanceId, key = block.event.instanceId,
@@ -771,13 +755,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),
) )
} }

View File

@@ -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),
) )
@@ -2176,7 +2172,7 @@ private fun monthChipDragModifier(
rowHeightPx: Float, rowHeightPx: Float,
isRtl: Boolean, isRtl: Boolean,
): Modifier = rememberDragSurface( ): Modifier = rememberDragSurface(
enabled = moveScope?.dragEnabled == true && controller != null, enabled = moveScope != null && controller != null,
key = week.days.first(), key = week.days.first(),
onPickUp = { local, pointerInRoot, nodeInRoot, size -> onPickUp = { local, pointerInRoot, nodeInRoot, size ->
val bandTop = band[0]?.takeIf { it.isAttached }?.positionInRoot()?.y val bandTop = band[0]?.takeIf { it.isAttached }?.positionInRoot()?.y
@@ -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),
) )
} }

View File

@@ -302,14 +302,13 @@ private fun ReportProblemRow(position: Position) {
@Composable @Composable
private fun LanguageRow(position: Position) { private fun LanguageRow(position: Position) {
val context = LocalContext.current val context = LocalContext.current
val supported = remember { AppLanguage.supportedTags(context, R.xml.locales_config) }
// Setting a locale recreates the activity; mirror the choice locally so the // Setting a locale recreates the activity; mirror the choice locally so the
// row updates instantly even before the recreation lands. // row updates instantly even before the recreation lands.
var current by remember { mutableStateOf(AppLanguage.currentTag(supported)) } var current by remember { mutableStateOf(AppLanguage.currentTag()) }
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
// null = follow the system; the rest are BCP-47 tags from locales_config.xml. // null = follow the system; the rest are BCP-47 tags from locales_config.xml.
val options = remember(supported) { listOf<String?>(null) + supported } val options = remember { listOf<String?>(null) + AppLanguage.supportedTags(context, R.xml.locales_config) }
GroupedRow( GroupedRow(
title = stringResource(R.string.settings_language), title = stringResource(R.string.settings_language),

View File

@@ -42,8 +42,6 @@ data class SettingsUiState(
val showWeekNumbers: Boolean = false, val showWeekNumbers: Boolean = false,
/** Whether the jump-to-today control sits in the top bar instead of the FAB (#60). */ /** Whether the jump-to-today control sits in the top bar instead of the FAB (#60). */
val todayButtonInToolbar: Boolean = false, val todayButtonInToolbar: Boolean = false,
/** Whether events can be dragged to another slot to reschedule them (#68, #173). */
val dragToReschedule: Boolean = true,
/** How far ahead the in-app Agenda screen shows events (v2.11). */ /** How far ahead the in-app Agenda screen shows events (v2.11). */
val agendaScreenRange: AgendaRange = AgendaRange.Month, val agendaScreenRange: AgendaRange = AgendaRange.Month,
/** How far ahead the agenda widget shows events (v2.11). */ /** How far ahead the agenda widget shows events (v2.11). */

View File

@@ -145,15 +145,9 @@ class SettingsViewModel @Inject constructor(
prefs.showWeekNumbers, prefs.showWeekNumbers,
prefs.agendaShowToday, prefs.agendaShowToday,
prefs.softenCalendarColors, prefs.softenCalendarColors,
combine( prefs.todayButtonInToolbar,
prefs.todayButtonInToolbar, ) { hourLines, weekNumbers, showToday, soften, todayInToolbar ->
prefs.dragToReschedule, DisplayToggles(hourLines, weekNumbers, showToday, soften, todayInToolbar)
::Pair,
),
) { hourLines, weekNumbers, showToday, soften, (todayInToolbar, dragToMove) ->
DisplayToggles(
hourLines, weekNumbers, showToday, soften, todayInToolbar, dragToMove,
)
}, },
) { view, screenRange, widgetRange, timeFormat, toggles -> ) { view, screenRange, widgetRange, timeFormat, toggles ->
ViewSettings( ViewSettings(
@@ -163,7 +157,6 @@ class SettingsViewModel @Inject constructor(
agendaShowToday = toggles.agendaShowToday, agendaShowToday = toggles.agendaShowToday,
softenColors = toggles.softenColors, softenColors = toggles.softenColors,
todayButtonInToolbar = toggles.todayButtonInToolbar, todayButtonInToolbar = toggles.todayButtonInToolbar,
dragToReschedule = toggles.dragToReschedule,
) )
}, },
combine( combine(
@@ -196,7 +189,6 @@ class SettingsViewModel @Inject constructor(
agendaShowToday = views.agendaShowToday, agendaShowToday = views.agendaShowToday,
softenColors = views.softenColors, softenColors = views.softenColors,
todayButtonInToolbar = views.todayButtonInToolbar, todayButtonInToolbar = views.todayButtonInToolbar,
dragToReschedule = views.dragToReschedule,
agendaShowRangeBar = misc.showRangeBar, agendaShowRangeBar = misc.showRangeBar,
autofocusEventTitle = misc.autofocusEventTitle, autofocusEventTitle = misc.autofocusEventTitle,
pastEventDisplay = misc.pastEventDisplay, pastEventDisplay = misc.pastEventDisplay,
@@ -297,7 +289,6 @@ class SettingsViewModel @Inject constructor(
val agendaShowToday: Boolean, val agendaShowToday: Boolean,
val softenColors: Boolean, val softenColors: Boolean,
val todayButtonInToolbar: Boolean, val todayButtonInToolbar: Boolean,
val dragToReschedule: Boolean,
) )
private data class DisplayToggles( private data class DisplayToggles(
@@ -306,7 +297,6 @@ class SettingsViewModel @Inject constructor(
val agendaShowToday: Boolean, val agendaShowToday: Boolean,
val softenColors: Boolean, val softenColors: Boolean,
val todayButtonInToolbar: Boolean, val todayButtonInToolbar: Boolean,
val dragToReschedule: Boolean,
) )
private data class MiscSettings( private data class MiscSettings(
@@ -544,10 +534,6 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch { prefs.setTodayButtonInToolbar(enabled) } viewModelScope.launch { prefs.setTodayButtonInToolbar(enabled) }
} }
fun setDragToReschedule(enabled: Boolean) {
viewModelScope.launch { prefs.setDragToReschedule(enabled) }
}
/** /**
* Switch the launcher label (#44). The card highlights at once, then settles * Switch the launcher label (#44). The card highlights at once, then settles
* on what the component state reports; the writes are binder round-trips and * on what the component state reports; the writes are binder round-trips and
@@ -591,15 +577,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)
)
} }
} }
} }

View File

@@ -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(
@@ -113,7 +114,7 @@ internal fun ViewsScreen(
GroupedRow( GroupedRow(
title = stringResource(R.string.settings_dim_completed), title = stringResource(R.string.settings_dim_completed),
summary = stringResource(R.string.settings_dim_completed_summary), summary = stringResource(R.string.settings_dim_completed_summary),
position = Position.Middle, position = Position.Bottom,
trailing = { trailing = {
Switch( Switch(
checked = state.dimCompletedEvents, checked = state.dimCompletedEvents,
@@ -122,18 +123,6 @@ internal fun ViewsScreen(
}, },
onClick = { viewModel.setDimCompletedEvents(!state.dimCompletedEvents) }, onClick = { viewModel.setDimCompletedEvents(!state.dimCompletedEvents) },
) )
GroupedRow(
title = stringResource(R.string.settings_drag_to_reschedule),
summary = stringResource(R.string.settings_drag_to_reschedule_summary),
position = Position.Bottom,
trailing = {
Switch(
checked = state.dragToReschedule,
onCheckedChange = viewModel::setDragToReschedule,
)
},
onClick = { viewModel.setDragToReschedule(!state.dragToReschedule) },
)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
SectionHeader(stringResource(R.string.settings_month_header)) SectionHeader(stringResource(R.string.settings_month_header))
@@ -220,6 +209,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 +226,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) },
) )
}, },

View File

@@ -52,7 +52,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -72,6 +71,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
@@ -99,9 +99,7 @@ import de.jeanlucmakiola.calendula.ui.common.TimelineDragController
import de.jeanlucmakiola.calendula.ui.common.TimelineDragOverlay import de.jeanlucmakiola.calendula.ui.common.TimelineDragOverlay
import de.jeanlucmakiola.calendula.ui.common.TimelineDrop 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.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
@@ -130,7 +128,6 @@ import de.jeanlucmakiola.calendula.ui.common.GUTTER_CONTENT_START_INSET
import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH
import de.jeanlucmakiola.calendula.ui.common.HourGutter import de.jeanlucmakiola.calendula.ui.common.HourGutter
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
import de.jeanlucmakiola.calendula.ui.common.tappedMinuteOfDay
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
import de.jeanlucmakiola.calendula.ui.common.next import de.jeanlucmakiola.calendula.ui.common.next
import de.jeanlucmakiola.floret.time.isoWeekNumber import de.jeanlucmakiola.floret.time.isoWeekNumber
@@ -266,7 +263,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 +443,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 +477,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 +653,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),
) )
} }
@@ -779,12 +771,6 @@ private fun DayColumnCard(
LaunchedEffect(blocks, dragController.settling) { LaunchedEffect(blocks, dragController.settling) {
dragController.noteGrid(date, blocks) dragController.noteGrid(date, blocks)
} }
// The tap detector outlives the composition that installed it — a pinch or a
// Settings change moves the hour height without restarting it — so it reads
// the height and the callback through state handles instead of capturing
// them, or taps land on the scale the column had before the zoom (#148).
val currentHourPx = rememberUpdatedState(hourPx)
val currentOnCreateAt = rememberUpdatedState(onCreateAt)
Card( Card(
// Plain rectangular columns — the soft corners come from the outer // Plain rectangular columns — the soft corners come from the outer
// rounded scroll viewport, so inner rounding would look odd at the edges. // rounded scroll viewport, so inner rounding would look odd at the edges.
@@ -804,10 +790,8 @@ private fun DayColumnCard(
// blocks are consumed by their own handler first. Snaps to hour. // blocks are consumed by their own handler first. Snaps to hour.
.pointerInput(date) { .pointerInput(date) {
detectTapGestures { offset -> detectTapGestures { offset ->
currentOnCreateAt.value( val hour = (offset.y / hourPx).toInt().coerceIn(0, 23)
date, onCreateAt(date, hour * 60)
tappedMinuteOfDay(offset.y, currentHourPx.value),
)
} }
}, },
) { ) {
@@ -911,7 +895,7 @@ private fun EventBlock(
val moveAction = eventMoveAction(block.event) val moveAction = eventMoveAction(block.event)
// A block clipped at the top continues from the previous day: its top edge // A block clipped at the top continues from the previous day: its top edge
// is midnight, not the event's start, so dragging it would invent a time. // is midnight, not the event's start, so dragging it would invent a time.
val draggable = eventDragAllowed(block.event) && block.beginsOn(date, zone) val draggable = moveAction != null && block.beginsOn(date, zone)
val dragModifier = rememberEventDragSource( val dragModifier = rememberEventDragSource(
enabled = draggable, enabled = draggable,
key = block.event.instanceId, key = block.event.instanceId,
@@ -939,13 +923,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),
) )
} }

View File

@@ -12,14 +12,14 @@
<string name="state_failure_provider">Kalender konnte nicht gelesen werden.</string> <string name="state_failure_provider">Kalender konnte nicht gelesen werden.</string>
<!-- Permission-Flow (F1) --> <!-- Permission-Flow (F1) -->
<string name="permission_rationale_title">Alle Termine, schön im Blick</string> <string name="permission_rationale_title">Alle Termine, schön im Blick</string>
<string name="permission_rationale_body">Calendula braucht Zugriff auf deinen Kalender, um deine Termine zu zeigen und zu verwalten.</string> <string name="permission_rationale_body">Calendula braucht Zugriff auf deinen Kalender, um deine Termine zu zeigen und zu verwalten. Mehr braucht sie nicht um zu funktionieren — und nichts verlässt dein Gerät.</string>
<string name="permission_request_button">Kalender-Zugriff erlauben</string> <string name="permission_request_button">Kalender-Zugriff erlauben</string>
<string name="permission_denied_title">Kalender-Zugriff abgelehnt</string> <string name="permission_denied_title">Kalender-Zugriff abgelehnt</string>
<string name="permission_denied_body">Ohne Kalender-Zugriff kann Calendula keine Termine anzeigen. Du kannst den Zugriff in den System-Einstellungen wieder erlauben.</string> <string name="permission_denied_body">Ohne Kalender-Zugriff kann Calendula keine Termine anzeigen. Du kannst den Zugriff in den System-Einstellungen wieder erlauben.</string>
<string name="permission_open_settings_button">System-Einstellungen öffnen</string> <string name="permission_open_settings_button">System-Einstellungen öffnen</string>
<string name="permission_retry_button">Erneut versuchen</string> <string name="permission_retry_button">Erneut versuchen</string>
<string name="permission_benefit_private_title">Datenschutz by Design</string> <string name="permission_benefit_private_title">Bleibt auf deinem Gerät</string>
<string name="permission_benefit_private_body">Keine Internetberechtigung, keine Telemetrie — nichts verlässt dein Telefon.</string> <string name="permission_benefit_private_body">Deine Kalender werden lokal gelesen und verlassen das Telefon nie.</string>
<string name="permission_benefit_sync_title">Alle Kalender vereint</string> <string name="permission_benefit_sync_title">Alle Kalender vereint</string>
<string name="permission_benefit_sync_body">Google, CalDAV, lokal — alles, was synchronisiert ist, erscheint automatisch.</string> <string name="permission_benefit_sync_body">Google, CalDAV, lokal — alles, was synchronisiert ist, erscheint automatisch.</string>
<string name="permission_benefit_privacy_title">Kein Tracking, niemals</string> <string name="permission_benefit_privacy_title">Kein Tracking, niemals</string>
@@ -310,9 +310,9 @@
<string name="settings_language">App-Sprache</string> <string name="settings_language">App-Sprache</string>
<string name="settings_language_auto">Systemstandard</string> <string name="settings_language_auto">Systemstandard</string>
<!-- Hub category subtitles --> <!-- Hub category subtitles -->
<string name="settings_appearance_subtitle">Theme, Farben, Schriftarten</string> <string name="settings_appearance_subtitle">Design, Standardansicht, Wochenstart</string>
<string name="settings_event_form_subtitle">Standardfelder und Verhalten</string> <string name="settings_event_form_subtitle">Standardfelder für neue Termine</string>
<string name="settings_notifications_subtitle">Erinnerungen und Zustellung</string> <string name="settings_notifications_subtitle">Termin-Erinnerungen</string>
<string name="settings_section_about">Über</string> <string name="settings_section_about">Über</string>
<string name="settings_license">Lizenz</string> <string name="settings_license">Lizenz</string>
<string name="settings_license_value">MIT</string> <string name="settings_license_value">MIT</string>
@@ -424,7 +424,7 @@
<string name="settings_calendar_reminders_title">Erinnerungen pro Kalender</string> <string name="settings_calendar_reminders_title">Erinnerungen pro Kalender</string>
<string name="settings_translate">Hilf beim Übersetzen</string> <string name="settings_translate">Hilf beim Übersetzen</string>
<string name="settings_translate_hint">Füge eine Sprache auf Weblate hinzu oder verbessere sie</string> <string name="settings_translate_hint">Füge eine Sprache auf Weblate hinzu oder verbessere sie</string>
<string name="settings_views_subtitle">Standartansicht, Anordnung, Menüreihenfolge</string> <string name="settings_views_subtitle">Schnellwechsellnopf und Menüreihenfolge</string>
<string name="settings_special_dates_subtitle">Kontakte-Geburtstage und Jubiläen</string> <string name="settings_special_dates_subtitle">Kontakte-Geburtstage und Jubiläen</string>
<string name="settings_section_special_dates">Besondere Termine von Kontakten</string> <string name="settings_section_special_dates">Besondere Termine von Kontakten</string>
<string name="settings_special_dates_enable">Kontakttermine anzeigen</string> <string name="settings_special_dates_enable">Kontakttermine anzeigen</string>
@@ -498,154 +498,4 @@
<string name="event_edit_timezone_none">Keine Zeitzone passt zu “%1$s”</string> <string name="event_edit_timezone_none">Keine Zeitzone passt zu “%1$s”</string>
<string name="event_edit_timezone_local_time">%1$s deiner Zeit</string> <string name="event_edit_timezone_local_time">%1$s deiner Zeit</string>
<string name="import_reminder_prompt_title">Deine Standarterinnerung anwenden?</string> <string name="import_reminder_prompt_title">Deine Standarterinnerung anwenden?</string>
<plurals name="import_reminder_prompt_body_existing">
<item quantity="one">Dieses Event wurde mit %1$d Erinnerung importiert.</item>
<item quantity="other">Dieses Event wurde mit %1$d Erinnerungen importiert.</item>
</plurals>
<string name="settings_widget_size">Agenda-Widget-Größe</string>
<string name="import_reminder_prompt_body_none">Dieser Termin wurde ohne eine Erinnerung importiert.</string>
<string name="import_reminder_prompt_apply">Standart anwenden</string>
<string name="settings_widgets_subtitle">Agenda-Widget, Schnelleinstellungen-Kachel</string>
<string name="import_reminder_prompt_keep">unverändert lassen</string>
<string name="event_access_public_summary">Jeder mit Zugriff sieht jedes Detail</string>
<string name="event_access_private_summary">Andere sehen nur das du beschäftigt bist</string>
<string name="event_edit_recurrence_next">Als nächstes: %1$s</string>
<string name="event_edit_recurrence_incomplete">Gebe eine Zahl zwischen 1 und 999 ein</string>
<string name="timeline_scale_custom">Benutzerdefiniert</string>
<string name="settings_calendar_duration_inherits">Standard (%1$s)</string>
<string name="settings_group_about">Über</string>
<string name="event_move_gone">Dieser Termin existiert nicht mehr</string>
<string name="duration_custom_max">Maximal %1$s</string>
<string name="reminder_day_yesterday">Gestern</string>
<string name="agenda_no_more_today">Keine Events mehr heute</string>
<string name="agenda_span_starts">Beginnt %1$s</string>
<string name="agenda_span_ends">Endet %1$s</string>
<string name="today_jump_action">Zu Heute springen</string>
<string name="event_move_action">Verschieben…</string>
<string name="event_move_recurring_title">Wiederkehrenden Termin verschieben</string>
<string name="event_move_occurrence_only">Die ganze Serie zu verschieben würde ändern, auf welche Tage sie fällt, deshalb kann nur dieser Termin verschoben werden.</string>
<string name="event_move_done">Verschoben auf %1$s</string>
<string name="event_move_undo">Rückgängig</string>
<string name="event_move_undone">Verschieben rückgängig gemacht</string>
<string name="event_move_failed">Termin konnte nicht verschoben werden</string>
<string name="event_move_write_denied">Calendula braucht Schreibzugriff, um Termine zu verschieben</string>
<string name="event_move_blocked_series_end">Ein Termin kann nicht über das Ende seiner Serie hinaus verschoben werden</string>
<string name="event_edit_recurrence_next_none">Diese Regel wiederholt sich nie</string>
<string name="event_access_default_summary">Was dieser Kalender normalerweise macht</string>
<string name="event_access_confidential_summary">Als vertraulich markiert; was das bedeutet, entscheidet das Kalenderkonto</string>
<string name="onboarding_step_counter">Schritt %1$d von %2$d</string>
<string name="onboarding_backup_title">Deine Termine liegen nur hier</string>
<string name="onboarding_backup_body">Nichts auf diesem Telefon wird mit einem Konto synchronisiert geht es verloren, ist auch dein Kalender weg. Calendula kann eine Sicherung für dich schreiben.</string>
<string name="onboarding_backup_benefit_folder_title">Ein Ordner deiner Wahl</string>
<string name="onboarding_backup_benefit_folder_body">Sicherungen sind einfache .ics-Dateien lege sie irgendwohin, wo synchronisiert wird, oder auf eine SD-Karte.</string>
<string name="onboarding_backup_benefit_daily_title">Einmal am Tag, von selbst</string>
<string name="onboarding_backup_benefit_daily_body">Calendula exportiert deine lokalen Kalender im Hintergrund. Wie oft, änderst du in den Einstellungen.</string>
<string name="onboarding_backup_enable_button">Ordner wählen und sichern</string>
<string name="onboarding_view_title">Was soll zuerst geöffnet werden?</string>
<string name="onboarding_month_style_title">Wie soll Monat aussehen?</string>
<string name="onboarding_view_continue_button">Weiter</string>
<string name="onboarding_visibility_button">Alles klar</string>
<string name="onboarding_done_title">Alles bereit</string>
<string name="onboarding_done_body">Deine Kalender sind eingerichtet. Alles, was du gerade gewählt hast, lässt sich später in den Einstellungen ändern.</string>
<string name="onboarding_done_button">Meinen Kalender öffnen</string>
<plurals name="search_selected_count">
<item quantity="one">%d ausgewählt</item>
<item quantity="other">%d ausgewählt</item>
</plurals>
<string name="search_in_descriptions">In Beschreibungen gefunden</string>
<string name="search_selection_close">Auswahl abbrechen</string>
<string name="search_select_all">Alle auswählen</string>
<string name="search_delete_selected">Ausgewählte löschen</string>
<string name="search_delete_partial">%1$d gelöscht, %2$d nicht</string>
<string name="search_delete_denied_partial">%1$d gelöscht, dann wurde der Schreibzugriff entzogen</string>
<string name="settings_theme_hint">Ob die App hell oder dunkel ist. Die Auswahl gilt sofort.</string>
<string name="settings_theme_system_summary">Aktuell %1$s</string>
<string name="settings_soften_colors">Kalenderfarben harmonisieren</string>
<string name="settings_soften_colors_summary">Behält den Farbton jedes Kalenders, gleicht aber seine Helligkeit an, damit jeder Termin lesbar bleibt und die Farben zusammenpassen. Ausschalten, um die Originalfarben aus der Kalenderquelle zu zeigen.</string>
<string name="settings_font_specimen_heading">Donnerstag, 14. Mai</string>
<string name="settings_font_specimen_body">Team-Besprechung um 10:00, danach Mittagessen mit Robin im Café am Marktplatz.</string>
<string name="settings_week_start_auto_summary">Aktuell %1$s</string>
<string name="settings_today_toolbar">Heute-Schaltfläche in der Symbolleiste</string>
<string name="settings_today_toolbar_summary">Zeigt die Schaltfläche für den Sprung zu heute in der Symbolleiste statt als schwebende Schaltfläche</string>
<string name="settings_app_name">App-Name</string>
<string name="settings_app_name_summary">Zeigt Calendula in deinem Launcher als „Kalender“. Nur der Name im Launcher ändert sich; das Symbol kann nach dem Wechsel an einer neuen Stelle liegen.</string>
<string name="settings_time_format_auto_summary">Folgt dem System: %1$s</string>
<string name="settings_timeline_scale">Stundenhöhe</string>
<string name="settings_timeline_scale_hint">Wie viel senkrechten Platz eine Stunde in der Wochen- und Tagesansicht einnimmt. Beide Ansichten teilen sich diese Einstellung. Du kannst die Zeitleiste auch mit zwei Fingern aufziehen, um jede Höhe dazwischen einzustellen.</string>
<string name="timeline_scale_fit_day">Ganzer Tag</string>
<string name="timeline_scale_fit_day_summary">Alle 24 Stunden auf einem Bildschirm, ohne Scrollen</string>
<string name="timeline_scale_compact">Kompakt</string>
<string name="timeline_scale_compact_summary">Mehr Stunden pro Bildschirm, kleinere Blöcke</string>
<string name="timeline_scale_regular">Normal</string>
<string name="timeline_scale_regular_summary">Der Standardabstand</string>
<string name="timeline_scale_comfortable">Komfortabel</string>
<string name="timeline_scale_comfortable_summary">Größere Blöcke, mehr Scrollen</string>
<string name="timeline_scale_custom_summary">Die Höhe, auf die du die Zeitleiste gezogen hast</string>
<string name="settings_past_events_sample_morning">Team-Besprechung</string>
<string name="settings_past_events_sample_midday">Mittagessen mit Robin</string>
<string name="settings_past_events_sample_evening">Chorprobe</string>
<string name="settings_widget_size_hint">Wie groß das Agenda-Widget auf dem Startbildschirm seinen Text zeichnet. Für das Monats-Widget gibt es keine Einstellung sein Raster passt sich immer dem Platz an, den du ihm gibst.</string>
<string name="settings_widget_size_small">Klein</string>
<string name="settings_widget_size_medium">Mittel</string>
<string name="settings_widget_size_large">Groß</string>
<string name="settings_widget_size_extra_large">Sehr groß</string>
<string name="settings_widget_size_summary">Termintext %1$d%%</string>
<string name="settings_agenda_show_today">Heute immer zeigen</string>
<string name="settings_agenda_show_today_hint">Behält Heute oben in der Agenda und ihrem Widget, auch wenn für heute nichts mehr ansteht.</string>
<string name="settings_month_header">Monatsansicht</string>
<string name="settings_month_view_style">Stil der Monatsansicht</string>
<string name="month_style_paged">Seiten</string>
<string name="month_style_paged_summary">Ein Monat füllt den Bildschirm. Wische nach links oder rechts, um den Monat zu wechseln.</string>
<string name="month_style_continuous">Fortlaufende Monate</string>
<string name="month_style_continuous_summary">Jeder Monat steht unter seiner eigenen Überschrift, mit etwas Abstand zum nächsten.</string>
<string name="month_style_dense">Nahtlose Wochen</string>
<string name="month_style_dense_summary">Die Wochen laufen ohne Unterbrechung weiter, jeder Monat geht ohne Lücke direkt in den nächsten über.</string>
<string name="month_style_split">Geteilt</string>
<string name="month_style_split_summary">Ein kompaktes Raster markiert die Tage mit Terminen, und der angetippte Tag wird darunter aufgelistet.</string>
<string name="month_split_no_events">Nichts geplant</string>
<string name="month_split_expand">Ganzen Monat zeigen</string>
<string name="month_split_collapse">Termine des Tages zeigen</string>
<string name="settings_event_duration">Standarddauer</string>
<string name="settings_event_duration_hint">Wie lange ein neuer Termin dauert, bis du seine Endzeit änderst. Ganztägige Termine sind nicht betroffen.</string>
<string name="settings_calendar_durations_title">Dauer pro Kalender</string>
<string name="settings_calendar_durations_hint">Gib einem Kalender seine eigene Standarddauer z. B. 8 Stunden für Arbeitsschichten.</string>
<string name="settings_calendar_duration_use_default">Standarddauer verwenden (%1$s)</string>
<string name="settings_allday_reminder_fires_at">Benachrichtigt um %1$s</string>
<string name="settings_group_look">Aussehen &amp; Verhalten</string>
<string name="settings_group_data">Daten</string>
<string name="settings_group_app">App</string>
<string name="settings_backup_subtitle">Export, Import, automatische Sicherung</string>
<string name="settings_section_widgets">Widgets &amp; Kacheln</string>
<string name="settings_widgets_hint">Einstellungen für die Widgets auf dem Startbildschirm und die Schnelleinstellungen-Kachel. Füge ein Widget hinzu, indem du lange auf den Startbildschirm drückst.</string>
<string name="settings_section_backup">Sicherung &amp; Wiederherstellung</string>
<string name="settings_views_all_header">Alle Ansichten</string>
<string name="settings_week_day_header">Woche &amp; Tag</string>
<string name="settings_default_view_hint">Die Ansicht, mit der Calendula startet.</string>
<string name="settings_week_start_hint">Der Tag, mit dem jede Woche beginnt in allen Ansichten und Widgets.</string>
<string name="settings_time_format_hint">Wie Zeiten in der App geschrieben werden. Automatisch folgt deiner Systemeinstellung.</string>
<string name="settings_past_events_hint">Was die Agenda mit Terminen macht, die bereits vorbei sind.</string>
<string name="settings_dynamic_color_summary">Nimmt die Farben der App von deinem Hintergrundbild.</string>
<string name="settings_about_privacy">Datenschutzerklärung</string>
<string name="calendars_visibility_hint">Schalte einen Kalender aus, um ihn auf diesem Gerät zu verbergen seine Termine verschwinden aus der App und er erinnert dich nicht mehr. Es ist derselbe Schalter, den deine anderen Kalender-Apps verwenden, sie verbergen ihn also ebenfalls. Nichts wird gelöscht, kein anderes Gerät ist betroffen, und du kannst ihn hier jederzeit wieder einschalten.</string>
<string name="calendars_visibility_a11y">„%1$s“ anzeigen</string>
<string name="calendars_visibility_notice_title">Einige Kalender sind ausgeschaltet</string>
<string name="calendars_visibility_notice_message">Calendula zeigt die Kalender, die für dieses Gerät eingeschaltet sind, und einige von deinen sind aus. Schalte sie unter Einstellungen → Kalender ein, um ihre Termine zu sehen.</string>
<string name="calendar_picker_missing_title">Fehlt ein Kalender?</string>
<string name="calendar_picker_missing_summary">Er kann ausgeschaltet, schreibgeschützt oder aus deinen Kontakten gefüllt sein verwalte deine Kalender hier.</string>
<string name="calendars_state_read_only">Schreibgeschützt</string>
<string name="calendars_state_not_synced">Nicht auf dieses Gerät synchronisiert</string>
<string name="calendars_state_managed">Aus deinen Kontakten gefüllt</string>
<string name="calendars_managed_delete_locked">Dieser Kalender wird aus deinen Kontakten gefüllt, Calendula würde ihn bei der nächsten Synchronisierung neu anlegen. Schalte die besonderen Termine unter Einstellungen → Besondere Termine aus, um ihn zu löschen.</string>
<string name="calendars_account_from_source">%1$s (%2$s)</string>
<string name="duration_hours_minutes">%1$s %2$s</string>
<string name="onboarding_backup_skip_button">Nicht jetzt</string>
<string name="onboarding_back">Zurück</string>
<plurals name="search_delete_title">
<item quantity="one">%d Termin löschen?</item>
<item quantity="other">%d Termine löschen?</item>
</plurals>
<plurals name="search_delete_done">
<item quantity="one">%d Termin gelöscht</item>
<item quantity="other">%d Termine gelöscht</item>
</plurals>
</resources> </resources>

View File

@@ -491,5 +491,4 @@
<item quantity="many">Importando %d eventos</item> <item quantity="many">Importando %d eventos</item>
<item quantity="other">Importando %d eventos</item> <item quantity="other">Importando %d eventos</item>
</plurals> </plurals>
<string name="settings_soften_colors">Armoniza los colores del calendario</string>
</resources> </resources>

View File

@@ -1,210 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="backup_interval_units">
<item>Minutes</item>
<item>Hours</item>
<item>Days</item>
<item>Weeks</item>
</string-array>
<string name="state_loading">Carregando…</string>
<string name="app_name">Calendula</string>
<string name="state_failure_permission">O acesso ao calendário é necessário.</string>
<string name="state_failure_no_calendars">Nenhum calendário configurado.</string>
<string name="state_failure_no_calendars_action">Abrir configurações do calendário do sistema</string>
<string name="state_retry">Tentar novamente</string>
<string name="state_failure_permission_action">Conceder acesso</string>
<string name="app_tagline">Um calendário moderno.</string>
<string name="state_failure_unknown">Algo deu errado.</string>
<string name="state_failure_provider">Não foi possível ler o calendário.</string>
<string name="permission_rationale_title">Veja todos os seus eventos, lindamente</string>
<string name="permission_rationale_body">Calendula precisa acessar seu calendário para mostrar e gerenciar eventos. É tudo que ele pede — e nada sai do seu dispositivo.</string>
<string name="permission_request_button">Conceder acesso ao calendário</string>
<string name="permission_denied_title">Acesso ao calendário negado</string>
<string name="permission_denied_body">Calendula não consegue mostrar eventos sem acesso ao calendário. Você pode conceder novamente nas configurações do sistema.</string>
<string name="permission_open_settings_button">Abrir configurações do sistema</string>
<string name="permission_retry_button">Tentar novamente</string>
<string name="permission_benefit_private_title">Fica no seu dispositivo</string>
<string name="permission_benefit_private_body">Seus calendários são lidos localmente e nunca saem do celular.</string>
<string name="permission_benefit_sync_title">Todos os seus calendários, juntos</string>
<string name="permission_benefit_sync_body">Google, CalDAV, local — tudo sincronizado ao dispositivo apenas aparece.</string>
<string name="permission_benefit_privacy_title">Nenhum rastreamento, jamais</string>
<string name="permission_benefit_privacy_body">Zero telemetria, zero dados para análise, sem anúncios.</string>
<string name="permission_privacy_footnote">Fica no seu dispositivo · sem permissão de internet</string>
<string name="month_prev">Mês anterior</string>
<string name="month_next">Próximo mês</string>
<string name="month_today_action">Hoje</string>
<string name="month_more_actions">Mais ações</string>
<string name="month_open_menu">Abrir menu</string>
<string name="month_action_settings">Configurações</string>
<string name="month_a11y_today_prefix">Hoje</string>
<string name="week_today_action">Esta semana</string>
<string name="week_number_label">Sem</string>
<string name="day_today_action">Hoje</string>
<string name="event_detail_back">Voltar</string>
<string name="event_detail_edit">Editar</string>
<string name="event_detail_delete">Excluir</string>
<string name="event_detail_share">Compartilhar</string>
<string name="event_detail_duplicate">Duplicar</string>
<string name="event_share_chooser_title">Compartilhar evento</string>
<string name="event_share_failed">Não foi possível compartilhar este evento.</string>
<string name="event_delete_title">Excluir evento?</string>
<string name="event_delete_body">O evento sairá do calendário e de todos os aparelhos que o sincronizam.</string>
<string name="event_delete_recurring_title">Excluir evento recorrente</string>
<string name="event_delete_option_occurrence">Apenas este evento</string>
<string name="event_delete_option_following">Este evento e todos os próximos</string>
<string name="event_delete_option_series">Todos os eventos nesta série</string>
<string name="event_edit_recurring_title">Editar evento recorrente</string>
<string name="event_delete_failed">Não foi possível excluir o evento</string>
<string name="event_delete_write_denied">Calendula requer permissão de escrita para excluir eventos</string>
<string name="dialog_cancel">Cancelar</string>
<string name="dialog_ok">OK</string>
<string name="event_edit_new_title">Novo evento</string>
<string name="event_edit_close">Fechar</string>
<string name="event_edit_save">Salvar</string>
<string name="event_edit_title_hint">Adicionar título</string>
<string name="event_edit_managed_hint">Gerenciado por “%1$s” — o título, data e frequência são sincronizados dos seus contatos. Lembretes, locais e notas são seus para editar.</string>
<string name="event_edit_starts">Inicia</string>
<string name="event_edit_ends">Termina</string>
<string name="event_edit_error_end_before_start">Termina antes de iniciar</string>
<string name="event_edit_error_no_calendar">Nenhum calendário disponível para escrita</string>
<string name="event_edit_save_failed">Não foi possível salvar o evento</string>
<string name="event_edit_write_denied">Calendula requer permissão de escrita para criar eventos</string>
<string name="event_edit_more_fields">Mais campos</string>
<string name="event_edit_add">Adicionar</string>
<string name="event_edit_add_reminder">Adicionar lembrete</string>
<string name="event_edit_remove_reminder">Remover lembrete</string>
<string name="event_edit_attendees">Convidados</string>
<string name="event_edit_add_guest">Adicionar convidado</string>
<string name="event_edit_add_guest_hint">Adicionar convidado via e-mail…</string>
<string name="event_edit_add_guest_from_contacts">Adicionar dos contatos</string>
<string name="event_edit_location_from_contacts">Escolher endereço dos contatos</string>
<string name="event_edit_remove_guest">Remover convidado</string>
<string name="event_edit_attendee_required">Obrigatório</string>
<string name="event_edit_attendee_optional">Opcional</string>
<string name="event_edit_attendees_note_synced">Calendula não manda convites. Sua conta de calendário poderá enviar e-mails para os convidados quando sincronizar.</string>
<string name="event_edit_attendees_note_local">Armazenado neste dispositivo. Ninguém é notificado.</string>
<string name="event_edit_reminder_custom">Personalizado</string>
<string name="reminder_unit_minutes">minutos</string>
<string name="reminder_unit_hours">horas</string>
<string name="reminder_unit_days">dias</string>
<string name="reminder_unit_weeks">semanas</string>
<string name="event_edit_availability">Disponibilidade</string>
<string name="event_edit_visibility">Visibilidade</string>
<string name="event_edit_timezone_device">Fuso horário do dispositivo</string>
<string name="event_edit_timezone_device_summary">Segue onde você estiver</string>
<string name="event_edit_timezone_search">Buscar fusos</string>
<string name="event_edit_timezone_recent">Recente</string>
<string name="event_edit_timezone_all">Todos os fusos</string>
<string name="event_edit_timezone_none">Nenhum fuso horário encontrado com “%1$s”</string>
<string name="event_edit_timezone_local_time">%1$s seu horário</string>
<string name="event_edit_color">Cor</string>
<string name="event_edit_color_default">Cor do calendário</string>
<string name="event_edit_color_custom">Cor personalizada</string>
<string name="event_edit_color_reset">Redefinir</string>
<string name="event_edit_color_unsupported">Não disponível neste calendário</string>
<string name="event_edit_color_unsupported_hint">Este calendário não publica um esquema de cores. Você pode permitir cores personalizadas nas Configurações.</string>
<string name="event_edit_color_sync_warning">Este calendário pode remover ou sobrescrever a cor na próxima sincronização.</string>
<string name="event_edit_conflict_title">Evento alterado em outro lugar</string>
<string name="event_edit_conflict_body">Enquanto estava editando, este evento foi alterado — por sincronia ou por outro app. O que deve acontecer com suas mudanças?</string>
<string name="event_edit_conflict_overwrite">Salvar mudanças</string>
<string name="event_edit_conflict_overwrite_hint">Apenas campos editados sobrescrevem a mudança externa</string>
<string name="event_edit_conflict_discard">Descartar mudanças</string>
<string name="event_edit_conflict_discard_hint">Evento fica como está agora</string>
<string name="event_edit_gone_title">Evento excluído</string>
<string name="event_edit_gone_body">Este evento foi excluído enquanto isso, por exemplo, em outro aparelho. Suas mudanças não podem mais ser salvas.</string>
<string name="import_reminder_prompt_title">Aplicar seu lembrete padrão?</string>
<string name="import_reminder_prompt_body_none">Este evento foi importado sem lembretes.</string>
<plurals name="import_reminder_prompt_body_existing">
<item quantity="one">Este evento foi importado com %1$d lembrete.</item>
<item quantity="many">Este evento foi importado com %1$d lembretes.</item>
<item quantity="other">Este evento foi importado com %1$d lembretes.</item>
</plurals>
<string name="import_reminder_prompt_apply">Aplicar padrão</string>
<string name="import_reminder_prompt_keep">Manter assim</string>
<string name="event_edit_recurrence_none">Não se repete</string>
<string name="event_edit_recurrence_custom">Personalizado</string>
<string name="event_edit_recurrence_every">A cada</string>
<string name="recurrence_unit_days">dias</string>
<string name="recurrence_unit_weeks">semanas</string>
<string name="recurrence_unit_months">meses</string>
<string name="recurrence_unit_years">anos</string>
<string name="event_edit_recurrence_incomplete">Insira um número de 1 a 999</string>
<string name="event_edit_recurrence_next">Próximo: %1$s</string>
<string name="event_edit_recurrence_next_none">Esta regra nunca se repete</string>
<string name="event_edit_recurrence_ends">Termina</string>
<string name="event_edit_recurrence_end_never">Nunca</string>
<string name="event_edit_recurrence_end_until">Em uma data</string>
<string name="event_edit_recurrence_end_count">Após um número de vezes</string>
<string name="event_edit_recurrence_times">vezes</string>
<string name="event_edit_error_recurrence_ends_before_start">Repetições terminam antes do evento começar</string>
<string name="event_availability_busy">Ocupado</string>
<string name="event_access_default">Padrão</string>
<string name="event_access_public">Público</string>
<string name="event_detail_all_day">O dia todo</string>
<string name="event_detail_calendar">Calendário</string>
<string name="event_detail_calendar_unknown">Calendário desconhecido</string>
<string name="event_detail_location">Local</string>
<string name="event_detail_description">Descrição</string>
<string name="event_detail_attendees">Participantes</string>
<string name="event_detail_recurrence">Recorrência</string>
<string name="event_detail_recurring">Evento recorrente</string>
<string name="recurrence_daily">Todo dia</string>
<string name="recurrence_weekly">Toda semana</string>
<string name="recurrence_monthly">Todo mês</string>
<string name="recurrence_yearly">Todo ano</string>
<string name="recurrence_every_n_days">A cada %1$d dias</string>
<string name="recurrence_every_n_weeks">A cada %1$d semanas</string>
<string name="recurrence_every_n_months">A cada %1$d meses</string>
<string name="recurrence_every_n_years">A cada %1$d anos</string>
<string name="recurrence_on_days">%1$s em %2$s</string>
<string name="recurrence_with_until">%1$s até %2$s</string>
<string name="recurrence_with_count">%1$s, %2$d vezes</string>
<string name="event_detail_not_found">Este evento não existe mais.</string>
<string name="event_attendee_accepted">Aceitou</string>
<string name="event_attendee_declined">Rejeitou</string>
<string name="event_attendee_tentative">Não decidiu</string>
<string name="event_attendee_needs_action">Sem resposta</string>
<string name="event_attendee_unknown"></string>
<string name="event_detail_reminders">Lembretes</string>
<string name="event_detail_timezone">Fuso horário</string>
<string name="event_status_cancelled">Cancelado</string>
<string name="event_availability_free">Disponível</string>
<string name="event_access_private">Privado</string>
<string name="event_access_confidential">Confidencial</string>
<string name="event_access_default_summary">O que este calendário normalmente faz</string>
<string name="event_access_public_summary">Todos com acesso veem tudo</string>
<string name="event_access_private_summary">Outros veem apenas que está ocupado</string>
<string name="event_access_confidential_summary">Marcado como confidencial; o que significa depende da conta</string>
<string name="event_attendee_organizer">Organizador</string>
<string name="event_attendee_optional">Opcional</string>
<string name="event_attendee_resource">Recurso</string>
<string name="event_detail_self_response">Sua resposta: %1$s</string>
<string name="reminder_at_time">Na hora do evento</string>
<string name="reminder_default">Lembrete padrão</string>
<plurals name="reminder_minutes">
<item quantity="one">%d minuto antes</item>
<item quantity="many">%d minutos antes</item>
<item quantity="other">%d minutos antes</item>
</plurals>
<plurals name="reminder_hours">
<item quantity="one">%d hora antes</item>
<item quantity="many">%d horas antes</item>
<item quantity="other">%d horas antes</item>
</plurals>
<plurals name="reminder_days">
<item quantity="one">%d dia antes</item>
<item quantity="many">%d dias antes</item>
<item quantity="other">%d dias antes</item>
</plurals>
<plurals name="reminder_weeks">
<item quantity="one">%d semana antes</item>
<item quantity="many">%d semanas antes</item>
<item quantity="other">%d semanas antes</item>
</plurals>
<plurals name="duration_minutes">
<item quantity="one">%d minuto</item>
<item quantity="many">%d minutos</item>
<item quantity="other">%d minutos</item>
</plurals>
<string name="settings_special_dates_grant">Conceder acesso</string>
</resources>

View File

@@ -439,8 +439,6 @@
<string name="timeline_scale_custom_summary">The height you pinched the timeline to</string> <string name="timeline_scale_custom_summary">The height you pinched the timeline to</string>
<string name="settings_dim_completed">Dim completed events</string> <string name="settings_dim_completed">Dim completed events</string>
<string name="settings_dim_completed_summary">Fade events that have already ended in month and week view</string> <string name="settings_dim_completed_summary">Fade events that have already ended in month and week view</string>
<string name="settings_drag_to_reschedule">Drag to reschedule</string>
<string name="settings_drag_to_reschedule_summary">Move an event by dragging it to another day or time</string>
<string name="settings_past_events">Past events</string> <string name="settings_past_events">Past events</string>
<string name="settings_past_events_show">Show</string> <string name="settings_past_events_show">Show</string>
<string name="settings_past_events_dim">Dim</string> <string name="settings_past_events_dim">Dim</string>
@@ -496,7 +494,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>

View File

@@ -15,6 +15,5 @@
<locale android:name="fr" /> <locale android:name="fr" />
<locale android:name="it" /> <locale android:name="it" />
<locale android:name="pl" /> <locale android:name="pl" />
<locale android:name="pt-BR" />
<locale android:name="zh-CN" /> <locale android:name="zh-CN" />
</locale-config> </locale-config>

View File

@@ -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()
}
}

View File

@@ -126,24 +126,4 @@ class TimelineScaleTest {
assertThat(parseTimelineScale(stored)).isEqualTo(TimelineScale.Regular) assertThat(parseTimelineScale(stored)).isEqualTo(TimelineScale.Regular)
} }
} }
@Test
fun `a tap creates at the hour it landed in, at whatever scale`() {
// Same point on the same column reads as a different hour once the
// timeline has been pinched — which is the whole of #148: the tap has to
// be measured against the height the column is drawing at now.
assertThat(tappedMinuteOfDay(offsetY = 500f, hourPx = 100f)).isEqualTo(5 * 60)
assertThat(tappedMinuteOfDay(offsetY = 500f, hourPx = 50f)).isEqualTo(10 * 60)
// Within an hour it snaps back to that hour's start.
assertThat(tappedMinuteOfDay(offsetY = 599f, hourPx = 100f)).isEqualTo(5 * 60)
}
@Test
fun `a tap outside the day stays inside it`() {
assertThat(tappedMinuteOfDay(offsetY = -20f, hourPx = 56f)).isEqualTo(0)
assertThat(tappedMinuteOfDay(offsetY = 99_999f, hourPx = 56f)).isEqualTo(23 * 60)
// A viewport measured at nothing has no grid to read — midnight, not a
// division by zero.
assertThat(tappedMinuteOfDay(offsetY = 500f, hourPx = 0f)).isEqualTo(0)
}
} }

View File

@@ -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)
}
} }

View File

@@ -1,2 +0,0 @@
إصلاحات
• عرض الأسبوع واليوم: النقر على مساحة فارغة يُنشئ الحدث في الساعة التي نقرت عليها، حتى بعد تكبير شريط الوقت أو تغيير ارتفاع الساعة.

View File

@@ -1,6 +0,0 @@
جديد
• اسحب الحدث لتغيير وقته أو يومه: اضغط عليه مطولاً في عرض الشهر أو الأسبوع أو اليوم ثم أفلته في المكان الجديد. في الأحداث المتكررة يُسأل عن نطاق النقل.
• احذف عدة نتائج بحث دفعة واحدة: اضغط مطولاً على نتيجة لبدء التحديد، ثم احذفها معاً.
تغييرات
• أصبح التشغيل الأول معالجاً قصيراً. يمكنه أيضاً تفعيل النسخ الاحتياطي التلقائي واختيار العرض الافتتاحي، وكلاهما مع معاينة.

View File

@@ -1,3 +0,0 @@
جديد
• صار بالإمكان إيقاف السحب لتغيير الموعد: الإعدادات ← العروض ← «السحب لتغيير الموعد». يبقى مفعّلاً افتراضياً؛ وعند إيقافه لا يمكن سحب الأحداث في عرض الشهر أو الأسبوع أو اليوم.
• صار Calendula يتحدّث البرتغالية البرازيلية بفضل ترجمة من المجتمع. اخترها من الإعدادات ← اللغة.

View File

@@ -1,2 +0,0 @@
Behoben
• Wochen- und Tagesansicht: Ein Tippen auf einen freien Bereich legt den Termin jetzt zur angetippten Uhrzeit an auch nachdem die Zeitleiste gezoomt oder die Stundenhöhe geändert wurde.

View File

@@ -1,6 +0,0 @@
Neu
• Termine per Drag & Drop verschieben: in der Monats-, Wochen- oder Tagesansicht lange gedrückt halten und zu einer anderen Zeit oder einem anderen Tag ablegen. Bei Serienterminen wird nachgefragt, wofür sie gilt.
• Mehrere Suchergebnisse auf einmal löschen: langer Druck auf einen Treffer startet die Auswahl.
Geändert
• Der erste Start ist jetzt ein kurzer Assistent. Er kann auch die automatische Sicherung einrichten und dich die Startansicht wählen lassen beides mit Vorschau.

View File

@@ -1,3 +0,0 @@
Neu
• Verschieben per Drag & Drop lässt sich abschalten: Einstellungen → Ansichten → „Per Ziehen verschieben“. Standardmäßig bleibt es an; abgeschaltet lassen sich Termine in der Monats-, Wochen- und Tagesansicht nicht mehr aufnehmen.
• Calendula spricht jetzt brasilianisches Portugiesisch dank einer Community-Übersetzung. Auswählbar unter Einstellungen → Sprache.

View File

@@ -1,2 +0,0 @@
Fixed
• Week and day view: tapping an empty slot now creates the event at the hour you tapped, even after zooming the timeline or changing the hour height.

View File

@@ -1,6 +0,0 @@
New
• Drag an event to reschedule it — press and hold it in month, week or day view and drop it on another time or day. Recurring events ask what the move applies to.
• Delete several search results at once: long-press a hit to start selecting, then delete them in one action.
Changed
• First launch is now a short wizard. It can also set up automatic backup and let you pick the view Calendula opens on, both with previews.

View File

@@ -1,3 +0,0 @@
Added
• Drag to reschedule can now be turned off: Settings → Views → “Drag to reschedule”. It stays on by default; switched off, events can no longer be picked up in the month, week or day view.
• Calendula now speaks Brazilian Portuguese, thanks to a community translation. Pick it under Settings → Language.

View File

@@ -1,2 +0,0 @@
Fixed
• Week and day view: tapping an empty slot now creates the event at the hour you tapped, even after zooming the timeline or changing the hour height.

View File

@@ -1,6 +0,0 @@
New
• Drag an event to reschedule it — press and hold it in month, week or day view and drop it on another time or day. Recurring events ask what the move applies to.
• Delete several search results at once: long-press a hit to start selecting, then delete them in one action.
Changed
• First launch is now a short wizard. It can also set up automatic backup and let you pick the view Calendula opens on, both with previews.

View File

@@ -1,3 +0,0 @@
Added
• Drag to reschedule can now be turned off: Settings → Views → “Drag to reschedule”. It stays on by default; switched off, events can no longer be picked up in the month, week or day view.
• Calendula now speaks Brazilian Portuguese, thanks to a community translation. Pick it under Settings → Language.

View File

@@ -1,2 +0,0 @@
Corregido
• Vista de semana y día: al tocar un hueco libre, el evento se crea a la hora que has tocado, incluso después de ampliar la línea de tiempo o cambiar la altura de la hora.

View File

@@ -1,6 +0,0 @@
Novedades
• Arrastra un evento para cambiarlo de hora o de día: mantenlo pulsado en la vista de mes, semana o día y suéltalo donde quieras. En los eventos periódicos se pregunta a qué afecta el cambio.
• Elimina varios resultados de búsqueda a la vez: mantén pulsado un resultado para empezar a seleccionar.
Cambios
• El primer inicio es ahora un breve asistente. También puede activar la copia de seguridad automática y dejarte elegir la vista inicial, ambas con vista previa.

View File

@@ -1,3 +0,0 @@
Novedades
• Ahora se puede desactivar el mover eventos arrastrando: Ajustes → Vistas → «Arrastrar para reprogramar». Sigue activado por defecto; desactivado, los eventos ya no se pueden arrastrar en las vistas de mes, semana y día.
• Calendula ya habla portugués de Brasil, gracias a una traducción de la comunidad. Elígelo en Ajustes → Idioma.

View File

@@ -1,2 +0,0 @@
Corrigé
• Vues Semaine et Jour : appuyer sur un créneau vide crée désormais l'événement à l'heure touchée, même après avoir zoomé sur la grille ou modifié la hauteur d'une heure.

View File

@@ -1,6 +0,0 @@
Nouveautés
• Déplacez un événement par glisser-déposer : appuyez longuement dessus en vue mois, semaine ou jour, puis déposez-le à une autre heure ou un autre jour. Pour un événement récurrent, la portée du déplacement est demandée.
• Supprimez plusieurs résultats de recherche d'un coup : un appui long lance la sélection.
Modifié
• Le premier lancement est désormais un court assistant. Il peut aussi activer la sauvegarde automatique et vous laisser choisir la vue d'ouverture, avec aperçu.

View File

@@ -1,3 +0,0 @@
Nouveautés
• Le déplacement par glisser-déposer peut désormais être désactivé : Réglages → Vues → « Glisser pour déplacer ». Il reste activé par défaut ; désactivé, les événements ne peuvent plus être saisis en vue mois, semaine ou jour.
• Calendula parle désormais le portugais brésilien, grâce à une traduction de la communauté. À choisir dans Réglages → Langue.

View File

@@ -1,2 +0,0 @@
Corretto
• Vista settimana e giorno: toccando uno spazio libero l'evento viene creato all'ora toccata, anche dopo aver ingrandito la linea temporale o cambiato l'altezza dell'ora.

View File

@@ -1,6 +0,0 @@
Novità
• Trascina un evento per spostarlo a un altro orario o giorno: tienilo premuto nella vista mese, settimana o giorno e rilascialo. Per gli eventi ricorrenti viene chiesto a cosa si applica lo spostamento.
• Elimina più risultati di ricerca in una volta: tieni premuto un risultato per iniziare a selezionare.
Modifiche
• Il primo avvio è ora una breve procedura guidata. Può anche attivare il backup automatico e farti scegliere la vista iniziale, entrambi con anteprima.

View File

@@ -1,3 +0,0 @@
Novità
• Ora lo spostamento con il trascinamento si può disattivare: Impostazioni → Viste → «Trascina per spostare». Resta attivo di default; disattivato, gli eventi non si possono più trascinare nelle viste mese, settimana e giorno.
• Calendula ora parla portoghese brasiliano, grazie a una traduzione della comunità. Si sceglie in Impostazioni → Lingua.

View File

@@ -1,2 +0,0 @@
Poprawki
• Widok tygodnia i dnia: dotknięcie pustego miejsca tworzy teraz wydarzenie o dotkniętej godzinie, także po powiększeniu osi czasu lub zmianie wysokości godziny.

View File

@@ -1,6 +0,0 @@
Nowości
• Przeciągnij wydarzenie, aby zmienić jego godzinę lub dzień: przytrzymaj je w widoku miesiąca, tygodnia lub dnia i upuść w nowym miejscu. Przy wydarzeniach cyklicznych pojawi się pytanie, czego dotyczy zmiana.
• Usuwaj wiele wyników wyszukiwania naraz: przytrzymaj wynik, aby rozpocząć zaznaczanie.
Zmiany
• Pierwsze uruchomienie to teraz krótki kreator. Może też włączyć automatyczną kopię zapasową i pozwolić wybrać widok startowy oba z podglądem.

View File

@@ -1,3 +0,0 @@
Nowości
• Przenoszenie wydarzeń przeciąganiem można teraz wyłączyć: Ustawienia → Widoki → „Przeciągnij, aby przenieść”. Domyślnie pozostaje włączone; po wyłączeniu wydarzeń nie da się już chwycić w widoku miesiąca, tygodnia i dnia.
• Calendula mówi teraz po portugalsku (Brazylia) dzięki tłumaczeniu społeczności. Wybierzesz je w Ustawienia → Język.

View File

@@ -1,6 +0,0 @@
Novidades
• Arraste um evento para outro horário ou dia: mantenha-o pressionado na visualização de mês, semana ou dia e solte-o onde quiser. Em eventos recorrentes, você escolhe a que a alteração se aplica.
• Exclua vários resultados da busca de uma vez: mantenha um resultado pressionado para começar a selecionar.
Alterações
• A primeira execução agora é um breve assistente. Ele também pode ativar o backup automático e deixar você escolher a visualização inicial, ambos com prévia.

View File

@@ -1,3 +0,0 @@
Novidades
• Agora é possível desativar o mover eventos arrastando: Configurações → Visualizações → “Arrastar para reagendar”. Continua ativo por padrão; desativado, os eventos não podem mais ser arrastados nas visualizações de mês, semana e dia.
• O Calendula agora fala português do Brasil, graças a uma tradução da comunidade. Escolha em Configurações → Idioma.

View File

@@ -1,2 +0,0 @@
Corrigido
• Vista de semana e de dia: tocar num espaço livre cria o evento à hora tocada, mesmo depois de ampliar a linha temporal ou alterar a altura da hora.

View File

@@ -1,6 +0,0 @@
Novidades
• Arraste um evento para outra hora ou dia: mantenha-o premido na vista de mês, semana ou dia e largue-o onde quiser. Nos eventos recorrentes, é perguntado a que se aplica a alteração.
• Elimine vários resultados de pesquisa de uma vez: mantenha um resultado premido para começar a selecionar.
Alterações
• O primeiro arranque é agora um breve assistente. Também pode ativar a cópia de segurança automática e deixá-lo escolher a vista inicial, ambas com pré-visualização.

View File

@@ -1,3 +0,0 @@
Novidades
• Já é possível desativar o mover eventos arrastando: Definições → Vistas → «Arrastar para reagendar». Continua ativo por predefinição; desativado, os eventos deixam de poder ser arrastados nas vistas de mês, semana e dia.
• O Calendula fala agora português do Brasil, graças a uma tradução da comunidade. Escolha em Definições → Idioma.

View File

@@ -1,2 +0,0 @@
Исправлено
• Вид недели и дня: нажатие на свободное место создаёт событие на выбранный час — в том числе после масштабирования шкалы времени или изменения высоты часа.

View File

@@ -1,6 +0,0 @@
Новое
• Перетащите событие, чтобы перенести его: удерживайте его в режиме месяца, недели или дня и отпустите на другом времени или дне. Для повторяющихся событий будет предложено выбрать, к чему применить перенос.
• Удаляйте несколько результатов поиска сразу: удержите результат, чтобы начать выделение.
Изменения
• Первый запуск теперь короткий мастер. Он также может включить автоматическое резервное копирование и дать выбрать стартовый вид — и то и другое с предпросмотром.

View File

@@ -1,3 +0,0 @@
Новое
• Перенос событий перетаскиванием теперь можно отключить: Настройки → Виды → «Перетаскивание для переноса». По умолчанию включён; при отключении события больше нельзя подхватить в видах месяца, недели и дня.
• Calendula теперь говорит на бразильском португальском — благодаря переводу сообщества. Выбрать можно в Настройки → Язык.

View File

@@ -1,2 +0,0 @@
修复
• 周视图和日视图:点按空白处会在所点的时间创建事件,即使在缩放时间轴或更改小时高度之后也是如此。

View File

@@ -1,6 +0,0 @@
新增
• 拖动事件即可改期:在月、周或日视图中长按事件,拖到其他时间或日期后放开。重复事件会询问此次移动的适用范围。
• 一次删除多条搜索结果:长按某条结果进入选择模式,然后一并删除。
变更
• 首次启动改为简短的向导。它还可以帮你开启自动备份,并选择打开时显示的视图,两者均有预览。

View File

@@ -1,3 +0,0 @@
新增
• 现在可以关闭拖动改期:设置 → 视图 → “拖动改期”。默认保持开启;关闭后,月、周、日视图中的事件将无法再被拖起。
• Calendula 现已支持巴西葡萄牙语,感谢社区翻译。可在设置 → 语言中选择。

View File

@@ -1,8 +1,8 @@
[versions] [versions]
agp = "9.2.1" agp = "9.2.1"
kotlin = "2.3.21" kotlin = "2.3.21"
ksp = "2.3.10" ksp = "2.3.9"
hilt = "2.60.1" hilt = "2.59.2"
coreKtx = "1.19.0" coreKtx = "1.19.0"
appcompat = "1.7.1" appcompat = "1.7.1"
lifecycleRuntime = "2.10.0" lifecycleRuntime = "2.10.0"
@@ -11,7 +11,7 @@ composeBom = "2026.06.01"
# Material 3 Expressive APIs currently live only in the 1.5 alpha line. # Material 3 Expressive APIs currently live only in the 1.5 alpha line.
# Pin explicitly to override the BOM (which ships stable 1.4.0). # Pin explicitly to override the BOM (which ships stable 1.4.0).
# Re-evaluate when 1.5.0 stable lands. # Re-evaluate when 1.5.0 stable lands.
material3 = "1.5.0-alpha25" material3 = "1.5.0-alpha24"
datastore = "1.2.1" datastore = "1.2.1"
junit = "6.1.2" junit = "6.1.2"
junitPlatform = "6.1.2" junitPlatform = "6.1.2"
@@ -19,15 +19,15 @@ truth = "1.4.5"
androidxJunit = "1.3.0" androidxJunit = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
kotlinxDatetime = "0.7.0" kotlinxDatetime = "0.7.0"
kotlinxCoroutines = "1.11.0" kotlinxCoroutines = "1.10.2"
turbine = "1.2.1" turbine = "1.2.1"
hiltNavigationCompose = "1.4.0" hiltNavigationCompose = "1.3.0"
lifecycleCompose = "2.10.0" lifecycleCompose = "2.10.0"
androidxTestRules = "1.7.0" androidxTestRules = "1.7.0"
# Glance: 1.1.1 is the latest stable (1.2.0 is still rc, 1.3.0 alpha). # Glance: 1.1.1 is the latest stable (1.2.0 is still rc, 1.3.0 alpha).
glance = "1.1.1" glance = "1.1.1"
work = "2.10.1" work = "2.10.1"
documentfile = "1.1.0" documentfile = "1.0.1"
[libraries] [libraries]
# AndroidX core # AndroidX core