Compare commits
11 Commits
013efef29e
...
v2.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
| faf2f27eda | |||
| 3d8e3ca69e | |||
| 3b5c0be765 | |||
| 871cc34cab | |||
| 5b99da32b0 | |||
| b8a7191fbe | |||
| 6b0eb48056 | |||
| 572a4734ea | |||
| 7f8a9069c0 | |||
| 22c4eded96 | |||
| 9839f9cd38 |
33
CHANGELOG.md
33
CHANGELOG.md
@@ -5,15 +5,46 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
## [2.8.0] — 2026-06-23
|
||||
|
||||
### Added
|
||||
- Find events fast. A search button in the top bar of every calendar view opens
|
||||
a search box — type a couple of letters and matching events (by title,
|
||||
location or description) appear, soonest first with past events below. Tap a
|
||||
result to open it. Search covers your whole calendar, not just what's on
|
||||
screen, and skips calendars you've hidden. A recurring event shows its next
|
||||
occurrence rather than the date the series first started.
|
||||
- A current-time line in the day and week views. A thin coloured line marks the
|
||||
present moment across today's column, so you can see at a glance where you are
|
||||
in the day. It updates every minute and only appears when today is in view.
|
||||
- Add and remove event guests. The create/edit form now has a Guests section:
|
||||
add people by email or pick them from your contacts, mark each as required or
|
||||
optional, and remove them. Calendula never sends invitations itself (it has no
|
||||
internet access) — it only records the guests; if the event lives on a synced
|
||||
account, that account may email them when it syncs, and on a local calendar no
|
||||
one is notified. The form tells you which applies. Picking a guest from
|
||||
contacts needs no contacts permission.
|
||||
- Pick a location from your contacts. A contacts button beside the location
|
||||
field drops a contact's address straight into an event — handy for a meeting
|
||||
at someone's home or office. Like the guest picker, it needs no contacts
|
||||
permission.
|
||||
- A "New event" Quick Settings tile. Add it to your quick settings to jump
|
||||
straight into the new-event form from anywhere. Settings → New event has a
|
||||
one-tap button to add the tile (Android 13+); on older versions you can add it
|
||||
from the quick-settings editor.
|
||||
- Snooze and dismiss buttons on reminder notifications. Dismiss clears the
|
||||
reminder; snooze hides it and brings it back after a delay you pick in
|
||||
Settings → Notifications (5 to 60 minutes, default 10). Android's calendar
|
||||
system won't re-post a reminder on its own, so Calendula schedules an exact
|
||||
alarm to bring a snoozed one back on time.
|
||||
|
||||
### Changed
|
||||
- Event details now show each guest's email beneath their name, instead of only
|
||||
when no name is available.
|
||||
- Crash and problem reports now open on the project's public Codeberg tracker,
|
||||
where anyone can register and file an issue. Nothing is sent automatically —
|
||||
you still review the report and submit it yourself in the browser.
|
||||
|
||||
## [2.7.5] — 2026-06-21
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -28,8 +28,8 @@ android {
|
||||
// which builds this version and then creates the matching vX.Y.Z tag +
|
||||
// release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 +
|
||||
// PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md.
|
||||
versionCode = 20705
|
||||
versionName = "2.7.5"
|
||||
versionCode = 20800
|
||||
versionName = "2.8.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -88,6 +88,20 @@
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<!-- Quick Settings tile: a one-tap "New event" shortcut in the QS panel.
|
||||
Exported with BIND_QUICK_SETTINGS_TILE so only the system QS host can
|
||||
bind it; the action mirrors the launcher "New event" shortcut. -->
|
||||
<service
|
||||
android:name=".qs.NewEventTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_qs_new_event"
|
||||
android:label="@string/qs_tile_new_event_label"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- The provider broadcasts EVENT_REMINDER at reminder time but posts
|
||||
no notification itself — a calendar app must (v1.4, Etar model).
|
||||
Exported: the broadcast arrives from the provider's process. -->
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package de.jeanlucmakiola.calendula.qs
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import de.jeanlucmakiola.calendula.MainActivity
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlin.time.Clock
|
||||
|
||||
/**
|
||||
* Quick Settings tile: tapping it opens the create-event form on today — the
|
||||
* same action as the launcher "New event" shortcut and the agenda widget's "+".
|
||||
* A stateless action tile, so there is no on/off state to keep in sync.
|
||||
*/
|
||||
class NewEventTileService : TileService() {
|
||||
|
||||
// The pre-34 branch intentionally uses the deprecated Intent overload: it is
|
||||
// the only form available below UpsideDownCake, and is reached only there.
|
||||
@Suppress("DEPRECATION", "StartActivityAndCollapseDeprecated")
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
val today = Clock.System.now()
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault()).date
|
||||
val intent = MainActivity.openCreateIntent(this, today)
|
||||
// Launch only once the device is unlocked: creating an event behind the
|
||||
// keyguard makes no sense, and the shade can't start an activity over a
|
||||
// locked screen anyway.
|
||||
unlockAndRun {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
val pending = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
startActivityAndCollapse(pending)
|
||||
} else {
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,16 +144,37 @@ fun CalendarHost(
|
||||
}
|
||||
}
|
||||
|
||||
// Close every overlay that can sit over the calendar, so an externally
|
||||
// requested destination (a widget/shortcut/QS-tile launch) is revealed on
|
||||
// top instead of underneath whatever the user had open.
|
||||
fun dismissCoveringOverlays() {
|
||||
showSettings = false
|
||||
showCalendars = false
|
||||
detailKey = null
|
||||
editKey = null
|
||||
importUri = null
|
||||
importForm = null
|
||||
}
|
||||
|
||||
// A home-screen widget launch asks to open a date (→ day view) or start a
|
||||
// create. Handled once and cleared, mirroring [requestedDetailKey].
|
||||
LaunchedEffect(widgetNavRequest) {
|
||||
when (val req = widgetNavRequest) {
|
||||
is WidgetNavRequest.OpenDate -> {
|
||||
// Reveal the day view: drop any overlay that would cover it, so
|
||||
// an external date-open doesn't land under an open Settings/form.
|
||||
dismissCoveringOverlays()
|
||||
createDateIso = null
|
||||
pendingDayIso = req.dateIso
|
||||
view = CalendarView.Day
|
||||
onWidgetNavConsumed()
|
||||
}
|
||||
is WidgetNavRequest.Create -> {
|
||||
// External "new event" entries (QS tile / launcher shortcut /
|
||||
// widget) must land on top of whatever is open — the form overlay
|
||||
// sits below Settings/calendars in the Box, so without this it
|
||||
// would open hidden underneath them.
|
||||
dismissCoveringOverlays()
|
||||
val iso = req.dateIso ?: Clock.System.now()
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault()).date.toString()
|
||||
heldCreateIso = iso
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package de.jeanlucmakiola.calendula.ui.common
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
private val DotSize = 8.dp
|
||||
private val LineThickness = 2.dp
|
||||
|
||||
/**
|
||||
* Current wall-clock instant that updates once a minute, re-aligning to each
|
||||
* minute boundary (rather than ticking a fixed 60 s) so it never drifts toward
|
||||
* the middle of a minute. Drives the "now" line in the day/week grids.
|
||||
*/
|
||||
@Composable
|
||||
private fun rememberCurrentMinute(): State<Instant> {
|
||||
val instant = remember { mutableStateOf(Clock.System.now()) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
val now = Clock.System.now()
|
||||
instant.value = now
|
||||
delay(60_000L - now.toEpochMilliseconds() % 60_000L)
|
||||
}
|
||||
}
|
||||
return instant
|
||||
}
|
||||
|
||||
/**
|
||||
* A thin "current time" indicator — a leading dot plus a line — drawn across a
|
||||
* day column. Positioned on the same [hourHeight] scale the event blocks use so
|
||||
* it lines up with the grid, and refreshed each minute. Renders nothing unless
|
||||
* the wall clock is on [date]; callers mount it only for the column showing
|
||||
* today, so the per-minute tick runs on a single column.
|
||||
*/
|
||||
@Composable
|
||||
fun NowLine(
|
||||
date: LocalDate,
|
||||
hourHeight: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val now by rememberCurrentMinute()
|
||||
val local = now.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
if (local.date != date) return
|
||||
|
||||
val minutes = local.hour * 60 + local.minute
|
||||
val top = hourHeight * (minutes / 60f)
|
||||
val color = MaterialTheme.colorScheme.primary
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(DotSize)
|
||||
.offset(y = top - DotSize / 2),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(LineThickness)
|
||||
.background(color),
|
||||
)
|
||||
// The dot anchors the line to the gutter edge, mirroring the standard
|
||||
// calendar "now" marker; drawn after the line so it sits on top.
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(DotSize)
|
||||
.background(color, CircleShape),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
||||
import de.jeanlucmakiola.calendula.ui.common.next
|
||||
@@ -506,6 +507,7 @@ private fun Timeline(
|
||||
blocks = state.timed,
|
||||
dark = dark,
|
||||
date = state.date,
|
||||
today = state.today,
|
||||
onEventClick = onEventClick,
|
||||
onCreateAt = onCreateAt,
|
||||
modifier = Modifier
|
||||
@@ -522,6 +524,7 @@ private fun DayColumnCard(
|
||||
blocks: List<TimedBlock>,
|
||||
dark: Boolean,
|
||||
date: LocalDate,
|
||||
today: LocalDate,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
onCreateAt: (LocalDate, Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -566,6 +569,10 @@ private fun DayColumnCard(
|
||||
.padding(horizontal = 1.dp),
|
||||
)
|
||||
}
|
||||
// Current-time line, on top of the events, only on today's column.
|
||||
if (date == today) {
|
||||
NowLine(date = date, hourHeight = HOUR_HEIGHT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,6 +461,21 @@ private fun EventEditContent(
|
||||
}
|
||||
}
|
||||
|
||||
// Pick a postal address from contacts: the same one-shot picker, scoped to
|
||||
// address rows, dropping the chosen contact's formatted address into the
|
||||
// location field. Same no-permission guarantee as the guest picker above.
|
||||
val pickContactAddress = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
readContactAddress(context, uri)?.let { address ->
|
||||
viewModel.setLocation(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val selectedCalendar = state.calendars.firstOrNull { it.id == form.calendarId }
|
||||
// The accent ties the form to the detail screen's design language: the
|
||||
// bar under the title takes the target calendar's colour.
|
||||
@@ -592,11 +607,39 @@ private fun EventEditContent(
|
||||
icon = Icons.Default.Place,
|
||||
iconContentDescription = stringResource(R.string.event_detail_location),
|
||||
) {
|
||||
InlineField(
|
||||
value = form.location,
|
||||
onValueChange = viewModel::setLocation,
|
||||
placeholder = stringResource(R.string.event_detail_location),
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
InlineField(
|
||||
value = form.location,
|
||||
onValueChange = viewModel::setLocation,
|
||||
placeholder = stringResource(R.string.event_detail_location),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 4.dp),
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
runCatching {
|
||||
pickContactAddress.launch(
|
||||
Intent(
|
||||
Intent.ACTION_PICK,
|
||||
ContactsContract.CommonDataKinds.StructuredPostal
|
||||
.CONTENT_URI,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(40.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Contacts,
|
||||
contentDescription = stringResource(
|
||||
R.string.event_edit_location_from_contacts,
|
||||
),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(22.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1578,6 +1621,27 @@ private fun readContactEmail(context: Context, uri: Uri): Pair<String, String>?
|
||||
email.takeIf { it.isNotEmpty() }?.let { it to name }
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the formatted postal address from a contact-pick result URI. No
|
||||
* READ_CONTACTS needed: ACTION_PICK grants this URI temporary read access.
|
||||
* The provider's formatted address is multi-line; collapse it to one line so
|
||||
* it sits cleanly in the single-line location field.
|
||||
*/
|
||||
private fun readContactAddress(context: Context, uri: Uri): String? =
|
||||
context.contentResolver.query(
|
||||
uri,
|
||||
arrayOf(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS),
|
||||
null, null, null,
|
||||
)?.use { cursor ->
|
||||
if (!cursor.moveToFirst()) return@use null
|
||||
cursor.getString(0).orEmpty()
|
||||
.lines()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.joinToString(", ")
|
||||
.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
|
||||
private fun fieldLabel(field: EventFormField): Int = when (field) {
|
||||
EventFormField.Location -> R.string.event_detail_location
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package de.jeanlucmakiola.calendula.ui.settings
|
||||
|
||||
import android.Manifest
|
||||
import android.app.StatusBarManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.text.format.DateFormat
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -80,6 +84,7 @@ import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
|
||||
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
||||
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
|
||||
import de.jeanlucmakiola.calendula.domain.EventFormField
|
||||
import de.jeanlucmakiola.calendula.qs.NewEventTileService
|
||||
import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog
|
||||
import de.jeanlucmakiola.calendula.ui.crash.openIssueTracker
|
||||
import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport
|
||||
@@ -514,9 +519,39 @@ private fun EventFormScreen(
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// One-tap add of the "New event" Quick Settings tile. The system prompt
|
||||
// is API 33+; on older versions the tile is still addable manually from
|
||||
// the QS editor, so the row simply doesn't appear there.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Spacer(Modifier.height(24.dp))
|
||||
val context = LocalContext.current
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_qs_tile),
|
||||
summary = stringResource(R.string.settings_qs_tile_hint),
|
||||
position = Position.Alone,
|
||||
onClick = { requestAddQsTile(context) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the system to add the "New event" Quick Settings tile (API 33+). The OS
|
||||
* shows its own confirmation dialog and handles the already-added case, so no
|
||||
* result handling is needed here.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private fun requestAddQsTile(context: Context) {
|
||||
val statusBar = context.getSystemService(StatusBarManager::class.java) ?: return
|
||||
statusBar.requestAddTileService(
|
||||
ComponentName(context, NewEventTileService::class.java),
|
||||
context.getString(R.string.qs_tile_new_event_label),
|
||||
Icon.createWithResource(context, R.drawable.ic_qs_new_event),
|
||||
context.mainExecutor,
|
||||
) { /* result code unused — the system surfaces its own feedback */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reminder-notifications toggle (v1.4), mirroring the onboarding step.
|
||||
* Turning it on re-requests `POST_NOTIFICATIONS` when missing (API 33+) —
|
||||
|
||||
@@ -77,6 +77,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||
@@ -620,6 +621,7 @@ private fun Timeline(
|
||||
blocks = state.timedByDay[day].orEmpty(),
|
||||
dark = dark,
|
||||
date = day,
|
||||
today = state.today,
|
||||
onEventClick = onEventClick,
|
||||
onCreateAt = onCreateAt,
|
||||
modifier = Modifier
|
||||
@@ -638,6 +640,7 @@ private fun DayColumnCard(
|
||||
blocks: List<TimedBlock>,
|
||||
dark: Boolean,
|
||||
date: LocalDate,
|
||||
today: LocalDate,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
onCreateAt: (LocalDate, Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -681,6 +684,10 @@ private fun DayColumnCard(
|
||||
.padding(horizontal = 1.dp),
|
||||
)
|
||||
}
|
||||
// Current-time line, on top of the events, only on today's column.
|
||||
if (date == today) {
|
||||
NowLine(date = date, hourHeight = HOUR_HEIGHT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
app/src/main/res/drawable/ic_qs_new_event.xml
Normal file
18
app/src/main/res/drawable/ic_qs_new_event.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Quick Settings tile icon ("New event"): a flat calendar+plus glyph. QS tile
|
||||
icons are tinted by the system from their alpha, so this is a bare white
|
||||
glyph on a transparent background (no brand circle, unlike the launcher
|
||||
shortcut icon). -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFFFF">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,3h-1V1h-2v2H8V1H6v2H5C3.89,3 3.01,3.9 3.01,5L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM19,19H5V8h14V19z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M11.5,10.5h1v2h2v1h-2v2h-1v-2h-2v-1h2z" />
|
||||
</vector>
|
||||
@@ -81,6 +81,7 @@
|
||||
<string name="event_edit_add_guest">Gast hinzufügen</string>
|
||||
<string name="event_edit_add_guest_hint">Gast per E-Mail hinzufügen…</string>
|
||||
<string name="event_edit_add_guest_from_contacts">Aus Kontakten hinzufügen</string>
|
||||
<string name="event_edit_location_from_contacts">Adresse aus Kontakten wählen</string>
|
||||
<string name="event_edit_remove_guest">Gast entfernen</string>
|
||||
<string name="event_edit_attendee_required">Erforderlich</string>
|
||||
<string name="event_edit_attendee_optional">Optional</string>
|
||||
@@ -255,6 +256,11 @@
|
||||
<string name="shortcut_new_event_short">Neuer Termin</string>
|
||||
<string name="shortcut_new_event_long">Neuen Termin erstellen</string>
|
||||
|
||||
<!-- Schnelleinstellungen-Kachel -->
|
||||
<string name="qs_tile_new_event_label">Neuer Termin</string>
|
||||
<string name="settings_qs_tile">Schnelleinstellungen-Kachel hinzufügen</string>
|
||||
<string name="settings_qs_tile_hint">Eine Kachel „Neuer Termin“ zu den Schnelleinstellungen hinzufügen.</string>
|
||||
|
||||
<!-- Kalender-Filter (M3) -->
|
||||
<string name="filter_title">Kalender</string>
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
<string name="event_edit_add_guest">Add guest</string>
|
||||
<string name="event_edit_add_guest_hint">Add a guest by email…</string>
|
||||
<string name="event_edit_add_guest_from_contacts">Add from contacts</string>
|
||||
<string name="event_edit_location_from_contacts">Pick address from contacts</string>
|
||||
<string name="event_edit_remove_guest">Remove guest</string>
|
||||
<string name="event_edit_attendee_required">Required</string>
|
||||
<string name="event_edit_attendee_optional">Optional</string>
|
||||
@@ -369,6 +370,11 @@
|
||||
<string name="shortcut_new_event_short">New event</string>
|
||||
<string name="shortcut_new_event_long">Create a new event</string>
|
||||
|
||||
<!-- Quick Settings tile -->
|
||||
<string name="qs_tile_new_event_label">New event</string>
|
||||
<string name="settings_qs_tile">Add Quick Settings tile</string>
|
||||
<string name="settings_qs_tile_hint">Add a “New event” tile to the Quick Settings panel.</string>
|
||||
|
||||
<string name="about_source_url" translatable="false">https://gitea.jeanlucmakiola.de/makiolaj/calendula</string>
|
||||
<string name="about_license_url" translatable="false">https://gitea.jeanlucmakiola.de/makiolaj/calendula/src/branch/main/LICENSE</string>
|
||||
|
||||
|
||||
38
fastlane/metadata/android/en-US/changelogs/20800.txt
Normal file
38
fastlane/metadata/android/en-US/changelogs/20800.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
### Added
|
||||
- Find events fast. A search button in the top bar of every calendar view opens
|
||||
a search box — type a couple of letters and matching events (by title,
|
||||
location or description) appear, soonest first with past events below. Tap a
|
||||
result to open it. Search covers your whole calendar, not just what's on
|
||||
screen, and skips calendars you've hidden. A recurring event shows its next
|
||||
occurrence rather than the date the series first started.
|
||||
- A current-time line in the day and week views. A thin coloured line marks the
|
||||
present moment across today's column, so you can see at a glance where you are
|
||||
in the day. It updates every minute and only appears when today is in view.
|
||||
- Add and remove event guests. The create/edit form now has a Guests section:
|
||||
add people by email or pick them from your contacts, mark each as required or
|
||||
optional, and remove them. Calendula never sends invitations itself (it has no
|
||||
internet access) — it only records the guests; if the event lives on a synced
|
||||
account, that account may email them when it syncs, and on a local calendar no
|
||||
one is notified. The form tells you which applies. Picking a guest from
|
||||
contacts needs no contacts permission.
|
||||
- Pick a location from your contacts. A contacts button beside the location
|
||||
field drops a contact's address straight into an event — handy for a meeting
|
||||
at someone's home or office. Like the guest picker, it needs no contacts
|
||||
permission.
|
||||
- A "New event" Quick Settings tile. Add it to your quick settings to jump
|
||||
straight into the new-event form from anywhere. Settings → New event has a
|
||||
one-tap button to add the tile (Android 13+); on older versions you can add it
|
||||
from the quick-settings editor.
|
||||
- Snooze and dismiss buttons on reminder notifications. Dismiss clears the
|
||||
reminder; snooze hides it and brings it back after a delay you pick in
|
||||
Settings → Notifications (5 to 60 minutes, default 10). Android's calendar
|
||||
system won't re-post a reminder on its own, so Calendula schedules an exact
|
||||
alarm to bring a snoozed one back on time.
|
||||
|
||||
### Changed
|
||||
- Event details now show each guest's email beneath their name, instead of only
|
||||
when no name is available.
|
||||
- Crash and problem reports now open on the project's public Codeberg tracker,
|
||||
where anyone can register and file an issue. Nothing is sent automatically —
|
||||
you still review the report and submit it yourself in the browser.
|
||||
|
||||
Reference in New Issue
Block a user