Compare commits

..
14 Commits
Author SHA1 Message Date
Jean-Luc Makiolaandmakiolaj ad8472c65f Reminders follow completions made outside the notification (#127, #129) (#130)
Beta — Codeberg pre-release / detect (push) Successful in 6s
Beta — Codeberg pre-release / beta (push) Successful in 13m29s
In the app's own store, nothing re-ran the reminder scheduler when a task was completed in the list, the detail screen or a widget, or when a sync pulled in a completion from another device. The alarm stayed armed until the next app start or the daily maintenance pass. A reminder that was already showing never came down, because only its own "Done" button cleared it.

There was a second cause for repeating tasks. A ticked occurrence is saved as a row with its own id, but `occurrence()` searched the list by the series id. It never found the ticked occurrence, fell back to the next open one and posted the reminder anyway. That's probably the first half of #127.

- `ReminderRefresher` runs one debounced `ReminderScheduler.sync()` per burst of changes to tasks, lists or alarms. Account bookkeeping doesn't trigger it. The refresher and `TaskWidgetUpdater` now share a `StoreChangeDebouncer`.
- Each scheduler pass withdraws posted reminders whose occurrence is closed. It also runs when reminders are switched off. It only looks at reminders from the current storage mode.
  - A missing task counts as deleted only in Agendula's own store. A provider that fails a read returns nothing rather than throwing.
- Reminders now record their occurrence in the notification extras. Reminders posted by the previous version are only withdrawn for tasks that don't repeat.
- `occurrence()` looks up occurrences by their start time, so it finds ticked rows. It falls back to scanning the list for providers whose exception rows the direct lookup misses.
- A fired reminder now stays marked as fired for the 6 h grace period, even after its task is ticked. Ticking a task and then tapping Undo no longer makes the reminder ring again.
- After posting, the receivers check the task once more. That catches a completion that lands between the check and the post.
- These callers no longer run the scheduler themselves, because the refresher now covers them: the edit screen, the Done action, a storage-mode switch, and the two import paths in Settings. `ProviderWriter` asks the refresher for a pass instead of syncing directly.
  - `SyncWorker` still syncs directly. Once the worker returns, nothing keeps the process alive for the debounce.

Closes #127
Closes #129

Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/agendula/pulls/130
2026-11-02 18:54:13 +01:00
makiolaj c2320de60b Prepare 1.3.0-beta.2
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Failing after 10m54s
2026-10-28 17:57:59 +01:00
makiolaj 62ee105f9d App name picker shows launcher previews like Calendula (#104) 2026-10-28 17:57:59 +01:00
makiolaj b1abcecd06 Prepare 1.3.0-beta.1
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Successful in 11m19s
2026-10-28 14:50:31 +01:00
makiolaj ba48a9821c Fix review findings on server trust (#68)
Beta — Codeberg pre-release / detect (push) Successful in 6s
Beta — Codeberg pre-release / beta (push) Skipped
Renewed pins can be re-trusted via sign in again, discovery keeps walking
past an untrusted SRV target, trust is bound to the address it was given
for, reauth replaces old pins, and the trust save is covered by rollback.
2026-10-28 14:49:49 +01:00
Jean-Luc Makiola fd8e4c9450 Self-signed certificates and plain http for self-hosted servers (#68) (#116)
Beta — Codeberg pre-release / detect (push) Successful in 6s
Beta — Codeberg pre-release / beta (push) Skipped
2026-10-28 14:32:17 +01:00
Jean-Luc Makiola 3452fd631b Name an account (#105) (#115)
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Skipped
2026-10-28 14:31:28 +01:00
Jean-Luc Makiola b8264e336f Pull to sync (#73) (#114) 2026-10-28 14:31:19 +01:00
Jean-Luc Makiola 2c450d300f Return home after adding or opening a task from a widget (#93) (#113)
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Skipped
2026-10-28 14:30:47 +01:00
Jean-Luc Makiola df1fb4e46b Collapsible task list sections (#91) (#112)
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Canceled after 0s
2026-10-28 14:30:36 +01:00
Jean-Luc Makiola 63c7aa64e2 Ignore task swipes that start at the screen edge (#90) (#111)
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Skipped
2026-10-28 14:30:27 +01:00
Jean-Luc Makiola c26ff5909b Switchable launcher name: Agendula or Tasks (#104) (#110)
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Skipped
2026-10-28 14:30:12 +01:00
Jean-Luc Makiola c1219a434d Pure black theme for AMOLED screens (#99) (#109)
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Skipped
2026-10-28 14:29:25 +01:00
Jean-Luc Makiola 5dc1d49295 Show the task description in reminder notifications (#94) (#108)
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Skipped
2026-10-28 14:28:45 +01:00
52 changed files with 79 additions and 963 deletions

No files matched your search

-11
View File
@@ -7,8 +7,6 @@ All notable changes to this project are documented here. The format follows
## [Unreleased]
## [1.3.0] - 2026-11-02
### Added
- Self-hosted servers with a self-signed certificate can be added: Agendula
shows the certificate's fingerprint and asks whether to trust it. A server
@@ -27,19 +25,10 @@ All notable changes to this project are documented here. The format follows
- Adding or opening a task from a widget returns you to the home screen
afterwards (#93).
- A swipe that starts at the screen edge no longer swipes the task (#90).
- Today also lists tasks that start today, and keeps the ones completed today
in a Completed section (#123).
### Fixed
- A task created from Today, or from a Today widget, is due today, so it shows
up there (#122).
- Completing a task from the list, a widget or another device now cancels its
upcoming reminder and clears one already showing (#127, #129).
- The splash screen follows the app's theme, including Black and dynamic
colour, instead of flashing white in dark mode (#119).
- A task saved without its default start or due reminder because it had
already rung (a start of "now", say) gets it once that date is moved later,
if the task has no reminder from that date (#121).
## [1.2.0] - 2026-10-26
+2 -2
View File
@@ -33,8 +33,8 @@ android {
// (1.0.x: 1.0.0 -> 10000; from 1.1.0: 1.1.0-beta.1 -> 1010001,
// 1.1.0 -> 1010099), and CI fails if the committed one doesn't match.
// See docs/RELEASING.md.
versionCode = 1030099
versionName = "1.3.0"
versionCode = 1030002
versionName = "1.3.0-beta.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -16,7 +16,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
import de.jeanlucmakiola.agendula.data.prefs.is24Hour
import de.jeanlucmakiola.agendula.ui.common.LocalFirstDayOfWeek
import de.jeanlucmakiola.agendula.ui.common.LocalUse24HourFormat
@@ -34,12 +33,9 @@ import de.jeanlucmakiola.agendula.ui.crash.CrashReportActivity
import de.jeanlucmakiola.agendula.ui.navigation.NavRequest
import de.jeanlucmakiola.agendula.ui.settings.SettingsViewModel
import de.jeanlucmakiola.agendula.ui.theme.AgendulaTheme
import de.jeanlucmakiola.agendula.ui.theme.SystemTheme
import de.jeanlucmakiola.floret.crash.CrashReportDialog
import de.jeanlucmakiola.floret.crash.CrashReporter
import de.jeanlucmakiola.floret.crash.submitCrashReport
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -53,8 +49,6 @@ class MainActivity : ComponentActivity() {
@Inject lateinit var launchHooks: Set<@JvmSuppressWildcards LaunchHook>
@Inject lateinit var settingsPrefs: SettingsPrefs
// A captured crash report awaiting the user's decision, surfaced as a dialog
// over the app on the next launch (the single-crash path). A startup
// crash-loop is handled out of band, before setContent — see below.
@@ -78,13 +72,6 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
AppShortcuts.publish(this)
// The stored setting, never the view model's placeholder: a night mode
// that flips back and forth would recreate the activity each time.
lifecycleScope.launch {
settingsPrefs.settings.map { it.themeMode to it.dynamicColor }.distinctUntilChanged()
.collect { (mode, dynamicColor) -> SystemTheme.apply(this@MainActivity, mode, dynamicColor) }
}
// Only on a fresh launch: after a rotation or process restore the intent
// is the same old one and the back stack already holds its destination.
val fromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0
@@ -168,7 +155,6 @@ class MainActivity : ComponentActivity() {
private const val EXTRA_LIST_ID = "de.jeanlucmakiola.agendula.extra.LIST_ID"
private const val EXTRA_RETURN_HOME = "de.jeanlucmakiola.agendula.extra.RETURN_HOME"
private const val EXTRA_PRESET_LIST_ID = "de.jeanlucmakiola.agendula.extra.PRESET_LIST_ID"
private const val EXTRA_DUE_TODAY = "de.jeanlucmakiola.agendula.extra.DUE_TODAY"
private const val SHARED_TITLE_LIMIT = 500
private const val NO_OCCURRENCE = -1L
@@ -197,17 +183,13 @@ class MainActivity : ComponentActivity() {
/**
* The launcher shortcut's, Quick Settings tile's and a widget's "New
* task". [listId] preset the list when it names exactly one, [dueToday]
* the due date for a Today widget; the data URI keeps each widget's
* PendingIntent apart from the others.
* task". [listId] preset the list when it names exactly one; the data
* URI keeps each widget's PendingIntent apart from the others.
*/
fun newTaskIntent(context: Context, listId: Long? = null, dueToday: Boolean = false): Intent {
val path = listOfNotNull(listId?.toString(), "today".takeIf { dueToday }).joinToString("/")
return Intent(ACTION_NEW_TASK, path.ifEmpty { null }?.let { "agendula://newtask/$it".toUri() }, context, MainActivity::class.java)
fun newTaskIntent(context: Context, listId: Long? = null): Intent =
Intent(ACTION_NEW_TASK, listId?.let { "agendula://newtask/$it".toUri() }, context, MainActivity::class.java)
.putExtra(EXTRA_PRESET_LIST_ID, listId ?: -1L)
.putExtra(EXTRA_DUE_TODAY, dueToday)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
fun todayIntent(context: Context): Intent =
Intent(ACTION_TODAY, null, context, MainActivity::class.java)
@@ -241,7 +223,6 @@ class MainActivity : ComponentActivity() {
when (intent.action) {
ACTION_NEW_TASK -> return NavRequest.NewTask(
listId = intent.getLongExtra(EXTRA_PRESET_LIST_ID, -1L).takeIf { it > 0L },
dueToday = intent.getBooleanExtra(EXTRA_DUE_TODAY, false),
returnHome = true,
)
ACTION_TODAY -> return NavRequest.OpenSmart(SmartList.TODAY)
@@ -5,7 +5,6 @@ import de.jeanlucmakiola.agendula.data.reminders.ReminderPlanner
import de.jeanlucmakiola.agendula.domain.Task
import de.jeanlucmakiola.agendula.domain.TaskForm
import de.jeanlucmakiola.agendula.domain.TaskReminder
import de.jeanlucmakiola.agendula.domain.calendarDate
import java.time.ZoneId
import kotlin.time.Instant
@@ -23,8 +22,7 @@ internal object StoredReminders {
*
* A task getting its first date takes the defaults only when the form
* names no reminder; one that already had a date takes those of the new
* date that no reminder of the form counts from, and those of a date
* whose defaults had all rung when it moved — see [revived]. Otherwise the form is
* date that no reminder of the form counts from. Otherwise the form is
* kept, so an edit that clears every reminder means "use the default
* here" as before.
*
@@ -35,7 +33,7 @@ internal object StoredReminders {
fun of(form: TaskForm, current: Task?, settings: Settings, now: Long, zone: ZoneId): List<TaskReminder> {
val wanted = form.reminders.map { it.lead }.distinct()
if (form.remindersChosen) return wanted
return withDefaults(form, current?.start, current?.due, current?.isAllDay ?: form.isAllDay, settings, now, zone)
return withDefaults(form, current?.start, current?.due, settings, now, zone)
}
/** [of] for a task whose dates were [start] and [due], whatever the form chose. */
@@ -43,7 +41,6 @@ internal object StoredReminders {
form: TaskForm,
start: Instant?,
due: Instant?,
wasAllDay: Boolean,
settings: Settings,
now: Long,
zone: ZoneId,
@@ -51,12 +48,10 @@ internal object StoredReminders {
val wanted = form.reminders.map { it.lead }.distinct()
val had = anchorsOf(start, due)
val gained = anchorsOf(form.start, form.due) - had
val revived = revived(form, start, due, wasAllDay, wanted, settings, now, zone)
if (gained.isEmpty() && revived.isEmpty()) return wanted
if (gained.isEmpty()) return wanted
if (had.isEmpty() || wanted.isEmpty()) return wanted.ifEmpty { defaultsFor(form, settings, now, zone) }
val named = wanted.map { it.fromStart }.toSet()
val fresh = gained + revived
return wanted + defaultsFor(form, settings, now, zone).filter { it.fromStart in fresh && it.fromStart !in named }
return wanted + defaultsFor(form, settings, now, zone).filter { it.fromStart in gained && it.fromStart !in named }
}
/**
@@ -84,41 +79,6 @@ internal object StoredReminders {
return kept.distinct().ifEmpty { defaultsFor(form, settings, now, zone) }
}
/**
* The dates of a one-off task that moved after every default of theirs
* had rung: a task saved then was left without them as past (#121), so
* the new date brings them like a date the task gains. Only on a task
* with reminders of its own and none from that date — no reminders at
* all is the planner's default or turned off, and either holds.
*/
private fun revived(
form: TaskForm,
start: Instant?,
due: Instant?,
wasAllDay: Boolean,
wanted: List<TaskReminder>,
settings: Settings,
now: Long,
zone: ZoneId,
): Set<Boolean> {
if (form.rrule != null || wanted.isEmpty()) return emptySet()
val named = wanted.map { it.fromStart }.toSet()
val defaults = settings.defaultRemindersFor(form.listId, wasAllDay)
fun revived(fromStart: Boolean, was: Instant?, isNow: Instant?): Boolean {
if (was == null || isNow == null || fromStart in named) return false
val moved = if (wasAllDay == form.isAllDay) {
isNow != was
} else {
isNow.calendarDate(form.isAllDay, zone) != was.calendarDate(wasAllDay, zone)
}
val leads = defaults.filter { it.fromStart == fromStart }
return moved && leads.isNotEmpty() && leads.all {
ReminderPlanner.triggerAt(was, wasAllDay, it.minutesBefore, settings.allDayReminderMinuteOfDay, zone) < now
}
}
return setOfNotNull(true.takeIf { revived(true, start, form.start) }, false.takeIf { revived(false, due, form.due) })
}
/** The dates a reminder can count from, as [TaskReminder.fromStart]. */
private fun anchorsOf(start: Any?, due: Any?): Set<Boolean> =
setOfNotNull(true.takeIf { start != null }, false.takeIf { due != null })
@@ -59,7 +59,7 @@ fun dueWindowOf(
todayEnd: Instant,
zone: ZoneId = ZoneId.systemDefault(),
): DueWindow {
val due = task.wallClock(task.due, zone)
val due = task.due?.let { if (task.isAllDay) it.rebasedForAllDay(allDay = false, zone = zone) else it }
return when {
due == null -> DueWindow.NO_DATE
due < todayStart -> DueWindow.OVERDUE
@@ -68,10 +68,6 @@ fun dueWindowOf(
}
}
/** [at], one of this task's dates, as a local instant: an all-day date is read as its calendar date. */
fun Task.wallClock(at: Instant?, zone: ZoneId = ZoneId.systemDefault()): Instant? =
at?.let { if (isAllDay) it.rebasedForAllDay(allDay = false, zone = zone) else it }
/** What a task created from a [SavedFilter] starts with, so it lands in that filter. */
data class NewTaskPreset(
val listIds: Set<Long> = emptySet(),
@@ -1,13 +1,8 @@
package de.jeanlucmakiola.agendula.domain
import java.time.ZoneId
import kotlin.time.Instant
/**
* The built-in "smart" lists, computed from dates rather than membership. All
* but [COMPLETED] hold open tasks, except [TODAY], which also keeps the ones
* completed today (see [TaskFiltering.isToday]).
*/
/** The built-in "smart" lists, computed from due dates rather than membership. */
enum class SmartList { ALL, TODAY, UPCOMING, OVERDUE, NO_DATE, COMPLETED }
/** What a task screen is showing: one real list, a smart list, or a tag. */
@@ -45,20 +40,6 @@ object TaskFiltering {
is TaskFilter.Saved -> saved?.takeIf { it.id == filter.id }?.matches(task, todayStart, todayEnd) ?: false
}
/**
* Whether [task] belongs in Today. An open task does when it is due today, or
* starts today without being overdue; a completed one when it was completed today.
*/
fun isToday(task: Task, todayStart: Instant, todayEnd: Instant, zone: ZoneId = ZoneId.systemDefault()): Boolean {
fun inToday(at: Instant?) = at != null && at >= todayStart && at < todayEnd
if (task.isClosed) return task.isCompleted && inToday(task.completedAt)
return when (dueWindowOf(task, todayStart, todayEnd, zone)) {
DueWindow.TODAY -> true
DueWindow.OVERDUE -> false
DueWindow.UPCOMING, DueWindow.NO_DATE -> inToday(task.wallClock(task.start, zone))
}
}
/** Whether [task] carries every one of [tags]; an empty set matches everything. */
fun hasAllTags(task: Task, tags: Set<String>): Boolean = task.tags.containsAll(tags)
@@ -68,7 +49,7 @@ object TaskFiltering {
SmartList.ALL -> !task.isClosed
SmartList.NO_DATE -> !task.isClosed && dueWindowOf(task, todayStart, todayEnd) == DueWindow.NO_DATE
SmartList.OVERDUE -> !task.isClosed && dueWindowOf(task, todayStart, todayEnd) == DueWindow.OVERDUE
SmartList.TODAY -> isToday(task, todayStart, todayEnd)
SmartList.TODAY -> !task.isClosed && dueWindowOf(task, todayStart, todayEnd) == DueWindow.TODAY
SmartList.UPCOMING -> !task.isClosed && dueWindowOf(task, todayStart, todayEnd) == DueWindow.UPCOMING
}
}
@@ -16,22 +16,19 @@ data class SectionedTasks(val section: TaskSection, val tasks: List<Task>)
*
* [todayStart] is local midnight today and [todayEnd] local midnight tomorrow
* (compute with [DayWindow]). Only non-empty sections are returned, in the
* fixed order Overdue → Today → Upcoming → No date → Completed. In the Today
* list ([todayList]) every open task sits under Today, as it may be there by its
* start date alone.
* fixed order Overdue → Today → Upcoming → No date → Completed.
*/
object TaskSections {
fun of(tasks: List<Task>, todayStart: Instant, todayEnd: Instant, todayList: Boolean = false): List<SectionedTasks> {
val bySection = tasks.groupBy { sectionOf(it, todayStart, todayEnd, todayList) }
fun of(tasks: List<Task>, todayStart: Instant, todayEnd: Instant): List<SectionedTasks> {
val bySection = tasks.groupBy { sectionOf(it, todayStart, todayEnd) }
return TaskSection.entries.mapNotNull { section ->
bySection[section]?.takeIf { it.isNotEmpty() }?.let { SectionedTasks(section, it) }
}
}
fun sectionOf(task: Task, todayStart: Instant, todayEnd: Instant, todayList: Boolean = false): TaskSection {
fun sectionOf(task: Task, todayStart: Instant, todayEnd: Instant): TaskSection {
if (task.isCompleted) return TaskSection.COMPLETED
if (todayList) return TaskSection.TODAY
return when (dueWindowOf(task, todayStart, todayEnd)) {
DueWindow.OVERDUE -> TaskSection.OVERDUE
DueWindow.TODAY -> TaskSection.TODAY
@@ -147,7 +147,6 @@ class TaskEditViewModel @Inject constructor(
private var loadedReminders: List<TaskReminder> = emptyList()
private var loadedStart: Instant? = null
private var loadedDue: Instant? = null
private var loadedAllDay = false
/** Start a fresh task, optionally pre-selecting a list / parent. */
fun bindNew(
@@ -156,7 +155,6 @@ class TaskEditViewModel @Inject constructor(
initialTitle: String? = null,
presetTag: String? = null,
presetFilterId: Long? = null,
presetDueToday: Boolean = false,
) {
if (bound) return
bound = true
@@ -169,7 +167,6 @@ class TaskEditViewModel @Inject constructor(
val lists = runCatching { repository.taskLists().first() }.getOrElse { emptyList() }
val preset = presetFilterId?.let { settingsPrefs.savedFilter(it).first() }
?.newTaskPreset(LocalDate.now())
val presetDue = preset?.dueDate ?: LocalDate.now().takeIf { presetDueToday }
// A read-only or vanished default — or a widget's preset list, if it was
// since deleted or turned read-only — would give the task a list it
// cannot be saved to.
@@ -191,12 +188,12 @@ class TaskEditViewModel @Inject constructor(
// today's start would land after a due of today.
start = when {
!settings.defaultStartToday -> null
presetDue != null -> allDayInstantOf(LocalDate.now())
preset?.dueDate != null -> allDayInstantOf(LocalDate.now())
else -> Clock.System.now()
},
// The same all-day value picking the date in the date field gives.
due = presetDue?.let(::allDayInstantOf),
isAllDay = presetDue != null,
due = preset?.dueDate?.let(::allDayInstantOf),
isAllDay = preset?.dueDate != null,
priority = preset?.priority ?: Priority.NONE,
parentId = parentId,
lists = lists,
@@ -229,7 +226,6 @@ class TaskEditViewModel @Inject constructor(
storedReminders = loadedReminders.toSet()
loadedStart = task.start
loadedDue = task.due
loadedAllDay = task.isAllDay
_state.value = withFields(
TaskEditUiState(
loading = false,
@@ -364,7 +360,6 @@ class TaskEditViewModel @Inject constructor(
toForm().copy(reminders = loadedReminders),
loadedStart,
loadedDue,
loadedAllDay,
settings,
nowMillis(),
ZoneId.systemDefault(),
@@ -172,12 +172,14 @@ class ListsViewModel @Inject constructor(
SmartCount(SmartList.ALL, smartTopLevel.size),
)
// Today ring: the completed rows of the Today list against all of its rows.
// Today ring: completed vs. total tasks *due today*. The numerator is the
// top-level completed tasks whose due date falls in today's window; the
// denominator adds the still-open ones (the Today smart count above).
val openToday = count(SmartList.TODAY)
val completedToday = completedTasks.count {
!it.isSubtask && it.listId !in hidden && TaskFiltering.isToday(it, todayStart, todayEnd)
val completedDueToday = completedTasks.count {
!it.isSubtask && it.listId !in hidden && it.due != null && it.due >= todayStart && it.due < todayEnd
}
val todayTotal = openToday + completedToday
val todayTotal = openToday + completedDueToday
// Upcoming preview: the next handful of open tasks due tomorrow onward,
// already sorted by the repository's default ordering.
@@ -212,7 +214,7 @@ class ListsViewModel @Inject constructor(
smartCounts = smartCounts,
filters = filters,
groups = groups,
todayDone = completedToday,
todayDone = completedDueToday,
todayTotal = todayTotal,
upcoming = upcoming,
allTasks = allTasks,
@@ -1,6 +1,5 @@
package de.jeanlucmakiola.agendula.ui.navigation
import android.annotation.SuppressLint
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.fadeIn
@@ -18,7 +17,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import de.jeanlucmakiola.agendula.domain.SmartList
import de.jeanlucmakiola.agendula.domain.Task
import kotlin.time.Instant
import androidx.navigation.compose.NavHost
@@ -58,8 +56,6 @@ fun AgendulaNavHost(
val nav = rememberNavController()
// The entry a widget, tile, shortcut or share opened; closing it leaves the app (#93).
var returnHomeEntry by rememberSaveable { mutableStateOf<String?>(null) }
// No public API exposes the whole back stack, and visibleEntries drops an entry covered by another.
@SuppressLint("RestrictedApi")
val backStack by nav.currentBackStack.collectAsState()
LaunchedEffect(backStack, returnHomeEntry) {
val id = returnHomeEntry ?: return@LaunchedEffect
@@ -79,9 +75,7 @@ fun AgendulaNavHost(
is NavRequest.OpenAccount ->
nav.navigate(Dest.Settings.buildAccount(navRequest.accountId)) { launchSingleTop = true }
is NavRequest.NewTask ->
nav.navigate(
Dest.TaskEdit.buildNew(presetListId = navRequest.listId, title = navRequest.title, dueToday = navRequest.dueToday),
)
nav.navigate(Dest.TaskEdit.buildNew(presetListId = navRequest.listId, title = navRequest.title))
is NavRequest.OpenSmart -> nav.navigate(Dest.TaskList.build(TaskFilter.Smart(navRequest.list))) {
launchSingleTop = true
}
@@ -166,7 +160,6 @@ fun AgendulaNavHost(
presetListId = (filter as? TaskFilter.OfList)?.listId,
tag = (filter as? TaskFilter.Tagged)?.tag,
filterId = (filter as? TaskFilter.Saved)?.id,
dueToday = filter == TaskFilter.Smart(SmartList.TODAY),
),
)
},
@@ -238,7 +231,6 @@ fun AgendulaNavHost(
val title = args?.getString(Dest.TaskEdit.ARG_TITLE)
val tag = args?.getString(Dest.TaskEdit.ARG_TAG)
val filterId = args?.getLong(Dest.TaskEdit.ARG_FILTER_ID)?.takeIf { it != Dest.NO_ID }
val dueToday = args?.getBoolean(Dest.TaskEdit.ARG_DUE_TODAY) ?: false
val vm: TaskEditViewModel = hiltViewModel()
LaunchedEffect(taskId, presetListId, parentId) {
if (taskId != Dest.NO_ID) {
@@ -250,7 +242,6 @@ fun AgendulaNavHost(
initialTitle = title,
presetTag = tag,
presetFilterId = filterId,
presetDueToday = dueToday,
)
}
}
@@ -119,7 +119,7 @@ object Dest {
/**
* The create / edit form. `taskId` set ([NO_ID] otherwise) ⇒ edit; for a new
* task, `presetListId` / `parentId` optionally seed the form, and `filterId`
* (or `dueToday`, for the Today list) fills it so the task lands in that view.
* fills it so the task lands in that saved filter.
*/
object TaskEdit {
const val ARG_TASK_ID = "taskId"
@@ -129,7 +129,6 @@ object Dest {
const val ARG_TITLE = "title"
const val ARG_TAG = "tag"
const val ARG_FILTER_ID = "filterId"
const val ARG_DUE_TODAY = "dueToday"
const val route =
"taskEdit?$ARG_TASK_ID={$ARG_TASK_ID}" +
"&$ARG_PRESET_LIST_ID={$ARG_PRESET_LIST_ID}" +
@@ -137,8 +136,7 @@ object Dest {
"&$ARG_OCCURRENCE={$ARG_OCCURRENCE}" +
"&$ARG_TITLE={$ARG_TITLE}" +
"&$ARG_TAG={$ARG_TAG}" +
"&$ARG_FILTER_ID={$ARG_FILTER_ID}" +
"&$ARG_DUE_TODAY={$ARG_DUE_TODAY}"
"&$ARG_FILTER_ID={$ARG_FILTER_ID}"
val arguments = listOf(
navArgument(ARG_TASK_ID) { type = NavType.LongType; defaultValue = NO_ID },
@@ -148,7 +146,6 @@ object Dest {
navArgument(ARG_TITLE) { type = NavType.StringType; nullable = true; defaultValue = null },
navArgument(ARG_TAG) { type = NavType.StringType; nullable = true; defaultValue = null },
navArgument(ARG_FILTER_ID) { type = NavType.LongType; defaultValue = NO_ID },
navArgument(ARG_DUE_TODAY) { type = NavType.BoolType; defaultValue = false },
)
fun buildEdit(taskId: Long, occurrenceStart: Long? = null): String =
@@ -161,7 +158,6 @@ object Dest {
title: String? = null,
tag: String? = null,
filterId: Long? = null,
dueToday: Boolean = false,
): String {
val params = buildList {
if (presetListId != null) add("$ARG_PRESET_LIST_ID=$presetListId")
@@ -169,7 +165,6 @@ object Dest {
if (!title.isNullOrBlank()) add("$ARG_TITLE=${android.net.Uri.encode(title)}")
if (!tag.isNullOrBlank()) add("$ARG_TAG=${android.net.Uri.encode(tag)}")
if (filterId != null) add("$ARG_FILTER_ID=$filterId")
if (dueToday) add("$ARG_DUE_TODAY=true")
}
return if (params.isEmpty()) "taskEdit" else "taskEdit?${params.joinToString("&")}"
}
@@ -27,14 +27,12 @@ sealed interface NavRequest {
/**
* A new task, optionally titled with text shared from another app, or
* preset to a list — a widget's own add button, when it shows one list —
* or due today, from a Today widget.
* preset to a list — a widget's own add button, when it shows one list.
* [returnHome] leaves the app once the form is closed (#93).
*/
data class NewTask(
val title: String? = null,
val listId: Long? = null,
val dueToday: Boolean = false,
val returnHome: Boolean = false,
) : NavRequest
@@ -367,7 +367,7 @@ private fun TaskListBody(
viewModel: TaskListViewModel,
onClearCompleted: (() -> Unit)? = null,
) {
val (todayStart, todayEnd) = remember(state.tasks) { DayWindow.today(Clock.System.now(), ZoneId.systemDefault()) }
val (todayStart, todayEnd) = remember { DayWindow.today(Clock.System.now(), ZoneId.systemDefault()) }
// A parent represents its children via a progress chip (counts come from the
// repository, over the full set), so drop children from the flat list — unless
// their parent isn't in this view (can happen in smart lists), where we keep
@@ -376,9 +376,7 @@ private fun TaskListBody(
val present = state.tasks.mapTo(HashSet()) { it.taskId }
state.tasks.filter { (it.parentId ?: 0L) <= 0L || it.parentId !in present }
}
val sections = remember(displayTasks, todayStart) {
TaskSections.of(displayTasks, todayStart, todayEnd, todayList = filter == TaskFilter.Smart(SmartList.TODAY))
}
val sections = remember(displayTasks) { TaskSections.of(displayTasks, todayStart, todayEnd) }
val showHeaders = sections.size > 1
val listId = (filter as? TaskFilter.OfList)?.listId
// In a mixed view (a smart list) the list colour bar + name disambiguate
@@ -1,44 +0,0 @@
package de.jeanlucmakiola.agendula.ui.theme
import android.app.Activity
import android.app.UiModeManager
import android.os.Build
import androidx.annotation.StyleRes
import de.jeanlucmakiola.agendula.R
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
/**
* Hands the app's theme setting to the system, which draws the launch window
* and splash before any of the app runs (#119). The system keeps both across
* launches, so the next start already matches.
*/
object SystemTheme {
/** The light or dark the app's resources follow, as [UiModeManager.setApplicationNightMode] takes it. */
fun nightModeOf(mode: ThemeMode): Int = when (mode) {
ThemeMode.SYSTEM -> UiModeManager.MODE_NIGHT_AUTO
ThemeMode.LIGHT -> UiModeManager.MODE_NIGHT_NO
ThemeMode.DARK, ThemeMode.BLACK -> UiModeManager.MODE_NIGHT_YES
}
/** The splash theme whose background is the one the app opens on. */
@StyleRes
fun splashThemeOf(mode: ThemeMode, dynamicColor: Boolean): Int = when {
mode.isPureBlack -> R.style.Theme_Agendula_Black
dynamicColor -> R.style.Theme_Agendula
else -> R.style.Theme_Agendula_Fallback
}
/**
* Applies [mode] and [dynamicColor]. A night mode the activity does not
* have yet recreates it, once; the same one again changes nothing.
*/
fun apply(activity: Activity, mode: ThemeMode, dynamicColor: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
activity.getSystemService(UiModeManager::class.java)?.setApplicationNightMode(nightModeOf(mode))
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.splashScreen.setSplashScreenTheme(splashThemeOf(mode, dynamicColor))
}
}
}
@@ -194,12 +194,10 @@ private fun TaskWidgetBody(
Spacer(GlanceModifier.height(R.dimen.widget_tasks_header_gap_tight))
WidgetDoneState(context.getString(R.string.widget_empty), context.getString(R.string.widget_empty_hint))
} else {
val todayList = filter == TaskFilter.Smart(SmartList.TODAY)
val allSections = widgetSections(
data.rows,
data.groupByList,
data.listNames,
todayList,
) { kind -> context.getString(dueBucketLabel(kind)) }
// A lone group whose bucket repeats the widget's own smart list
// (the "Today" smart list showing one "Today" bucket) earns no
@@ -250,8 +248,7 @@ private fun TaskWidgetBody(
// (Today / Tomorrow) whenever one is shown — and when
// it isn't, that's only because it would have repeated
// the title, which names the same day just as well.
suppressDayLabel = !data.groupByList &&
(!todayList || section.row.dueKind == DueKind.TODAY),
suppressDayLabel = !data.groupByList,
)
Spacer(GlanceModifier.height(8.dp))
}
@@ -307,9 +304,7 @@ private fun TaskWidgetHeader(title: String, filter: TaskFilter, singleListId: Lo
WidgetHeaderAction(
icon = ImageProvider(GlanceR.drawable.floret_ic_widget_add),
contentDescription = context.getString(R.string.new_task),
action = actionStartActivity(
MainActivity.newTaskIntent(context, singleListId, dueToday = filter == TaskFilter.Smart(SmartList.TODAY)),
),
action = actionStartActivity(MainActivity.newTaskIntent(context, singleListId)),
)
}
}
@@ -100,7 +100,7 @@ private fun TodayWidgetBody(data: WidgetStatsData?, background: WidgetBackground
WidgetHeaderAction(
icon = ImageProvider(GlanceR.drawable.floret_ic_widget_add),
contentDescription = context.getString(R.string.new_task),
action = actionStartActivity(MainActivity.newTaskIntent(context, dueToday = true)),
action = actionStartActivity(MainActivity.newTaskIntent(context)),
)
}
Spacer(GlanceModifier.height(HeaderGap))
@@ -200,14 +200,12 @@ private fun headedRuns(runs: List<WidgetRun>): List<WidgetSection> =
* (already sorted that way by [widgetRows]), otherwise one per [DueKind]
* bucket — "Overdue" / "Today" / "Tomorrow" / "Later" / "No date" — in that
* fixed order, since [rows] isn't due-sorted. Each run becomes one header
* followed by its own standalone, independently coloured task pills. The Today
* list ([todayList]) is one "Today" bucket, as a row may be there by its start.
* followed by its own standalone, independently coloured task pills.
*/
internal fun widgetSections(
rows: List<WidgetTaskRow>,
groupByList: Boolean,
listNames: Map<Long, String>,
todayList: Boolean = false,
dueBucketLabel: (DueKind?) -> String,
): List<WidgetSection> {
if (groupByList) {
@@ -222,7 +220,7 @@ internal fun widgetSections(
}
return headedRuns(runs)
}
val buckets = rows.groupBy { if (todayList) DueKind.TODAY else it.dueKind }
val buckets = rows.groupBy { it.dueKind }
val runs = DueKind.entries.plus(null).mapNotNull { kind ->
buckets[kind]?.takeIf { it.isNotEmpty() }
?.let { WidgetRun("due:${kind?.name.orEmpty()}", dueBucketLabel(kind), it) }
@@ -7,11 +7,9 @@ import de.jeanlucmakiola.agendula.data.prefs.is24Hour
import de.jeanlucmakiola.agendula.domain.SmartList
import de.jeanlucmakiola.agendula.domain.Task
import de.jeanlucmakiola.agendula.domain.TaskFilter
import de.jeanlucmakiola.agendula.domain.TaskFiltering
import de.jeanlucmakiola.agendula.domain.TaskList
import de.jeanlucmakiola.agendula.domain.calendarDate
import de.jeanlucmakiola.agendula.domain.comparator
import de.jeanlucmakiola.floret.time.DayWindow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
@@ -47,15 +45,10 @@ internal sealed interface WidgetStatsData {
private const val MAX_TODAY_ROWS = 20
/** Whether this open task is on the Today widget's plate: in the app's Today, or overdue. */
private fun Task.isDueToday(now: Instant, today: LocalDate, todayStart: Instant, todayEnd: Instant, zone: ZoneId): Boolean =
TaskFiltering.isToday(this, todayStart, todayEnd, zone) ||
due?.let { isOverdue(it, isAllDay, now, today, zone) } == true
/**
* How far through today a set of [open] and [completed] tasks says you are:
* the app's Today tasks done and still open ([TaskFiltering.isToday]) plus the
* overdue ones, the open ones in [comparator] order, and those tasks as rows.
* tasks done today (by [Task.completedAt]'s local date), tasks still open that
* are overdue or due today (in [comparator] order), and those tasks as rows.
*/
internal fun todayProgress(
open: List<Task>,
@@ -65,9 +58,14 @@ internal fun todayProgress(
today: LocalDate,
zone: ZoneId,
): Triple<Int, Int, List<WidgetTaskRow>> {
val (todayStart, todayEnd) = DayWindow.today(now, zone)
val done = completed.count { TaskFiltering.isToday(it, todayStart, todayEnd, zone) }
val remaining = open.filter { it.isDueToday(now, today, todayStart, todayEnd, zone) }.sortedWith(comparator)
val done = completed.count { task ->
val at = task.completedAt ?: return@count false
at.toLocalDate(zone) == today
}
val remaining = open.filter { task ->
val due = task.due ?: return@filter false
isOverdue(due, task.isAllDay, now, today, zone) || dueKind(due, task.isAllDay, today, zone) == DueKind.TODAY
}.sortedWith(comparator)
return Triple(done, remaining.size, remaining.take(MAX_TODAY_ROWS).map { it.toWidgetRow(now, today, zone) })
}
@@ -78,29 +76,29 @@ internal fun upcoming(open: List<Task>, now: Instant, today: LocalDate, zone: Zo
private const val MAX_UPCOMING_ROWS = 200
/**
* Seven [WeekDay]s starting at [today]. Today's bucket holds what the Today
* widget counts — the app's Today tasks plus every overdue one; every other day
* holds the rest by their own due date.
* Seven [WeekDay]s starting at [today]. Today's bucket folds in every overdue
* task regardless of its own due day, since that's where the app's own
* Overdue/Today smart lists put it; every other day matches its own due date.
*/
internal fun weekDays(open: List<Task>, comparator: Comparator<Task>, now: Instant, today: LocalDate, zone: ZoneId): List<WeekDay> {
val (todayStart, todayEnd) = DayWindow.today(now, zone)
val (todays, later) = open.partition { it.isDueToday(now, today, todayStart, todayEnd, zone) }
return (0..6L).map { offset ->
internal fun weekDays(open: List<Task>, comparator: Comparator<Task>, now: Instant, today: LocalDate, zone: ZoneId): List<WeekDay> =
(0..6L).map { offset ->
val date = today.plusDays(offset)
val dayTasks = if (offset == 0L) {
todays
} else {
later.filter { task -> task.due?.calendarDate(task.isAllDay, zone) == date }
val dayTasks = open.filter { task ->
val due = task.due ?: return@filter false
if (offset == 0L) {
isOverdue(due, task.isAllDay, now, today, zone) || due.calendarDate(task.isAllDay, zone) == date
} else {
due.calendarDate(task.isAllDay, zone) == date
}
}
val overdueCount = if (offset == 0L) {
dayTasks.count { task -> task.due?.let { isOverdue(it, task.isAllDay, now, today, zone) } == true }
dayTasks.count { isOverdue(it.due!!, it.isAllDay, now, today, zone) }
} else {
0
}
val first = dayTasks.sortedWith(comparator).firstOrNull()?.toWidgetRow(now, today, zone)
WeekDay(date, dayTasks.size, overdueCount, first)
}
}
/** One tile per visible list: its open, non-subtask count — a subtask whose parent is also open is left out. */
internal fun listTiles(open: List<Task>, lists: List<TaskList>): List<ListTile> {
+11 -113
View File
@@ -55,8 +55,8 @@
<string name="settings_theme">السمة</string>
<string name="settings_theme_light">فاتح</string>
<string name="settings_theme_dark">داكن</string>
<string name="settings_section_reminders">الإشعارات</string>
<string name="settings_list_notifications">الصوت والاهتزاز</string>
<string name="settings_section_reminders">التذكيرات</string>
<string name="settings_list_notifications">الإشعارات</string>
<string name="settings_group_data">البيانات</string>
<string name="settings_group_app">التطبيق</string>
<string name="settings_group_about">حول</string>
@@ -65,7 +65,7 @@
<string name="settings_section_storage">التخزين</string>
<string name="settings_section_accounts">الحسابات</string>
<string name="settings_list_reminder_inherits">الافتراضي (%1$s)</string>
<string name="settings_section_task_form">نموذج مهمة جديدة</string>
<string name="settings_section_task_form">نموذج مهمة</string>
<string name="settings_about_support">دَعم التطوير</string>
<string name="settings_about_version">الإصدار %1$s</string>
<string name="settings_language">لغة التطبيق</string>
@@ -73,7 +73,7 @@
<string name="settings_translate">المساعدة في الترجمة</string>
<string name="settings_theme_system">اتباع النظام</string>
<string name="settings_dynamic_color">اللون الديناميكي</string>
<string name="settings_reminders">تذكيرات المهام</string>
<string name="settings_reminders">التذكيرات المستحقة</string>
<string name="settings_exact_alarms">التوقيت الدقيق</string>
<string name="settings_default_list">القائمة الافتراضية</string>
<string name="settings_time_format">تنسيق الوقت</string>
@@ -90,7 +90,7 @@
<string name="settings_about_author">بواسطة Jean-Luc Makiola</string>
<string name="settings_about_logo_desc">رمز تطبيق Agendula</string>
<string name="settings_report_problem">الإبلاغ عن مشكلة</string>
<string name="settings_default_reminder">التذكير الافتراضي</string>
<string name="settings_default_reminder">متى لتذكير</string>
<string name="settings_group_look">المظهر و السلوك</string>
<string name="settings_bottom_add_bar">شريط الإضافة السريع السفلي</string>
<string name="settings_time_format_12h">١٢-ساعة (٢:٠٠ م)</string>
@@ -168,7 +168,7 @@
<string name="reminder_unit_days">أيام</string>
<string name="reminder_unit_weeks">أسابيع</string>
<string name="settings_list_reminders_title">التذكيرات لكل قائمة</string>
<string name="settings_list_reminders_hint">تجاوز الافتراضي لكل قائمة — بشكل منفصل للمحدد بوقت ومهام طوال اليوم. يمكن لقائمة الاحتفاظ بالافتراضي، إسقاطه أو تعيين خاصّته.</string>
<string name="settings_list_reminders_hint">تجاوز التذكير الافتراضي للقوائم الفردية</string>
<string name="lists_reorder_handle">اسحب لإعادة الترتيب</string>
<string name="settings_list_notifications_hint">الصوت، الاهتزاز و الأهمية لتذكيرات هذه القائمة</string>
<string name="smart_today">اليوم</string>
@@ -446,8 +446,8 @@
<string name="filter_missing">تم حذف عامل التصفية</string>
<string name="view_filter_tags">تصفية حسب الوسم</string>
<string name="filter_section_lists">القوائم</string>
<string name="settings_appearance_subtitle">السمة، الألوان، تنسيق الوقت</string>
<string name="settings_task_form_subtitle">الخانات الافتراضية و السلوك</string>
<string name="settings_appearance_subtitle">السمة، اللون و تنسيق الوقت</string>
<string name="settings_task_form_subtitle">الحقول، القائمة والمهام الفرعية الافتراضية</string>
<string name="recurrence_on_days">%1$s في %2$s</string>
<string name="recurrence_with_until">%1$s حتى %2$s</string>
<plurals name="recurrence_with_count">
@@ -521,7 +521,7 @@
<string name="add_account_server_title">أين مهامك؟</string>
<string name="add_account_working_reading_lists">جاري قراءة قوائم المهام خاصّتك</string>
<string name="add_account_browser_title">جاري الانتظار لمتصفحك</string>
<string name="add_account_browser_use_password">استخدم كلمة مرور بدلاً من ذلك</string>
<string name="add_account_browser_use_password">استخدم كلمة مرور بدلاً</string>
<string name="add_account_browser_open">فتح صفحة تسجيل الدخول</string>
<string name="add_account_lists_title">أيّ القوائم يجب المزامنة؟</string>
<string name="add_account_provider_unsupported">لا دعم للمهام عبر CalDAV</string>
@@ -569,7 +569,7 @@
<string name="add_account_browser_error_expired">تم إغلاق نافذة الموافقة قبل أن يرد الخادم.</string>
<string name="add_account_browser_error_rate_limited">يرفض الخادم المحاولات المتكررة. انتظر بضع دقائق وحاول مرة أخرى.</string>
<string name="add_account_browser_error_maintenance">الخادم في وضع الصيانة. حاول مرة أخرى بمجرد عودته.</string>
<string name="add_account_browser_error_unavailable">لا يحتوي هذا الجهاز على متصفح يمكنه فتح صفحة تسجيل الدخول. استخدم كلمة مرور بدلاً من ذلك.</string>
<string name="add_account_browser_error_unavailable">لا يحتوي هذا الجهاز على متصفح يمكنه فتح صفحة تسجيل الدخول. استخدم كلمة مرور بدلاً.</string>
<string name="add_account_quirk_hint_fastmail">يحتاج Fastmail كلمة مرور تطبيق، ليس كلمة مرور حسابك — وCalDAV ليس على الخطة الأساسية.</string>
<string name="add_account_quirk_hint_icloud">يحتاج iCloud كلمة مرور خاصة بتطبيق، التي يمكنك إنشاؤها في appleid.apple.com مع تفعيل المصادقة الثنائية.</string>
<string name="add_account_setup_body">ستقوم بذلك في متصفحك، ثم العودة هنا لإكمال.</string>
@@ -580,7 +580,7 @@
<string name="add_account_browser_body">وافق على Agendula في المتصفح، ثم ارجع هنا. لا يتم إرسال كلمة مرورك مطلقًا إلى هذا التطبيق — يصدر الخادم كلمة مرور تطبيق منفصلة يمكنك إلغاؤها في أي وقت.</string>
<string name="add_account_browser_insecure">صفحة تسجيل الدخول هذه غير مشفرة (http). أي شيء تكتبه هناك، بما في ذلك كلمة مرور حسابك، ينتقل بشكل غير مشفر.</string>
<string name="add_account_browser_host_mismatch">الخادم أرسلنا إلى %1$s، لكنك كتبت %2$s. يعني ذلك عادةً أن إعداد overwrite.cli.url خاصّته غير صحيح.</string>
<string name="add_account_origin_mismatch">أبلغ الخادم عن عنوانه كـ %1$s، الذي لا يستطيع هذا الجهاز الوصول، لذلك تم استخدام %2$s بدلاً من ذلك. من المحتمل أن يكون إعداد overwrite.cli.url أو trusted_proxies خاصّته غير صحيح.</string>
<string name="add_account_origin_mismatch">أبلغ الخادم عن عنوانه كـ %1$s، الذي لا يستطيع هذا الجهاز الوصول، لذلك تم استخدام %2$s بدلاً. من المحتمل أن يكون إعداد overwrite.cli.url أو trusted_proxies خاصّته غير صحيح.</string>
<string name="settings_licences_subtitle">المشاريع التي بنيت ‌‌Agendula عليها</string>
<string name="licences_intro">يتضمن Agendula العمل أدناه. اضغط على أي إدخال للحصول على شفرة المصدر الخاصة به.</string>
<string name="licences_footer">يتم تضمين بعض من هذا الكود في شكل معدل. أين يكون هذا هو الحال، يتم إدراج كل تغيير في مستودع المصدر المرتبط أعلاه.</string>
@@ -661,106 +661,4 @@
<string name="list_save_failed_server">الخادم لن يقبل هذا التغيير.</string>
<string name="list_delete_confirm_message">سيتم حذف ”%1$s“ وجميع مهامه. هذا لا يمكن التراجع عنه.</string>
<string name="lists_empty_action">إنشاء قائمة</string>
<string name="settings_dynamic_color_unavailable">متوفر على أندوريد ١٢ و أحدث</string>
<string name="settings_reminders_subtitle">التذكيرات و التسليم</string>
<string name="settings_form_fields_hint">الخانات المعروضة افتراضيًا على مهمة جديدة. الباقية تقع ضمن \"المزيد من الخانات\".</string>
<string name="settings_dynamic_color_hint">استخدام الألوان من خلفيتك</string>
<string name="settings_export_hint">حفظ قوائمك كملفات iCalendar</string>
<string name="settings_import_hint">إحضار المهام من ملف iCalendar</string>
<string name="settings_autofocus_title">تركيز العنوان على مهمة جديدة</string>
<string name="settings_autofocus_title_hint">عند بدء مهمة جديدة، ضع المؤشر في حقل العنوان وافتح لوحة المفاتيح على الفور.</string>
<string name="settings_storage_subtitle">أين مهامك تُبقى</string>
<string name="settings_allday_reminder_time">وقت تذكير اليوم الكامل</string>
<string name="settings_reliable_delivery_hint">أندرويد قد يؤخر التذكيرات لتوفير البطارية. استثنِ Agendula لكي تصل في الوقت المحدد.</string>
<string name="settings_reliable_delivery_exempt">معفى من تحسين البطارية — تصل التذكيرات في الوقت المحدّد.</string>
<string name="settings_translate_hint">إضافة أو تحسين لغة على Weblate</string>
<string name="settings_reminders_hint">أعلمني عندما تكون مهمة مستحقة</string>
<string name="settings_exact_alarms_allowed">تنطلق التذكيرات في الوقت المحدّد</string>
<string name="settings_notifications_blocked_hint">التذكيرات لا يمكنها الوصول لك. اضغط للسماح بإشعارات لـ Agendula.</string>
<string name="settings_exact_alarms_blocked">محظور — اضغط للسماح بالتذكيرات الدقيقة</string>
<string name="settings_default_list_first">أول قائمة متاحة</string>
<string name="settings_add_subtask_row">إظهار صف \"أضف مهمة فرعية\"</string>
<string name="settings_add_subtask_row_hint">خانة إضافة في نهاية المهام الفرعية للمهمة الموسعة</string>
<string name="settings_default_start_today">البداية الافتراضية إلى اليوم</string>
<string name="settings_default_start_today_hint">ملأ مسبق بداية مهمة جديدة بتاريخ اليوم</string>
<string name="settings_bottom_add_bar_hint">إضافة مهام من شريط مثبت إلى أسفل لقائمة، بدلاً من الزر العائم</string>
<string name="settings_task_store_hint">كل مخزن يحتفظ بمهامه الخاصة. التبديل لا يؤدي لنقلهم — انسخهم أولاً، أو صدِّرهم من النسخ الاحتياطي والاستعادة.</string>
<string name="settings_store_own_hint">التخزين خاصّة Agendula. لا شيء آخر للتثبيت.</string>
<string name="settings_store_external_hint">مشاركة المهام مع التطبيق الذي يزامنهم</string>
<string name="settings_store_external_missing">لا تطبيق مهام متوافق مثبّت</string>
<string name="accounts_remove_wipe">إزالة و حذف المهام</string>
<string name="list_delete_confirm_message_synced">”%1$s“ وجميع مهامه ستتم حذفها من الخادم، من كل جهاز آخر سُجِّل دخوله لهذا الحساب. هذا لا يمكن التراجع عنه.</string>
<plurals name="home_today_progress_count">
<item quantity="zero">تم %1$d من %2$d</item>
<item quantity="one">تم %1$d من %2$d</item>
<item quantity="two">تم %1$d من %2$d</item>
<item quantity="few">تم %1$d من %2$d</item>
<item quantity="many">تم %1$d من %2$d</item>
<item quantity="other">تم %1$d من %2$d</item>
</plurals>
<plurals name="home_today_remaining_count">
<item quantity="zero">تبقي %1$d</item>
<item quantity="one">تبقي %1$d</item>
<item quantity="two">تبقي %1$d</item>
<item quantity="few">تبقي %1$d</item>
<item quantity="many">تبقي %1$d</item>
<item quantity="other">تبقي %1$d</item>
</plurals>
<string name="list_where_device">على هذا الجهاز فقط</string>
<string name="list_read_only">تتم مشاركة هذه القائمة معك للقراءة فقط، لذلك فإن اسمها ولونها للمالك للتغيير.</string>
<string name="onboarding_no_provider_title">مُحتاج تطبيق مهام</string>
<string name="onboarding_permission_title">السماح بالوصول إلى مهامك</string>
<string name="onboarding_permission_body">Agendula يحتاج إذن لقراءة و كتابة مهامك. هذا هو الشيء الوحيد الذي يطلب له دائمًا.</string>
<string name="onboarding_permission_button">منح الوصول إلى المهام</string>
<string name="onboarding_welcome_sync_body">المزامنة عبر CalDAV مع الخادم أنت تختاره، أو الاحتفاظ بكل شيء على هذا الجهاز.</string>
<string name="onboarding_welcome_offline_title">لا إنترنت، بالتصميم</string>
<string name="onboarding_welcome_offline_body">هذا الإصدار من Agendula ليس لديه الوصول إلى الشبكة، لذلك فإنه لا يمكنه المزامنة أو الاتصال لأي شيء نفسه.</string>
<string name="onboarding_sync_title">مزامنة مع خادمك الخاص؟</string>
<string name="onboarding_sync_body">Agendula يدعم CalDAV — Nextcloud, Radicale, Posteo, Fastmail و أي شيء آخر يدعمه. لا حساب من خاصّتنا، أبدًا.</string>
<plurals name="widget_today_progress_cd">
<item quantity="zero">تم %1$d من %2$d</item>
<item quantity="one">تم %1$d من %2$d</item>
<item quantity="two">تم %1$d من %2$d</item>
<item quantity="few">تم %1$d من %2$d</item>
<item quantity="many">تم %1$d من %2$d</item>
<item quantity="other">تم %1$d من %2$d</item>
</plurals>
<plurals name="sync_notice_more">
<item quantity="zero">و (%1$d) لا أخرى</item>
<item quantity="one">و %1$d آخر</item>
<item quantity="two">و %1$d آخران</item>
<item quantity="few">و %1$d أخرى</item>
<item quantity="many">و %1$d آخر</item>
<item quantity="other">و %1$d آخر</item>
</plurals>
<string name="onboarding_no_provider_body">أنت اخترت للاحتفاظ بالمهام في تطبيق آخر، لكن لا واحد متوافق مثبّت. قم بتثبيت OpenTasks أو tasks.org، أو قم بالتبديل عودتًا إلى التخزين خاصّة Agendula.</string>
<string name="onboarding_sync_connect">ربط حساب</string>
<string name="onboarding_sync_devices_title">كل جهاز في وقت واحد</string>
<string name="onboarding_sync_devices_body">ضع علامة صح لشيء ما هنا و سيتم وضع العلامة عليه في كل مكان.</string>
<string name="onboarding_sync_copy_title">نسخة تبقَى بعد الهاتف</string>
<string name="onboarding_sync_copy_body">تعيش مهامك على الخادم أيضًا، لذلك جهاز مفقود لا يعني فقدان العمل.</string>
<string name="onboarding_sync_yours_title">لك الاختيار</string>
<string name="onboarding_sync_yours_body">مستضاف ذاتيًا (Self-hosted) أو مزود أنت تدفع له بالفعل — Agendula فقط يخزن كلمة المرور.</string>
<string name="onboarding_list_title">قائمتك الأولى</string>
<string name="onboarding_list_body">تحافظ القوائم على فصل الأشياء. ابدأ بواحدة — يمكنك إضافة المزيد، وإعادة تسمية هذه القائمة، أيًّا كان الوقت.</string>
<string name="onboarding_backup_title">احتفظ بنسخة</string>
<string name="settings_section_backup">النسخ الاحتياطي والاستعادة</string>
<string name="onboarding_backup_body_later">دون حساب، قوائمك تعيش فقط على هذا الجهاز. احفظهم كملفات iCalendar أي تطبيق مهام أو تقويم آخر يمكنه قراءتها، أيّا كان الوقت من الإعدادات ← النسخ الاحتياطي والاستعادة ← تصدير.</string>
<string name="onboarding_quick_add_title">كيف ترغب في إضافة المهام؟</string>
<string name="onboarding_quick_add_bar">شريط الإضافة السريع</string>
<string name="onboarding_quick_add_bar_hint">خانة في أسفل كل قائمة: اكتب، اضغط إدخال، إلى التالي.</string>
<string name="onboarding_quick_add_button">زر مهمة جديدة</string>
<string name="onboarding_quick_add_button_hint">زر عائم يفتح النموذج الكامل.</string>
<string name="onboarding_exact_alarms_title">صحيح على الوقت</string>
<string name="onboarding_exact_alarms_body">أندوريد يسأل قبل أن يتمكن تطبيق من إعلامك في دقيقة محددة. اسمح به والتذكيرات تصل في موعد الاستحقاق خاصّتهم — بدونه قد يتأخروا بضع دقائق.</string>
<string name="onboarding_exact_alarms_allow">السماح بالتوقيت الدقيق</string>
<string name="onboarding_use_own_store">استخدم تخزين هذا الجهاز بدلاً من ذلك</string>
<string name="widget_config_theme_follow_app">اتبع التطبيق</string>
<string name="widget_config_theme_light">فاتح</string>
<string name="widget_config_theme_dark">داكن</string>
<string name="widget_config_row_color">ألوان خام</string>
<string name="widget_config_row_color_neutral">محايد</string>
<string name="widget_config_background">الخلفية</string>
<string name="widget_config_background_solid">سادة</string>
<string name="widget_config_theme">السمة</string>
</resources>
+4 -209
View File
@@ -68,7 +68,7 @@
<string name="settings_translate_hint">Přidejte nový jazyk nebo vylepšete překlad na Weblate</string>
<string name="settings_translate">Pomozte s překladem</string>
<string name="onboarding_no_provider_body">Rozhodli jste se ukládat úkoly v jiné aplikaci, ale nemáte nainstalovanou žádnou kompatibilní aplikaci. Nainstalujte OpenTasks nebo tasks.org, případně přepněte zpět na vlastní úložiště aplikace Agendula.</string>
<string name="onboarding_backup_body_later">Pokud nemáte účet, vaše seznamy jsou uložené pouze v tomto zařízení. Kdykoli je můžete uložit jako soubory iCalendar, které lze otevřít v jiných aplikacích pro úkoly nebo kalendář. Stačí přejít do Nastavení → Zálohování a obnovení → Export.</string>
<string name="onboarding_backup_body_later">Pokud nemáte účet, vaše seznamy jsou uložené pouze v tomto zařízení. Kdykoli je můžete exportovat jako soubory iCalendar, které lze otevřít v jiných aplikacích pro úkoly nebo kalendář. Stačí přejít do Nastavení → Úložiště → Exportovat úkoly.</string>
<string name="onboarding_use_own_store">Používat úložiště tohoto zařízení</string>
<string name="settings_section_storage">Úložiště</string>
<string name="settings_storage_subtitle">Kde jsou úkoly uložené a jak je exportovat</string>
@@ -76,9 +76,9 @@
<string name="export_to_folder">Uložit do adresáře</string>
<string name="export_to_zip">Uložit jako ZIP soubor</string>
<string name="settings_export">Exportovat úkoly</string>
<string name="settings_about_support">Podpořit vývoj</string>
<string name="settings_about_support">Podpora vývoje</string>
<string name="settings_section_appearance">Vzhled</string>
<string name="settings_appearance_subtitle">Motiv, barvy, formát času</string>
<string name="settings_appearance_subtitle">Motiv, barva a formát času</string>
<string name="settings_theme">Motiv</string>
<string name="settings_theme_system">Dle systému</string>
<string name="settings_theme_light">Světlý</string>
@@ -94,7 +94,7 @@
<string name="settings_time_format_auto">Automaticky</string>
<string name="settings_time_format_auto_summary">Podle systému: %1$s</string>
<string name="settings_time_format_12h">12hodinový (2:00 PM)</string>
<string name="settings_time_format_24h">24hodinový (14:00)</string>
<string name="settings_time_format_24h">24hodinový formát (14:00)</string>
<string name="settings_week_start">Víkend začíná v</string>
<string name="settings_week_start_auto">Automaticky</string>
<string name="settings_group_look">Vzhled a chování</string>
@@ -115,209 +115,4 @@
<string name="list_color">Barva</string>
<string name="widget_config_row_color">Barva řádků</string>
<string name="widget_config_row_color_by_list_hint">Každý řádek je lehce zabarven barvou svého seznamu</string>
<string name="settings_section_task_form">Formulář úkolu</string>
<string name="settings_task_form_subtitle">Výchozí pole, seznam a podúkoly</string>
<string name="recurrence_title">Opakování</string>
<string name="home_today_all_done">Vše hotovo 🎉</string>
<string name="edit_start_label">Začátek</string>
<string name="detail_due">Termín splnění</string>
<string name="edit_due_label">Termín splnění</string>
<string name="error_due_before_start">Termín splnění nemůže předcházet začátku.</string>
<string name="error_recurrence_without_date">Opakující se úkol vyžaduje datum začátku nebo termín splnění.</string>
<string name="filter_section_due">Termín splnění</string>
<string name="filter_any_due">Jakýkoli termín splnění</string>
<string name="home_due_today">Dnes</string>
<string name="home_due_tomorrow">Zítra</string>
<string name="reminder_onboarding_body">Agendula vás upozorní, když nastane čas něco splnit, takže si nemusíte všechno pamatovat.</string>
<string name="onboarding_exact_alarms_body">Android se předem zeptá, zda aplikaci povolíte posílat upozornění v přesně stanovený čas. Pokud to povolíte, připomínky dorazí včas. Bez tohoto oprávnění se mohou o několik minut opozdit.</string>
<string name="settings_week_start_auto_summary">Aktuálně %1$s</string>
<string name="settings_group_about">O aplikaci</string>
<string name="settings_about_author">od Jean-Luc Makiola</string>
<string name="settings_about_version">Verze %1$s</string>
<string name="settings_about_logo_desc">Ikona aplikace Agendula</string>
<string name="settings_group_app">Aplikace</string>
<string name="settings_section_backup">Zálohování a obnovení</string>
<string name="settings_backup_subtitle">Export a import</string>
<string name="import_title">Importovat úkoly</string>
<plurals name="import_found">
<item quantity="one">%1$d úkol</item>
<item quantity="few">%1$d úkolů</item>
<item quantity="many">%1$d úkolů</item>
<item quantity="other">%1$d úkolů</item>
</plurals>
<string name="settings_export_hint">Uložit seznamy jako soubory iCalendar</string>
<string name="settings_import_hint">Importovat úkoly ze souboru iCalendar</string>
<string name="import_pick">Vybrat soubor</string>
<string name="import_pick_other">Vybrat jiný soubor</string>
<string name="import_hint">Vyberte soubor .ics nebo archiv ZIP obsahující více takových souborů, jaký vytváří export. Zachovají se opakování, podúkoly i všechny ostatní údaje úkolů. Úkoly, které už ve vybraném seznamu jsou, budou přeskočeny.</string>
<string name="import_pick_another">Importovat jiný soubor</string>
<string name="add_account_provider_body">Vyberte službu, ve které máte uložené seznamy úkolů. Agendula se synchronizuje se všemi službami podporujícími protokol CalDAV.</string>
<string name="add_account_lists_body">Vyberte seznamy úkolů, které chcete synchronizovat. Toto nastavení můžete později změnit.</string>
<string name="settings_copy_from_external">Kopírovat úkoly z %1$s</string>
<string name="onboarding_welcome_sync_body">Úkoly můžete synchronizovat přes CalDAV s vlastním serverem, nebo je uchovávat pouze v tomto zařízení.</string>
<string name="settings_store_own">Na tomto zařízení</string>
<string name="import_new_list_hint">Nový seznam na tomto zařizení</string>
<string name="settings_task_store_hint">Každé úložiště uchovává vlastní úkoly. Přepnutím mezi úložišti se úkoly nepřesunou. Nejprve je zkopírujte do druhého úložiště nebo je exportujte v sekci Zálohování a obnovení.</string>
<string name="settings_store_permission_denied_hint">K úkolům v jiné aplikaci nebude možné přistupovat, dokud nepovolíte přístup. Klepnutím otevřete nastavení aplikace.</string>
<string name="settings_section_accounts">Účty</string>
<string name="accounts_sync_interval">Synchronizovat automaticky</string>
<string name="accounts_sync_interval_hint">Jak často se jednotlivé účty synchronizují na pozadí. Změny, které provedete, se v každém případě odešlou do jedné minuty. Android může interval prodloužit, aby šetřil baterii.</string>
<string name="reminder_unit_minutes">Minuty</string>
<plurals name="duration_minutes">
<item quantity="one">%d minuta</item>
<item quantity="few">%d minut</item>
<item quantity="many">%d minut</item>
<item quantity="other">%d minut</item>
</plurals>
<plurals name="accounts_sync_every_minutes">
<item quantity="one">Každou %d minutu</item>
<item quantity="few">Každé %d minuty</item>
<item quantity="many">Každých %d minut</item>
<item quantity="other">Každých %d minut</item>
</plurals>
<string name="add_account_browser_error_rate_limited">Server dočasně odmítá další pokusy. Počkejte několik minut a zkuste to znovu.</string>
<plurals name="duration_hours">
<item quantity="one">%d hodina</item>
<item quantity="few">%d hodin</item>
<item quantity="many">%d hodin</item>
<item quantity="other">%d hodin</item>
</plurals>
<plurals name="reminder_hours">
<item quantity="one">Před %1$d hodinou</item>
<item quantity="few">Před %1$d hodinami</item>
<item quantity="many">Před %1$d hodinami</item>
<item quantity="other">Před %1$d hodinami</item>
</plurals>
<plurals name="reminder_hours_before_start">
<item quantity="one">%1$d hodina před začátkem</item>
<item quantity="few">%1$d hodiny před začátkem</item>
<item quantity="many">%1$d hodin před začátkem</item>
<item quantity="other">%1$d hodin před začátkem</item>
</plurals>
<plurals name="accounts_sync_every_hours">
<item quantity="one">Každou hodinu</item>
<item quantity="few">Každé %d hodiny</item>
<item quantity="many">Každých %d hodin</item>
<item quantity="other">Každých %d hodin</item>
</plurals>
<string name="accounts_sync_interval_manual">Jen při synchronizaci</string>
<string name="accounts_push">Okamžité doručování</string>
<string name="accounts_push_off">Vypnuto</string>
<string name="accounts_push_unsupported">Váš server zatím nepodporuje okamžité doručování</string>
<string name="accounts_push_no_distributor">Nainstalujte si aplikaci podporující UnifiedPush, abyste dostávali změny okamžitě</string>
<string name="accounts_push_hint">Pokud máte nainstalovanou aplikaci podporující UnifiedPush, například ntfy, změny provedené na ostatních zařízeních se projeví během několika sekund, místo aby se zobrazily až při další synchronizaci. Váš server musí podporovat WebDAV-Push. V případě Nextcloudu tuto funkci zajišťuje aplikace WebDAV Push.</string>
<string name="accounts_push_none_installed">Nemáte nainstalovanou žádnou aplikaci podporující UnifiedPush. Nainstalujte si například ntfy, NextPush nebo Sunup a její nabídka se zobrazí zde.</string>
<string name="onboarding_sync_body">Agendula komunikuje přes CalDAV – podporuje Nextcloud, Radicale, Posteo, Fastmail i jakoukoli jinou službu, která tento protokol používá. U nás si nikdy nemusíte zakládat účet.</string>
<string name="onboarding_sync_connect">Připojit účet</string>
<string name="accounts_empty_body">Přidejte účet CalDAV a Agendula bude synchronizovat vaše seznamy úkolů s tímto účtem. Podporuje Nextcloud, Radicale, Baïkal i jakoukoli jinou službu, která používá tento protokol.</string>
<string name="accounts_add">Přidat účet</string>
<string name="accounts_failed_misconfigured">Synchronizace selhala – účet není kompletní. Odeberte ho a přidejte znovu</string>
<string name="add_account_summary_body">Agendula bude odteď tyto seznamy synchronizovat. Které seznamy se synchronizují, můžete změnit v Nastavení.</string>
<string name="add_account_server_body">Zadejte adresu svého serveru CalDAV. Pokud server používá jako svou adresu vlastní doménu, můžete zadat také e-mailovou adresu.</string>
<string name="add_account_credentials_body">Pokud váš poskytovatel tuto možnost nabízí, použijte heslo pro aplikaci. Lze ho kdykoli zrušit, aniž byste museli měnit heslo ke svému účtu.</string>
<string name="add_account_error_not_dav">Zadaná adresa odpověděla, ale nejedná se o server CalDAV. Pokud jste vložili adresu webové stránky, zkuste použít adresu serveru, kterou váš poskytovatel uvádí pro kalendáře.</string>
<string name="add_account_error_unreachable">K serveru se nepodařilo připojit. Zkontrolujte adresu a připojení k internetu.</string>
<string name="add_account_error_server">Na straně serveru došlo k chybě. Zkuste to za chvíli znovu.</string>
<string name="add_account_error_google_unsupported">Kalendář Google nepodporuje synchronizaci úkolů přes CalDAV, jak uvádí i jeho vlastní dokumentace. Agendula s ním proto nemůže synchronizovat úkoly.</string>
<string name="add_account_error_cross_domain">Tento server uchovává některé seznamy úkolů na adrese %1$s, ke kterým se Agendula zatím nedokáže přihlásit.</string>
<string name="add_account_error_no_usable_lists">Přihlášení proběhlo úspěšně, ale tento účet neobsahuje žádné seznamy úkolů, které může Agendula používat.</string>
<string name="add_account_browser_error_expired">Časový limit pro schválení vypršel dříve, než server odpověděl.</string>
<string name="add_account_browser_error_maintenance">Server je v režimu údržby. Zkuste to znovu, až bude údržba dokončena.</string>
<string name="add_account_browser_error_unavailable">V tomto zařízení není prohlížeč, který by dokázal otevřít přihlašovací stránku. Použijte místo toho heslo.</string>
<string name="add_account_quirk_hint_fastmail">Fastmail vyžaduje heslo pro aplikaci, nikoli heslo k vašemu účtu. Protokol CalDAV navíc není součástí tarifu Basic.</string>
<string name="add_account_quirk_hint_icloud">iCloud vyžaduje heslo pro konkrétní aplikaci. Vytvoříte ho na adrese appleid.apple.com, pokud máte zapnuté dvoufaktorové ověřování.</string>
<string name="add_account_setup_icloud_title">iCloud vyžaduje heslo pro konkrétní aplikaci</string>
<string name="add_account_setup_fastmail_title">Fastmail vyžaduje heslo pro aplikaci</string>
<string name="add_account_setup_fastmail_footnote">CalDAV není součástí tarifu Fastmail Basic.</string>
<string name="add_account_password_hint">Pokud váš server tuto možnost nabízí, použijte heslo pro aplikaci – můžete ho zrušit, aniž byste museli měnit heslo ke svému účtu.</string>
<string name="add_account_browser_body">Schvalte Agendulu v prohlížeči a pak se vraťte sem. Vaše heslo se této aplikaci nikdy neodesílá – server vytvoří samostatné heslo pro aplikaci, které můžete kdykoli zrušit.</string>
<string name="add_account_browser_insecure">Přihlašovací stránka není zabezpečená šifrováním (HTTP). Vše, co na ní zadáte, včetně hesla k účtu, se přenáší nešifrovaně.</string>
<string name="add_account_browser_host_mismatch">Server nás přesměroval na adresu %1$s, ale vy jste zadali adresu %2$s. To obvykle znamená, že nastavení `overwrite.cli.url` není správné.</string>
<string name="add_account_origin_mismatch">Server oznámil svou adresu jako %1$s, na kterou se toto zařízení nemůže připojit, proto byla použita adresa %2$s. Nastavení `overwrite.cli.url` nebo `trusted_proxies` serveru je pravděpodobně nesprávné.</string>
<string name="cd_expand_subtasks">Ukázat podúkoly</string>
<string name="cd_collapse_subtasks">Skrýt podúkoly</string>
<string name="detail_subtasks">Podúkoly</string>
<plurals name="detail_delete_body_subtasks">
<item quantity="one">Tento úkol a jeho podúkol budou smazány.</item>
<item quantity="few">Tento úkol a jeho %1$d podúkoly budou smazány.</item>
<item quantity="many">Tento úkol a jeho %1$d podúkolů budou smazány.</item>
<item quantity="other">Tento úkol a jeho %1$d podúkolů budou smazány.</item>
</plurals>
<string name="recurrence_none">Neopakovat</string>
<string name="edit_recurrence_label">Opakování</string>
<string name="edit_recurrence_none">Neopakovat</string>
<string name="reminder_custom">Vlastní</string>
<string name="reminder_custom_with_value">Vlastní (%1$s)</string>
<string name="recurrence_weekly">Každý týden</string>
<plurals name="recurrence_every_n_weeks">
<item quantity="one">Každý %1$d týden</item>
<item quantity="few">Každé %1$d týdny</item>
<item quantity="many">Každých %1$d týdnů</item>
<item quantity="other">Každých %1$d týdnů</item>
</plurals>
<plurals name="reminder_weeks_before_start">
<item quantity="one">%1$d týden před začátkem</item>
<item quantity="few">%1$d týdny před začátkem</item>
<item quantity="many">%1$d týdnů před začátkem</item>
<item quantity="other">%1$d týdnů před začátkem</item>
</plurals>
<plurals name="reminder_weeks_before_due">
<item quantity="one">%1$d týden před termínem splnění</item>
<item quantity="few">%1$d týdny před termínem splnění</item>
<item quantity="many">%1$d týdnů před termínem splnění</item>
<item quantity="other">%1$d týdnů před termínem splnění</item>
</plurals>
<string name="recurrence_incomplete">Zadejte číslo od 1 do 999</string>
<string name="recurrence_end_count">Po určitém počtu opakování</string>
<string name="recurrence_end_until">V konkrétní den</string>
<string name="error_reminder_without_date">Pro nastavení připomínky zadejte datum začátku nebo termín splnění.</string>
<string name="reminder_on_start_date">V den začátku</string>
<string name="reminder_on_due_date">V den termínu splnění</string>
<string name="reminder_anchor_due">Od termínu splnění</string>
<string name="filters_empty_hint">Kombinujte termíny splnění, seznamy, štítky a priority do vlastního přehledu</string>
<string name="settings_default_start_today_hint">Předvyplnit datum začátku nového úkolu dnešním datem</string>
<plurals name="reminder_minutes_before_due">
<item quantity="one">%1$d minutu před termínem splnění</item>
<item quantity="few">%1$d minut před termínem splnění</item>
<item quantity="many">%1$d minut před termínem splnění</item>
<item quantity="other">%1$d minut před termínem splnění</item>
</plurals>
<plurals name="reminder_hours_before_due">
<item quantity="one">%1$d hodinu před termínem splnění</item>
<item quantity="few">%1$d hodin před termínem splnění</item>
<item quantity="many">%1$d hodin před termínem splnění</item>
<item quantity="other">%1$d hodin před termínem splnění</item>
</plurals>
<plurals name="reminder_days_before_due">
<item quantity="one">%1$d den před termínem splnění</item>
<item quantity="few">%1$d dny před termínem splnění</item>
<item quantity="many">%1$d dní před termínem splnění</item>
<item quantity="other">%1$d dní před termínem splnění</item>
</plurals>
<string name="clear_completed_confirm">Smazat</string>
<string name="detail_delete_title">Smazat úkol?</string>
<string name="detail_delete_body">Tento úkol bude smazán.</string>
<string name="filter_delete_title">Smazat “%1$s”?</string>
<string name="list_delete_confirm_title">Smazat seznam?</string>
<string name="list_delete_confirm_message_synced">„%1$s“ a všechny jeho úkoly budou smazány ze serveru i ze všech ostatních zařízení přihlášených k tomuto účtu. Tuto akci nelze vrátit zpět.</string>
<string name="list_delete_confirm_message">„%1$s“ a všechny jeho úkoly budou smazány. Tuto akci nelze vrátit zpět.</string>
<string name="accounts_remove_body">%1$s se přestane synchronizovat s tímto zařízením. Na serveru se nic nesmaže.</string>
<string name="accounts_remove_wipe_body">Z tohoto zařízení se zároveň smažou všechny seznamy a úkoly patřící k tomuto účtu.</string>
<string name="accounts_lists_drop_body">U seznamů, u kterých jste zrušili zaškrtnutí, se zastaví synchronizace s tímto zařízením. Na serveru se nic nesmaže.</string>
<string name="accounts_lists_drop_wipe_body">Z tohoto zařízení se zároveň smažou vybrané seznamy i všechny jejich úkoly.</string>
<string name="sync_notice_cause_delete_lost">Tady jste ho smazali, ale na serveru byl mezitím změněn</string>
<string name="permission_write_tasks_description">Umožňuje aplikaci přidávat, upravovat, označovat jako dokončené a mazat úkoly uložené v Agendule, včetně jejich připomínek a štítků. Seznamy úkolů upravovat ani mazat nemůže.</string>
<string name="edit_set">Nastavit</string>
<string name="reminder_custom_set">Nastavit</string>
<string name="settings_list_reminders_hint">Přepíše výchozí nastavení pro každý seznam zvlášť, a to samostatně pro úkoly s konkrétním časem a celodenní úkoly. Seznam může převzít výchozí nastavení, nepoužívat žádné nebo mít vlastní nastavení.</string>
<string name="settings_list_notifications">Zvuk a vibrace</string>
<string name="settings_list_notifications_hint">Zvuk, vibrace a důležitost připomínek pro tento seznam</string>
<string name="detail_duplicate">Vytvořit kopii</string>
<string name="detail_status_cancelled">Zrušeno</string>
<string name="detail_cancel_task">Zrušit úkol</string>
<string name="detail_restore">Obnovit úkol</string>
<string name="edit_reminder_label">Upozornění</string>
<string name="reminder_at_due">V čase úkolu</string>
<string name="reminder_on_the_day">V daný den</string>
<string name="settings_list_reminders_title">Upozornění pro jednotlivé seznamy</string>
</resources>
-15
View File
@@ -442,19 +442,4 @@
<string name="recurring_scope_series">Alle Termine der Serie</string>
<string name="edit_recurrence_label">Wiederholung</string>
<string name="edit_recurrence_none">Wiederholt sich nicht</string>
<string name="reminder_time">Zeit</string>
<string name="filters_header">Filter</string>
<string name="filter_new_title">Neuer Filter</string>
<string name="filter_edit_title">Filter bearbeiten</string>
<string name="filter_name_hint">Name des Filters</string>
<string name="filter_section_due">Fällig</string>
<string name="filter_section_priority">Priorität</string>
<string name="filter_section_lists">Listen</string>
<string name="filter_priority_low">Niedriger und höher</string>
<string name="filter_priority_medium">Mittel und höher</string>
<string name="filter_priority_high">Nur hoch</string>
<string name="filter_delete">Filter löschen</string>
<string name="filter_delete_title">“%1$s” löschen?</string>
<string name="filter_delete_confirm">Löschen</string>
<string name="filter_missing">Filter gelöscht</string>
</resources>
@@ -1,4 +0,0 @@
<resources>
<!-- Nearest to the dynamic scheme's background, tone 6 of the neutral variant. -->
<color name="window_background">@android:color/system_neutral2_900</color>
</resources>
@@ -1,3 +0,0 @@
<resources>
<color name="window_background">@android:color/system_background_dark</color>
</resources>
-3
View File
@@ -1,3 +0,0 @@
<resources>
<bool name="light_system_bars">false</bool>
</resources>
-3
View File
@@ -1,3 +0,0 @@
<resources>
<color name="window_background_fallback">#FF161113</color>
</resources>
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Agendula</string>
<string name="task_untitled">Задача без имени</string>
<string name="back">Назад</string>
<string name="save">Сохранить</string>
<string name="delete">Удалить</string>
<string name="tasks_title">Задачи</string>
<string name="task_list_empty">Здесь пока нет задач.</string>
<string name="task_list_failure">Невозможно загрузить эти задачи.</string>
<string name="smart_no_date">Без даты</string>
<string name="smart_completed">Завершено</string>
<string name="task_detail_title">Задача</string>
<string name="task_detail_not_found">Задачи больше не существует.</string>
<string name="edit">Редактировать</string>
<string name="field_description">Описание</string>
<string name="edit_save_failed">Не удалось сохранить. Попробуйте снова.</string>
<string name="add_task_hint">Добавить задачу</string>
<string name="add_subtask_hint">Добавить подзадачу</string>
<string name="cd_add_task">Добавить задачу</string>
<string name="cd_complete">Завершить</string>
<string name="cd_reopen">Открыть заново</string>
<string name="task_deleted">Удалено</string>
<string name="task_completed">Завершено</string>
<string name="cd_repeats">Повторить</string>
<string name="shortcut_new_task">Новая задача</string>
<string name="shortcut_today">Сегодня</string>
<string name="qs_tile_new_task">Новая задача</string>
<string name="lists_reorder">Переопределить список</string>
<string name="lists_reorder_done">Завершить переопределение</string>
</resources>
-3
View File
@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
-4
View File
@@ -1,4 +0,0 @@
<resources>
<!-- Nearest to the dynamic scheme's background, tone 98 of the neutral variant. -->
<color name="window_background">@android:color/system_neutral2_10</color>
</resources>
-3
View File
@@ -1,3 +0,0 @@
<resources>
<color name="window_background">@android:color/system_background_light</color>
</resources>
@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
-3
View File
@@ -1,3 +0,0 @@
<resources>
<bool name="light_system_bars">true</bool>
</resources>
-4
View File
@@ -2,8 +2,4 @@
<!-- Seed color: warm mauve. Material 3 derives Light & Dark schemes from this. -->
<!-- Adaptive icon background -->
<color name="ic_launcher_background">#FF7A5C6B</color>
<!-- The Compose fallback schemes' background, kept equal by WindowColorsTest. -->
<color name="window_background_fallback">#FFFEFBFC</color>
<!-- The launch window and splash: the dynamic background where there is one. -->
<color name="window_background">@color/window_background_fallback</color>
</resources>
+2 -14
View File
@@ -1,19 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- The launch window and splash; Compose draws the rest. Its light or dark
follows the app's theme setting, handed to the system by SystemTheme. -->
<style name="Theme.Agendula" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowBackground">@color/window_background</item>
<style name="Theme.Agendula" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">@bool/light_system_bars</item>
</style>
<!-- Splash themes picked at runtime on API 33+, see SystemTheme. -->
<style name="Theme.Agendula.Fallback">
<item name="android:windowBackground">@color/window_background_fallback</item>
</style>
<style name="Theme.Agendula.Black">
<item name="android:windowBackground">@android:color/black</item>
<item name="android:windowLightStatusBar">true</item>
</style>
</resources>
-4
View File
@@ -10,8 +10,4 @@
<locale android:name="en" />
<locale android:name="de" />
<locale android:name="pt-BR" />
<locale android:name="fr" />
<locale android:name="ar" />
<locale android:name="cs" />
<locale android:name="ru" />
</locale-config>
@@ -61,77 +61,6 @@ class StoredRemindersTest {
assertThat(stored(form.copy(reminders = listOf(hourBeforeDue)), due)).containsExactly(hourBeforeDue, dayBeforeStart)
}
private fun moved(
form: TaskForm,
start: Instant?,
due: Instant?,
wasAllDay: Boolean = form.isAllDay,
at: ZoneId = zone,
) = StoredReminders.withDefaults(form, start, due, wasAllDay, settings, now.toEpochMilliseconds(), at)
@Test
fun `a date moved after its defaults rang brings them`() {
val form = TaskForm(title = "t", listId = 1, start = now + 2.days, due = now + 5.days, reminders = listOf(hourBeforeDue))
assertThat(moved(form, start = now, due = now + 5.days)).containsExactly(hourBeforeDue, dayBeforeStart)
// Its date still ahead, but the lead already past.
assertThat(moved(form, start = now + 2.hours, due = now + 5.days)).containsExactly(hourBeforeDue, dayBeforeStart)
}
@Test
fun `a moved due brings its defaults the same way`() {
val ownStart = TaskReminder(0, fromStart = true)
val form = TaskForm(title = "t", listId = 1, start = now - 3.days, due = now + 5.days, reminders = listOf(ownStart))
assertThat(moved(form, start = now - 3.days, due = now)).containsExactly(ownStart, dayBeforeDue, hourBeforeDue)
}
@Test
fun `a moved date keeps the form when its defaults had not rung or are named`() {
val form = TaskForm(title = "t", listId = 1, start = now + 3.days, due = now + 5.days, reminders = listOf(hourBeforeDue))
assertThat(moved(form, start = now + 2.days, due = now + 5.days)).containsExactly(hourBeforeDue)
val ownStart = TaskReminder(30, fromStart = true)
assertThat(moved(form.copy(reminders = listOf(ownStart)), start = now, due = now + 5.days)).containsExactly(ownStart)
}
@Test
fun `a task without reminders of its own keeps none when a past date moves`() {
val form = TaskForm(title = "t", listId = 1, start = now + 2.days, due = now + 5.days)
assertThat(moved(form, start = now, due = now + 5.days)).isEmpty()
assertThat(moved(form.copy(rrule = "FREQ=DAILY", reminders = listOf(hourBeforeDue)), start = now, due = now + 5.days))
.containsExactly(hourBeforeDue)
}
@Test
fun `turning all-day without moving the date is not a move`() {
val form = TaskForm(
title = "t",
listId = 1,
start = allDayInstantOf(LocalDate.of(2026, 10, 1)),
due = allDayInstantOf(LocalDate.of(2026, 10, 6)),
isAllDay = true,
reminders = listOf(TaskReminder(1_440)),
)
assertThat(moved(form, start = now, due = now + 5.days, wasAllDay = false)).containsExactly(TaskReminder(1_440))
}
@Test
fun `an all-day date is past by its own calendar, not by UTC midnight`() {
val ownStart = TaskReminder(0, fromStart = true)
val newYork = ZoneId.of("America/New_York")
// 20:00 on Sep 30 in New York: Oct 1 has not begun there.
val form = TaskForm(
title = "t",
listId = 1,
start = allDayInstantOf(LocalDate.of(2026, 9, 28)),
due = allDayInstantOf(LocalDate.of(2026, 10, 2)),
isAllDay = true,
reminders = listOf(ownStart),
)
val due = allDayInstantOf(LocalDate.of(2026, 10, 1))
assertThat(
StoredReminders.withDefaults(form, form.start, due, true, settings, Instant.parse("2026-10-01T00:00:00Z").toEpochMilliseconds(), newYork),
).containsExactly(ownStart)
}
@Test
fun `a gained date the form already counts from keeps the form`() {
val current = testTask(start = now + 2.days)
@@ -45,36 +45,11 @@ class TaskFilteringTest {
}
@Test
fun `completed matches COMPLETED, never the open lists`() {
fun `completed only matches COMPLETED, never the open lists`() {
val task = testTask(status = TaskStatus.COMPLETED, due = at(1000 * dayMs + 1))
assertThat(matches(task, SmartList.COMPLETED)).isTrue()
assertThat(matches(task, SmartList.ALL)).isFalse()
assertThat(matches(task, SmartList.UPCOMING)).isFalse()
}
@Test
fun `today takes a task that starts today`() {
assertThat(matches(testTask(start = at(1000 * dayMs + 1)), SmartList.TODAY)).isTrue()
assertThat(matches(testTask(start = at(1000 * dayMs + 1), due = at(1003 * dayMs)), SmartList.TODAY)).isTrue()
assertThat(matches(testTask(start = at(1001 * dayMs + 1)), SmartList.TODAY)).isFalse()
assertThat(matches(testTask(start = at(999 * dayMs)), SmartList.TODAY)).isFalse()
}
@Test
fun `a task starting today but already overdue stays in Overdue only`() {
val task = testTask(start = at(1000 * dayMs + 1), due = at(999 * dayMs))
assertThat(matches(task, SmartList.TODAY)).isFalse()
assertThat(matches(task, SmartList.OVERDUE)).isTrue()
}
@Test
fun `today takes a task completed today, whatever its dates`() {
val done = testTask(status = TaskStatus.COMPLETED, due = at(990 * dayMs), completedAt = at(1000 * dayMs + 5))
assertThat(matches(done, SmartList.TODAY)).isTrue()
val dueTodayDoneEarlier = done.copy(due = at(1000 * dayMs + 1), completedAt = at(999 * dayMs))
assertThat(matches(dueTodayDoneEarlier, SmartList.TODAY)).isFalse()
val cancelled = testTask(status = TaskStatus.CANCELLED, due = at(1000 * dayMs + 1))
assertThat(matches(cancelled, SmartList.TODAY)).isFalse()
}
@Test
@@ -40,14 +40,6 @@ class TaskSectionsTest {
.inOrder()
}
@Test
fun `the Today list keeps open tasks under Today`() {
fun inToday(task: Task) = TaskSections.sectionOf(task, todayStart, todayEnd, todayList = true)
assertThat(inToday(testTask(start = at(1000 * dayMs + 1)))).isEqualTo(TaskSection.TODAY)
assertThat(inToday(testTask(start = at(1000 * dayMs + 1), due = at(1003 * dayMs)))).isEqualTo(TaskSection.TODAY)
assertThat(inToday(testTask(status = TaskStatus.COMPLETED))).isEqualTo(TaskSection.COMPLETED)
}
@Test
fun `empty input yields no sections`() {
assertThat(TaskSections.of(emptyList(), todayStart, todayEnd)).isEmpty()
@@ -1,26 +0,0 @@
package de.jeanlucmakiola.agendula.ui.theme
import android.app.UiModeManager
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.agendula.R
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
import org.junit.jupiter.api.Test
class SystemThemeTest {
@Test
fun `the system follows the app's light or dark`() {
assertThat(SystemTheme.nightModeOf(ThemeMode.SYSTEM)).isEqualTo(UiModeManager.MODE_NIGHT_AUTO)
assertThat(SystemTheme.nightModeOf(ThemeMode.LIGHT)).isEqualTo(UiModeManager.MODE_NIGHT_NO)
assertThat(SystemTheme.nightModeOf(ThemeMode.DARK)).isEqualTo(UiModeManager.MODE_NIGHT_YES)
assertThat(SystemTheme.nightModeOf(ThemeMode.BLACK)).isEqualTo(UiModeManager.MODE_NIGHT_YES)
}
@Test
fun `the splash opens on the background the app draws`() {
assertThat(SystemTheme.splashThemeOf(ThemeMode.SYSTEM, dynamicColor = true)).isEqualTo(R.style.Theme_Agendula)
assertThat(SystemTheme.splashThemeOf(ThemeMode.DARK, dynamicColor = false)).isEqualTo(R.style.Theme_Agendula_Fallback)
assertThat(SystemTheme.splashThemeOf(ThemeMode.BLACK, dynamicColor = true)).isEqualTo(R.style.Theme_Agendula_Black)
assertThat(SystemTheme.splashThemeOf(ThemeMode.BLACK, dynamicColor = false)).isEqualTo(R.style.Theme_Agendula_Black)
}
}
@@ -1,27 +0,0 @@
package de.jeanlucmakiola.agendula.ui.theme
import androidx.compose.ui.graphics.toArgb
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
/** The launch window's colour has to be the background Compose then draws, or the splash flashes. */
class WindowColorsTest {
private fun fallbackIn(qualifier: String): Int {
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(File("src/main/res/$qualifier/colors.xml"))
val colors = doc.getElementsByTagName("color")
val hex = (0 until colors.length).map { colors.item(it) }
.single { it.attributes.getNamedItem("name").nodeValue == "window_background_fallback" }
.textContent.trim().removePrefix("#")
return hex.toLong(16).toInt()
}
@Test
fun `the window matches the fallback schemes`() {
assertThat(fallbackIn("values")).isEqualTo(AgendulaLightFallback.background.toArgb())
assertThat(fallbackIn("values-night")).isEqualTo(AgendulaDarkFallback.background.toArgb())
}
}
@@ -171,13 +171,6 @@ class WidgetDataTest {
assertThat(sections.filterIsInstance<WidgetSection.TaskItem>().map { it.row.taskId }).containsExactly(1L, 2L, 3L).inOrder()
}
@Test
fun `the Today list is one Today bucket whatever the rows' due days`() {
val rows = listOf(row(1, dueKind = DueKind.TODAY), row(2, dueKind = DueKind.LATER), row(3, dueKind = null))
val sections = widgetSections(rows, groupByList = false, listNames = emptyMap(), todayList = true) { it?.name ?: "none" }
assertThat(sections.filterIsInstance<WidgetSection.Header>().map { it.key }).containsExactly("due:TODAY")
}
@Test
fun `grouping by list keeps the widgetRows order and headers each run once`() {
val rows = listOf(row(1, listId = 1), row(2, listId = 1), row(3, listId = 2))
@@ -47,37 +47,6 @@ class WidgetStatsTest {
assertThat(remaining).isEqualTo(2)
}
@Test
fun `a task starting today counts as remaining`() {
val open = listOf(
testTask(id = 1, start = at("2026-07-20T16:00:00")),
testTask(id = 2, start = at("2026-07-20T09:00:00"), due = at("2026-07-23T09:00:00")),
testTask(id = 3, start = at("2026-07-21T09:00:00")),
)
val (_, remaining, _) = todayProgress(open, emptyList(), comparator, now, today, newYork)
assertThat(remaining).isEqualTo(2)
}
@Test
fun `a task due today but completed yesterday is not done today`() {
val completed = listOf(
testTask(id = 1, status = TaskStatus.COMPLETED, due = at("2026-07-20T18:00:00"), completedAt = at("2026-07-19T20:00:00")),
)
val (done, _, _) = todayProgress(emptyList(), completed, comparator, now, today, newYork)
assertThat(done).isEqualTo(0)
}
@Test
fun `the week's today holds a task starting today, and not again on its due day`() {
val open = listOf(
testTask(id = 1, start = at("2026-07-20T09:00:00"), due = at("2026-07-22T09:00:00")),
testTask(id = 2, start = at("2026-07-20T16:00:00")),
)
val week = weekDays(open, comparator, now, today, newYork)
assertThat(week[0].count).isEqualTo(2)
assertThat(week[2].count).isEqualTo(0)
}
@Test
fun `an earlier-due remaining task sorts ahead of a later one`() {
val open = listOf(
@@ -1,13 +0,0 @@
### جديد
- خوادم ذاتية الاستضافة بشهادة موقّعة ذاتيًا أو عبر http.
- تسمية الحسابات، والسحب للمزامنة، وطيّ أقسام القائمة.
- سمة سوداء خالصة لشاشات AMOLED.
- يمكن أن يظهر Agendula باسم "Tasks" في المشغّل.
- تعرض التذكيرات وصف المهمة.
### تغييرات
- يعرض "اليوم" أيضًا المهام التي تبدأ اليوم ويُبقي ما أُنجز اليوم.
### إصلاحات
- إنجاز المهمة من أي مكان يلغي تذكيرها.
- شاشة البداية تتبع سمة التطبيق.
@@ -1,13 +0,0 @@
### Nové
- Vlastní servery s certifikátem podepsaným sebou samým nebo přes http.
- Pojmenování účtů, synchronizace tažením, sbalitelné sekce seznamu.
- Čistě černý motiv pro displeje AMOLED.
- Agendula se může v launcheru jmenovat „Tasks“.
- Připomínky zobrazují popis úkolu.
### Změny
- Dnes ukazuje i úkoly začínající dnes a ponechá dnes dokončené.
### Opravy
- Dokončení úkolu kdekoli zruší jeho připomínku.
- Úvodní obrazovka sleduje motiv aplikace.
@@ -1,13 +0,0 @@
### Neu
- Eigene Server mit selbstsigniertem Zertifikat oder über http.
- Konten benennen, zum Synchronisieren ziehen, Listenabschnitte einklappen.
- Ein reines Schwarz-Design für AMOLED-Displays.
- Agendula kann im Launcher als „Tasks“ erscheinen.
- Erinnerungen zeigen die Beschreibung der Aufgabe.
### Geändert
- Heute zeigt auch heute beginnende und heute erledigte Aufgaben.
### Behoben
- Abhaken, egal wo, beendet die Erinnerung der Aufgabe.
- Der Startbildschirm folgt dem App-Design.
@@ -1,13 +0,0 @@
### Added
- Self-hosted servers with a self-signed certificate or plain http.
- Name your accounts, pull to sync and collapse list sections.
- A pure black theme for AMOLED screens.
- Agendula can show up as "Tasks" in your launcher.
- Reminders show the task's description.
### Changed
- Today also lists tasks starting today and keeps today's completions.
### Fixed
- Completing a task anywhere cancels its reminder.
- The splash screen follows the app theme.
@@ -1,13 +0,0 @@
### Added
- Self-hosted servers with a self-signed certificate or plain http.
- Name your accounts, pull to sync and collapse list sections.
- A pure black theme for AMOLED screens.
- Agendula can show up as "Tasks" in your launcher.
- Reminders show the task's description.
### Changed
- Today also lists tasks starting today and keeps today's completions.
### Fixed
- Completing a task anywhere cancels its reminder.
- The splash screen follows the app theme.
@@ -1,13 +0,0 @@
### Nouveautés
- Serveurs perso avec certificat auto-signé ou en http.
- Nommer les comptes, tirer pour synchroniser, replier les sections.
- Un thème noir pur pour les écrans AMOLED.
- Agendula peut s'afficher « Tasks » dans le lanceur.
- Les rappels affichent la description de la tâche.
### Modifications
- Aujourd'hui montre aussi les tâches débutées ou finies du jour.
### Corrections
- Cocher une tâche, où que ce soit, annule son rappel.
- L'écran de démarrage suit le thème de l'appli.
@@ -1,13 +0,0 @@
### Novidades
- Servidores próprios com certificado autoassinado ou via http.
- Dê nome às contas, puxe para sincronizar e recolha seções da lista.
- Um tema preto puro para telas AMOLED.
- O Agendula pode aparecer como "Tasks" no launcher.
- Lembretes mostram a descrição da tarefa.
### Alterações
- Hoje também lista tarefas que começam hoje e mantém as concluídas hoje.
### Correções
- Concluir uma tarefa em qualquer lugar cancela o lembrete.
- A tela de abertura segue o tema do app.
@@ -1,13 +0,0 @@
### Новое
- Свои серверы с самоподписанным сертификатом или по http.
- Имена для аккаунтов, синхронизация жестом вниз, сворачиваемые разделы списка.
- Чисто чёрная тема для AMOLED-экранов.
- Agendula может называться «Tasks» в лаунчере.
- Напоминания показывают описание задачи.
### Изменения
- «Сегодня» показывает и задачи, начинающиеся сегодня, и сохраняет выполненные сегодня.
### Исправления
- Выполнение задачи в любом месте отменяет её напоминание.
- Заставка следует теме приложения.
@@ -1,11 +0,0 @@
Agendula — современное приложение для задач с открытым исходным кодом для Android.
Синхронизируйте задачи через CalDAV: добавьте Nextcloud, Radicale, Baïkal или любой другой CalDAV-аккаунт и выберите, какие списки задач подключить. Или храните всё на телефоне — Agendula сама хранит ваши задачи, без аккаунта и без других приложений.
Уже пользуетесь OpenTasks или tasks.org? Подключите Agendula к этому приложению, и задачи, синхронизированные через DAVx5 и подобные, появятся как есть. В любой момент их можно скопировать в собственное хранилище Agendula: Настройки → Хранилище.
Что умеет: списки, управляемые в приложении, даты начала и срока, задачи на весь день, подзадачи, приоритеты, прогресс, повторяющиеся задачи с правкой отдельных повторений, несколько напоминаний на задачу с откладыванием, импорт и экспорт iCalendar, виджет на главный экран, ярлыки в лаунчере и плитка быстрых настроек.
Дизайн — настоящий Material 3 Expressive: динамические цвета, выразительная анимация и формы.
Конфиденциальность: без телеметрии, аналитики и рекламы. Agendula выходит в сеть только для синхронизации с добавленными вами CalDAV-аккаунтами. Без них ваши задачи не покидают устройство, если вы их не экспортируете.
@@ -1 +0,0 @@
Приложение задач в стиле Material 3 Expressive с синхронизацией CalDAV.
@@ -1 +0,0 @@
Agendula: Tasks