Compare commits

...

38 Commits
14.8.4 ... main

Author SHA1 Message Date
Clio Germanou 6361145925 Translated using Weblate (Greek)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/el/
2 days ago
Clio Germanou 9c53287aa3 Translated using Weblate (Greek)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/el/
2 days ago
Alex Baker 1e1fc71738 Update version and changelog 4 days ago
Alex Baker 6f3767399d Fix google task queries
Ensure sync metadata matches list
5 days ago
Alex Baker 50c5cd5049 Remove LinkedResource from Microsoft To Do 5 days ago
Alex Baker 2f55ede9ef Don't update widget if appWidgetIds is empty 5 days ago
Alex Baker cdc4febe6f Update widget to use RemoteCollectionItems 5 days ago
Fahed Oudeh 29a4210fae Translated using Weblate (Arabic)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ar/
1 week ago
Alex Baker e46ecfa44e Attempt to fix showing keyboard for new tasks 2 weeks ago
Alex Baker 098ade6111 Update version and changelog 2 weeks ago
Alex Baker 297a7525c6 Delete microsoft task when update returns HTTP 404 2 weeks ago
Alex Baker c0962fc832 Fix lint errors 2 weeks ago
Alex Baker 431fdd6e95 Make appWidgetManager lazy 2 weeks ago
Alex Baker a8e6e43811 compose-bom 2025.12.01 2 weeks ago
Alex Baker 7628fe9ad3 Kotlin 2.2.20 2 weeks ago
Alex Baker ac19d1977e Fix opening keyboard for new tasks 2 weeks ago
Alex Baker 2b27c43188 New SystemEventReceiver 2 weeks ago
Alex Baker 2d29672198 Catch interruption during widget refresh 2 weeks ago
dependabot[bot] 22f63feede
Bump rexml from 3.4.1 to 3.4.2 (#3898)
Bumps [rexml](https://github.com/ruby/rexml) from 3.4.1 to 3.4.2.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.4.1...v3.4.2)

---
updated-dependencies:
- dependency-name: rexml
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 weeks ago
dependabot[bot] 583aaac767
Bump aws-sdk-s3 from 1.191.0 to 1.208.0 (#4040)
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.191.0 to 1.208.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-version: 1.208.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 weeks ago
Paulo e2486fcb0c Translated using Weblate (Portuguese)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt/
2 weeks ago
Alex Baker 62622b5979 Fix more widget icon mappings 2 weeks ago
Alex Baker 7a8aca0dbb Add optional limit to queries 2 weeks ago
Alex Baker 14530aa7a6 Disable Iconics animation processors to avoid ANRs 2 weeks ago
Alex Baker f1019a24fc Report missing widget icons 2 weeks ago
Alex Baker f0c57a0287 Fix widget icon mappings 2 weeks ago
Alex Baker 533aca1ac5 Fix crash in SignInActivity 3 weeks ago
ngocanhtve 50489708b5 Translated using Weblate (Vietnamese)
Currently translated at 92.6% (608 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/vi/
3 weeks ago
ngocanhtve 93db824b99 Translated using Weblate (Vietnamese)
Currently translated at 87.8% (29 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/vi/
3 weeks ago
Alex Baker d1dd5c1d99 Remove What's New dialog 3 weeks ago
Alex Baker 1ac39cc6bf Restore task edit state if activity is lost 3 weeks ago
Alex Baker fc38b8e676 Preserve timezone when modifying DateTime 3 weeks ago
Alex Baker ef0c1ac981 Handle timezone when calculating start of day 3 weeks ago
Alex Baker 46412a92c4 Handle loops when calculating subtask count 3 weeks ago
renovate[bot] ee10f1b62e
Update dependency com.google.apis:google-api-services-drive to v3-rev20251210-2.0.0 (#4007)
* Update dependency com.google.apis:google-api-services-drive to v3-rev20251210-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 1affa8264f
Update dependency androidx.datastore:datastore-preferences to v1.2.0 (#4055)
* Update dependency androidx.datastore:datastore-preferences to v1.2.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
Alex Baker 3ba6d7a384 Merge branch '14.8.4' 4 weeks ago
renovate[bot] d2e28e3807
Update dependency androidx.activity:activity-compose to v1.12.2 (#4052)
* Update dependency androidx.activity:activity-compose to v1.12.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago

@ -1,3 +1,18 @@
### 14.8.5 (2026-01-09)
* Widget performance improvements
* What's New now opens changelog on GitHub
* Fix automatically opening keyboard for new tasks [#4035](https://github.com/tasks/tasks/issues/4035)
* Fix parent-child cycle causing crash [#4065](https://github.com/tasks/tasks/issues/4065)
* Fix state restoration issue [#4025](https://github.com/tasks/tasks/issues/4025)
* Fix all day calendar entries created on previous day
* Fix Microsoft To Do and Google Task sync errors
* Fix multiple icons missing from widget
* Update translations
* Arabic - @fahedoudeh
* Portuguese - Paulo
* Vietnamese - @ngocanhtve
### 14.8.4 (2025-12-20)
* Fix flashing widgets [#3902](https://github.com/tasks/tasks/issues/3902)

@ -11,25 +11,27 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1122.0)
aws-sdk-core (3.226.1)
aws-partitions (1.1196.0)
aws-sdk-core (3.240.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.106.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sdk-kms (1.118.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.191.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sdk-s3 (1.208.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
bigdecimal (4.0.1)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
@ -180,7 +182,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.1)
rexml (3.4.2)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)

@ -181,6 +181,7 @@ dependencies {
implementation(libs.androidx.hilt.navigation)
implementation(libs.androidx.hilt.work)
implementation(libs.androidx.core.remoteviews)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.datastore)
implementation(libs.androidx.fragment.compose)

@ -3,17 +3,13 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.Task
import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase
@ -24,29 +20,15 @@ import javax.inject.Inject
@HiltAndroidTest
class OfflineSubtaskTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = runBlocking { MyTasksFilter.create() }
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
override fun getTaskCount() = tasks.size
}
@Before
override fun setUp() {
super.setUp()
preferences.clear()
tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover)
adapter.setDataSource(dataSource)
}
@Test
@ -54,7 +36,7 @@ class OfflineSubtaskTest : InjectingTestCase() {
val parent = addTask()
val child = addTask(with(PARENT, parent))
query()
val tasks = query()
assertEquals(child, tasks[1].id)
assertEquals(parent, tasks[1].parent)
@ -67,20 +49,81 @@ class OfflineSubtaskTest : InjectingTestCase() {
val parent = addTask(with(PARENT, grandparent))
val child = addTask(with(PARENT, parent))
query()
val tasks = query()
assertEquals(child, tasks[2].id)
assertEquals(parent, tasks[2].parent)
assertEquals(2, tasks[2].indent)
}
@Test
fun parentWithOneChildHasChildrenCountOne() {
val parent = addTask()
addTask(with(PARENT, parent))
val tasks = query()
val parentTask = tasks.find { it.id == parent }!!
assertEquals(1, parentTask.children)
}
@Test
fun parentWithMultipleChildrenHasCorrectCount() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
val tasks = query()
val parentTask = tasks.find { it.id == parent }!!
assertEquals(3, parentTask.children)
}
@Test
fun grandparentCountsAllDescendants() {
val grandparent = addTask()
val parent = addTask(with(PARENT, grandparent))
addTask(with(PARENT, parent))
val tasks = query()
val grandparentTask = tasks.find { it.id == grandparent }!!
assertEquals(2, grandparentTask.children)
}
@Test
fun leafTaskHasNoChildren() {
val parent = addTask()
val child = addTask(with(PARENT, parent))
val tasks = query()
val childTask = tasks.find { it.id == child }!!
assertEquals(0, childTask.children)
}
@Test
fun deepHierarchyCountsAllDescendants() {
val root = addTask()
val level1 = addTask(with(PARENT, root))
val level2 = addTask(with(PARENT, level1))
val level3 = addTask(with(PARENT, level2))
addTask(with(PARENT, level3))
val tasks = query()
val rootTask = tasks.find { it.id == root }!!
assertEquals(4, rootTask.children)
}
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking {
val task = newTask(*properties)
taskDao.createNew(task)
task.id
}
private fun query() = runBlocking {
tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter)))
private fun query(): List<TaskContainer> = runBlocking {
taskDao.fetchTasks(getQuery(preferences, filter))
}
}
}

@ -68,6 +68,17 @@ class RecursiveLoopTest : InjectingTestCase() {
assertEquals(grandchild, tasks[2].id)
}
@Test
fun descendantsRecursiveLoopBothMatchFilter() = runBlocking {
val parent = addTask(with(DUE_DATE, newDateTime()))
val child = addTask(with(DUE_DATE, newDateTime()), with(PARENT, parent))
taskDao.setParent(child, listOf(parent))
val tasks = getTasks()
assertEquals(2, tasks.size)
}
private suspend fun getTasks() = taskDao.fetchTasks(
getQuery(preferences, TodayFilter.create())
)

@ -0,0 +1,142 @@
package com.todoroo.astrid.gcal
import android.Manifest
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.provider.CalendarContract
import android.provider.CalendarContract.Calendars
import android.provider.CalendarContract.Events
import androidx.core.net.toUri
import androidx.test.core.app.ApplicationProvider
import androidx.test.rule.GrantPermissionRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.tasks.TestUtilities.withTZ
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTime
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidTest
class GCalHelperTest : InjectingTestCase() {
@get:Rule
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
)
@Inject lateinit var gcalHelper: GCalHelper
private var testCalendarId: Long = -1
@Before
override fun setUp() {
super.setUp()
testCalendarId = createTestCalendar()
}
@After
fun tearDown() {
if (testCalendarId > 0) {
try {
val context = ApplicationProvider.getApplicationContext<Context>()
context.contentResolver.delete(
ContentUris.withAppendedId(Calendars.CONTENT_URI, testCalendarId),
null,
null
)
} catch (e: Exception) {
Timber.e(e)
}
}
}
@Test fun allDayEventInNewYork() = assertAllDayEvent("America/New_York") // UTC-5
@Test fun allDayEventInBerlin() = assertAllDayEvent("Europe/Berlin") // UTC+1
@Test fun allDayEventInAuckland() = assertAllDayEvent("Pacific/Auckland") // UTC+13
@Test fun allDayEventInTokyo() = assertAllDayEvent("Asia/Tokyo") // UTC+9
@Test fun allDayEventInHonolulu() = assertAllDayEvent("Pacific/Honolulu") // UTC-10
@Test fun allDayEventInChatham() = assertAllDayEvent("Pacific/Chatham") // UTC+13:45
private fun assertAllDayEvent(timezone: String) = withTZ(timezone) {
val task = Task(dueDate = DateTime(2024, 12, 20).millis)
val eventUri = gcalHelper.createTaskEvent(task, testCalendarId.toString())
?: throw RuntimeException("Event not created")
val event = queryEvent(eventUri.toString()) ?: throw RuntimeException("Event not found")
assertEquals(
"DTSTART should be Dec 20 00:00 UTC",
DateTime(2024, 12, 20, timeZone = DateTime.UTC).millis,
event.dtStart
)
assertEquals(
"DTEND should be Dec 21 00:00 UTC",
DateTime(2024, 12, 21, timeZone = DateTime.UTC).millis,
event.dtEnd
)
}
private fun createTestCalendar(): Long {
val context = ApplicationProvider.getApplicationContext<Context>()
val values = ContentValues().apply {
put(Calendars.ACCOUNT_NAME, "test@test.com")
put(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
put(Calendars.NAME, "Test Calendar")
put(Calendars.CALENDAR_DISPLAY_NAME, "Test Calendar")
put(Calendars.CALENDAR_COLOR, 0xFF0000)
put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
put(Calendars.OWNER_ACCOUNT, "test@test.com")
put(Calendars.VISIBLE, 1)
put(Calendars.SYNC_EVENTS, 1)
}
val uri = Calendars.CONTENT_URI.buildUpon()
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(Calendars.ACCOUNT_NAME, "test@test.com")
.appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
.build()
val calendarUri = context.contentResolver.insert(uri, values)
return ContentUris.parseId(calendarUri!!)
}
private fun queryEvent(eventUri: String): CalendarEvent? {
val context = ApplicationProvider.getApplicationContext<Context>()
val cursor = context.contentResolver.query(
eventUri.toUri(),
arrayOf(
Events.DTSTART,
Events.DTEND,
Events.ALL_DAY,
Events.EVENT_TIMEZONE
),
null,
null,
null
)
return cursor?.use {
if (it.moveToFirst()) {
CalendarEvent(
dtStart = it.getLong(0),
dtEnd = it.getLong(1),
allDay = it.getInt(2) == 1,
timezone = it.getString(3)
)
} else null
}
}
private data class CalendarEvent(
val dtStart: Long,
val dtEnd: Long,
val allDay: Boolean,
val timezone: String?
)
}

@ -96,13 +96,13 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Test
fun getTaskFromRemoteId() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals(1L, googleTaskDao.getTask("1234"))
assertEquals(1L, googleTaskDao.getTask("1234", "calendar"))
}
@Test
fun getRemoteIdForTask() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals("1234", googleTaskDao.getRemoteId(1L))
assertEquals("1234", googleTaskDao.getRemoteId(1L, "calendar"))
}
@Test
@ -256,7 +256,7 @@ class GoogleTaskDaoTests : InjectingTestCase() {
}
private suspend fun getOrder(remoteId: String): Long? {
return taskDao.fetch(googleTaskDao.getByRemoteId(remoteId)!!.task)?.order
return taskDao.fetch(googleTaskDao.getByRemoteId(remoteId, "calendar")!!.task)?.order
}
private suspend fun insertTop(googleTask: CaldavTask) {
@ -278,6 +278,6 @@ class GoogleTaskDaoTests : InjectingTestCase() {
}
private suspend fun getByRemoteId(remoteId: String): CaldavTask {
return googleTaskDao.getByRemoteId(remoteId)!!
return googleTaskDao.getByRemoteId(remoteId, "calendar")!!
}
}

@ -340,8 +340,9 @@
<!-- ======================================================== Services = -->
<service
android:name=".widget.TasksWidgetAdapter"
android:permission="android.permission.BIND_REMOTEVIEWS"/>
android:name="androidx.core.widget.RemoteViewsCompatService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false"/>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
@ -363,6 +364,18 @@
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
<meta-data
android:name="com.mikepenz.iconics.animation.SpinProcessor"
android:value="androidx.startup"
tools:node="remove" />
<meta-data
android:name="com.mikepenz.iconics.animation.BlinkAlphaProcessor"
android:value="androidx.startup"
tools:node="remove" />
<meta-data
android:name="com.mikepenz.iconics.animation.BlinkScaleProcessor"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<provider
@ -493,10 +506,11 @@
</activity>
<receiver
android:name=".receivers.BootCompletedReceiver"
android:name=".receivers.SystemEventReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.USER_PRESENT"/>
</intent-filter>
</receiver>

@ -1 +0,0 @@
../../../../CHANGELOG.md

@ -50,7 +50,7 @@ import javax.inject.Inject
@HiltViewModel
class MainActivityViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val savedStateHandle: SavedStateHandle,
private val defaultFilterProvider: DefaultFilterProvider,
private val filterProvider: FilterProvider,
private val taskDao: TaskDao,
@ -81,10 +81,15 @@ class MainActivityViewModel @Inject constructor(
}
?: runBlocking { defaultFilterProvider.getStartupFilter() },
begForMoney = if (IS_GENERIC) !inventory.hasTasksAccount else !inventory.hasPro,
task = savedStateHandle.get<Task>(EXTRA_TASK),
)
)
val state = _state.asStateFlow()
companion object {
private const val EXTRA_TASK = "extra_task"
}
val accountExists: Flow<Boolean>
get() = caldavDao.watchAccountExists()
@ -107,6 +112,7 @@ class MainActivityViewModel @Inject constructor(
if (filter == _state.value.filter && task == null) {
return
}
savedStateHandle[EXTRA_TASK] = task
_state.update {
it.copy(
filter = filter,
@ -226,6 +232,7 @@ class MainActivityViewModel @Inject constructor(
}
fun setTask(task: Task?) {
savedStateHandle[EXTRA_TASK] = task
_state.update { it.copy(task = task) }
}

@ -112,7 +112,6 @@ import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.PriorityPicker.Companion.newPriorityPicker
import org.tasks.dialogs.SortSettingsActivity
import org.tasks.dialogs.WhatsNewDialog
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.extensions.Context.openAppNotificationSettings
import org.tasks.extensions.Context.openReminderSettings
@ -528,10 +527,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
Banner.AppUpdated ->
AppUpdatedBanner(
whatsNew = {
val fragmentManager = parentFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_WHATS_NEW) == null) {
WhatsNewDialog().show(parentFragmentManager, FRAG_TAG_WHATS_NEW)
}
context.openUri(R.string.url_changelog)
listViewModel.dismissBanner()
},
dismiss = { listViewModel.dismissBanner() },
@ -1221,7 +1217,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
const val EXTRA_FILTER = "extra_filter"
private const val FRAG_TAG_DATE_TIME_PICKER = "frag_tag_date_time_picker"
private const val FRAG_TAG_PRIORITY_PICKER = "frag_tag_priority_picker"
private const val FRAG_TAG_WHATS_NEW = "frag_tag_whats_new"
private const val REQUEST_TAG_TASKS = 10106
}
}

@ -151,7 +151,7 @@ class GCalHelper @Inject constructor(
values.put(CalendarContract.Events.ALL_DAY, "0")
values.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
} else {
val utcMidnight = DateTime(dueDate).toUTC().startOfDay()
val utcMidnight = DateTime(dueDate).startOfDay(DateTime.UTC)
values.put(CalendarContract.Events.DTSTART, utcMidnight.millis)
values.put(CalendarContract.Events.DTEND, utcMidnight.plusDays(1).millis)
values.put(CalendarContract.Events.ALL_DAY, "1")

@ -1,9 +1,10 @@
package com.todoroo.astrid.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.fragment.app.viewModels
@ -113,11 +114,14 @@ class StartDateControlSet : TaskEditControlFragment() {
private const val REQUEST_START_DATE = 11011
private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker"
internal fun Context.getRelativeDateString(resId: Int, time: Int) =
if (time == NO_TIME) {
getString(resId)
@Composable
internal fun getRelativeDateString(resId: Int, time: Int): String {
val label = stringResource(resId)
return if (time == NO_TIME) {
label
} else {
"${getString(resId)} ${getTimeString(currentTimeMillis().withMillisOfDay(time), this.is24HourFormat)}"
"$label ${getTimeString(currentTimeMillis().withMillisOfDay(time), LocalContext.current.is24HourFormat)}"
}
}
}
}

@ -71,7 +71,7 @@ class LocalBroadcastManager @Inject constructor(
}
fun reconfigureWidget(appWidgetId: Int) {
appWidgetManager.reconfigureWidgets(appWidgetId)
appWidgetManager.rebuildWidgets(appWidgetId)
}
companion object {

@ -6,7 +6,6 @@ import android.app.ApplicationExitInfo
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
@ -42,7 +41,6 @@ import org.tasks.scheduling.NotificationSchedulerIntentService
import org.tasks.sync.SyncAdapters
import org.tasks.themes.ThemeBase
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.widget.AppWidgetManager
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -58,7 +56,6 @@ class TasksApplication : Application(), Configuration.Provider {
@Inject lateinit var upgrader: Lazy<Upgrader>
@Inject lateinit var workManager: Lazy<WorkManager>
@Inject lateinit var geofenceApi: Lazy<GeofenceApi>
@Inject lateinit var appWidgetManager: Lazy<AppWidgetManager>
@Inject lateinit var workerFactory: HiltWorkerFactory
@Inject lateinit var contentObserver: Lazy<OpenTaskContentObserver>
@Inject lateinit var syncAdapters: Lazy<SyncAdapters>
@ -139,7 +136,6 @@ class TasksApplication : Application(), Configuration.Provider {
}
OpenTaskContentObserver.registerObserver(context, contentObserver.get())
geofenceApi.get().registerAll()
appWidgetManager.get().reconfigureWidgets()
CaldavSynchronizer.registerFactories()
}

@ -265,6 +265,9 @@ class SignInActivity : ComponentActivity() {
private fun handleConfigurationRetrievalResult(
config: AuthorizationServiceConfiguration?,
ex: AuthorizationException?) {
if (isFinishing || isDestroyed) {
return
}
if (config == null) {
returnError(ex ?: Exception("Failed to retrieve discovery document"))
return
@ -311,6 +314,9 @@ class SignInActivity : ComponentActivity() {
private fun handleRegistrationResponse(
response: RegistrationResponse?,
ex: AuthorizationException?) {
if (isFinishing || isDestroyed) {
return
}
authStateManager.updateAfterRegistration(response, ex)
if (response == null) {
runOnUiThread { returnError(ex ?: Exception("Failed to dynamically register client")) }

@ -9,6 +9,7 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import dagger.hilt.android.AndroidEntryPoint
@ -51,11 +52,12 @@ class PurchaseActivity : AppCompatActivity(), OnPurchasesUpdated {
skus = state.skus,
snackbarHostState = snackbarHostState,
)
val dismissLabel = stringResource(R.string.dismiss)
LaunchedEffect(state.error) {
if (state.error?.isNotBlank() == true) {
snackbarHostState.showSnackbar(
message = state.error,
actionLabel = context.getString(R.string.dismiss),
actionLabel = dismissLabel,
duration = SnackbarDuration.Long,
)
viewModel.dismissError()

@ -50,7 +50,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -309,7 +308,7 @@ object FilterCondition {
)
if (isExtended)
Text(
text = LocalContext.current.getString(R.string.CFA_button_add),
text = stringResource(R.string.CFA_button_add),
modifier = Modifier.padding(end = 16.dp)
)
}

@ -1,8 +1,9 @@
package org.tasks.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.pluralStringResource
import org.tasks.R
import org.tasks.themes.TasksIcons
import java.text.NumberFormat
@ -14,7 +15,7 @@ fun SubtaskChip(
compact: Boolean,
onClick: () -> Unit,
) {
val context = LocalContext.current
val chipColor = colorResource(R.color.default_chip_background).toArgb()
Chip(
icon = if (collapsed)
TasksIcons.KEYBOARD_ARROW_DOWN
@ -23,13 +24,11 @@ fun SubtaskChip(
name = if (compact)
NumberFormat.getInstance().format(children)
else
remember(children) {
context.resources.getQuantityString(R.plurals.subtask_count, children, children)
},
pluralStringResource(R.plurals.subtask_count, children, children),
theme = 0,
showText = true,
showIcon = true,
onClick = onClick,
colorProvider = { context.getColor(R.color.default_chip_background) },
colorProvider = { chipColor },
)
}

@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -102,14 +101,26 @@ fun EditTextView(
context.resources.getDimension(R.dimen.task_edit_text_size)
)
linkify?.linkify(this)
if (requestFocus) {
post { shouldRequestFocus = true }
}
}
},
update = { view ->
if (shouldRequestFocus) {
view.requestFocus()
val imm = context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
shouldRequestFocus = false
view.post {
fun tryFocus(attempts: Int = 3) {
if (view.requestFocus()) {
val imm = context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
} else if (attempts > 1) {
view.postDelayed({ tryFocus(attempts - 1) }, 50)
}
}
tryFocus()
}
}
view.paintFlags = if (strikethrough) {
view.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
@ -118,10 +129,4 @@ fun EditTextView(
}
},
)
LaunchedEffect(Unit) {
if (requestFocus) {
shouldRequestFocus = true
}
}
}
}

@ -8,7 +8,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -57,15 +56,14 @@ fun StartDate(
hasDueDate: Boolean,
printDate: () -> String,
) {
val context = LocalContext.current
Text(
text = when (selectedDay) {
StartDatePicker.DUE_DATE -> context.getRelativeDateString(R.string.due_date, selectedTime)
StartDatePicker.DUE_TIME -> context.getString(R.string.due_time)
StartDatePicker.DAY_BEFORE_DUE -> context.getRelativeDateString(R.string.day_before_due, selectedTime)
StartDatePicker.WEEK_BEFORE_DUE -> context.getRelativeDateString(R.string.week_before_due, selectedTime)
StartDatePicker.DUE_DATE -> getRelativeDateString(R.string.due_date, selectedTime)
StartDatePicker.DUE_TIME -> stringResource(R.string.due_time)
StartDatePicker.DAY_BEFORE_DUE -> getRelativeDateString(R.string.day_before_due, selectedTime)
StartDatePicker.WEEK_BEFORE_DUE -> getRelativeDateString(R.string.week_before_due, selectedTime)
in 1..Long.MAX_VALUE -> printDate()
else -> stringResource(id = R.string.no_start_date)
else -> stringResource(R.string.no_start_date)
},
color = when {
selectedDay < 0 && !hasDueDate -> MaterialTheme.colorScheme.error

@ -43,7 +43,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -138,17 +137,12 @@ fun CustomRecurrence(
number = state.interval,
onTextChanged = setInterval,
)
val context = LocalContext.current
val options by remember(state.interval, state.frequency) {
derivedStateOf {
state.frequencyOptions.map {
context.resources.getQuantityString(
it.plural,
state.interval,
state.interval,
)
}
}
val options = state.frequencyOptions.map {
pluralStringResource(
id = it.plural,
count = state.interval,
state.interval,
)
}
OutlinedSpinner(
text = pluralStringResource(
@ -279,36 +273,37 @@ private fun MonthlyPicker(
modifier = Modifier.padding(vertical = 16.dp),
color = border()
)
val context = LocalContext.current
val options = remember(dayNumber, dayOfWeek, nthWeek, isLastWeek, locale) {
val dayOfWeekDisplayName = remember(dayOfWeek, locale) {
dayOfWeek.getDisplayName(TextStyle.FULL, locale)
}
val onDayNumber = stringResource(R.string.repeat_monthly_on_day_number, dayNumber)
val nth = stringResource(
when (nthWeek - 1) {
0 -> R.string.repeat_monthly_first_week
1 -> R.string.repeat_monthly_second_week
2 -> R.string.repeat_monthly_third_week
3 -> R.string.repeat_monthly_fourth_week
4 -> R.string.repeat_monthly_fifth_week
else -> throw IllegalArgumentException()
}
)
val onNthWeekday = stringResource(
R.string.repeat_monthly_on_the_nth_weekday,
nth,
dayOfWeekDisplayName
)
val lastWeekString = stringResource(R.string.repeat_monthly_last_week)
val onLastWeekday = stringResource(
R.string.repeat_monthly_on_the_nth_weekday,
lastWeekString,
dayOfWeekDisplayName
)
val options = remember(onDayNumber, onNthWeekday, onLastWeekday, isLastWeek) {
ArrayList<String>().apply {
add(context.getString(R.string.repeat_monthly_on_day_number, dayNumber))
val nth = context.getString(
when (nthWeek - 1) {
0 -> R.string.repeat_monthly_first_week
1 -> R.string.repeat_monthly_second_week
2 -> R.string.repeat_monthly_third_week
3 -> R.string.repeat_monthly_fourth_week
4 -> R.string.repeat_monthly_fifth_week
else -> throw IllegalArgumentException()
}
)
val dayOfWeekDisplayName = dayOfWeek.getDisplayName(TextStyle.FULL, locale)
add(
context.getString(
R.string.repeat_monthly_on_the_nth_weekday,
nth,
dayOfWeekDisplayName
)
)
add(onDayNumber)
add(onNthWeekday)
if (isLastWeek) {
add(
context.getString(
R.string.repeat_monthly_on_the_nth_weekday,
context.getString(R.string.repeat_monthly_last_week),
dayOfWeekDisplayName
)
)
add(onLastWeekday)
}
}
}
@ -347,8 +342,7 @@ private fun EndsPicker(
RadioRow(selected = selection == 1, onClick = { setSelection(1) }) {
Text(text = stringResource(id = R.string.repeats_on))
Spacer(modifier = Modifier.width(8.dp))
val context = LocalContext.current
val endDateString by remember(context, endDate) {
val endDateString by remember(endDate) {
derivedStateOf {
runBlocking {
getRelativeDay(endDate)

@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.flask.colorpicker.ColorPickerView
@ -142,7 +143,7 @@ fun SelectColorRow(
},
center = {
Text(
text = LocalContext.current.getString(R.string.color),
text = stringResource(R.string.color),
modifier = Modifier.padding(start = Constants.KEYLINE_FIRST)
)
},

@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import org.tasks.R
import org.tasks.compose.Constants
@ -30,7 +31,7 @@ fun SelectIconRow(icon: String, selectIcon: () -> Unit) =
},
center = {
Text(
text = LocalContext.current.getString(R.string.icon),
text = stringResource(R.string.icon),
modifier = Modifier.padding(start = Constants.KEYLINE_FIRST)
)
}

@ -1,37 +0,0 @@
package org.tasks.dialogs
import android.app.Dialog
import android.os.Bundle
import android.text.method.LinkMovementMethod
import androidx.fragment.app.DialogFragment
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.databinding.DialogWhatsNewBinding
import org.tasks.markdown.MarkdownProvider
import java.io.BufferedReader
import javax.inject.Inject
@AndroidEntryPoint
class WhatsNewDialog : DialogFragment() {
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var markdownProvider: MarkdownProvider
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val binding = DialogWhatsNewBinding.inflate(layoutInflater)
val textStream = requireContext().assets.open("CHANGELOG.md")
val text = BufferedReader(textStream.reader()).readText()
binding.changelog.movementMethod = LinkMovementMethod.getInstance()
markdownProvider
.markdown(linkify = true, force = true)
.setMarkdown(binding.changelog, text)
binding.dismissButton.setOnClickListener {
dismiss()
}
return dialogBuilder.newDialog()
.setView(binding.root)
.show()
}
}

@ -2,12 +2,23 @@ package org.tasks.extensions
import android.graphics.Paint.ANTI_ALIAS_FLAG
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
import android.os.Parcel
import android.widget.RemoteViews
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.graphics.ColorUtils
import org.tasks.R
fun RemoteViews.estimateParcelSize(): Int {
val parcel = Parcel.obtain()
return try {
writeToParcel(parcel, 0)
parcel.dataSize()
} finally {
parcel.recycle()
}
}
fun RemoteViews.setColorFilter(viewId: Int, @ColorInt color: Int) =
setInt(viewId, "setColorFilter", color)

@ -247,7 +247,7 @@ class GoogleTaskSynchronizer @Inject constructor(
}
if (newlyCreated) {
val parent = task.parent
val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent) else null
val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent, listId) else null
val previous = googleTaskDao.getPrevious(
listId, if (isNullOrEmpty(localParent)) 0 else parent, task.order ?: 0)
val created: Task? = try {
@ -271,7 +271,7 @@ class GoogleTaskSynchronizer @Inject constructor(
if (!task.isDeleted && gtasksMetadata.isMoved) {
try {
val parent = task.parent
val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent) else null
val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent, listId) else null
val previous = googleTaskDao.getPrevious(
listId,
if (localParent.isNullOrBlank()) 0 else parent,
@ -359,12 +359,12 @@ class GoogleTaskSynchronizer @Inject constructor(
Collections.sort(tasks, PARENTS_FIRST)
for (gtask in tasks) {
val remoteId = gtask.id
var googleTask = googleTaskDao.getByRemoteId(remoteId)
var googleTask = googleTaskDao.getByRemoteId(remoteId, listId!!)
var task: org.tasks.data.entity.Task? = null
if (googleTask == null) {
googleTask = CaldavTask(
task = 0,
calendar = "",
calendar = listId,
remoteId = null,
)
} else if (googleTask.task > 0) {
@ -402,7 +402,6 @@ class GoogleTaskSynchronizer @Inject constructor(
val dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(gtask.due?.let(::DateTime))
mergeDates(createDueDate(org.tasks.data.entity.Task.URGENCY_SPECIFIC_DAY, dueDate), task)
task.notes = getTruncatedValue(task.notes, gtask.notes, MAX_DESCRIPTION_LENGTH)
googleTask.calendar = listId
if (task.title?.isNotBlank() == true || task.notes?.isNotBlank() == true) {
write(task, googleTask)
}
@ -417,7 +416,7 @@ class GoogleTaskSynchronizer @Inject constructor(
private suspend fun setOrderAndParent(googleTask: CaldavTask, task: Task, local: org.tasks.data.entity.Task) {
task.position?.toLongOrNull()?.let { googleTask.remoteOrder = it }
googleTask.remoteParent = task.parent?.takeIf { it.isNotBlank() }
local.parent = googleTask.remoteParent?.let { googleTaskDao.getTask(it) } ?: 0L
local.parent = googleTask.remoteParent?.let { googleTaskDao.getTask(it, googleTask.calendar!!) } ?: 0L
}
private suspend fun write(task: org.tasks.data.entity.Task, googleTask: CaldavTask) {

@ -4,7 +4,6 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.utility.Constants
import dagger.hilt.android.AndroidEntryPoint
@ -13,13 +12,10 @@ import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.TasksApplication.Companion.IS_GENERIC
import org.tasks.analytics.Firebase
import org.tasks.dialogs.WhatsNewDialog
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.logging.FileLogger
import javax.inject.Inject
private const val FRAG_TAG_WHATS_NEW = "frag_tag_whats_new"
@AndroidEntryPoint
class HelpAndFeedback : InjectingPreferenceFragment() {
@ -29,15 +25,9 @@ class HelpAndFeedback : InjectingPreferenceFragment() {
override fun getPreferenceXml() = R.xml.help_and_feedback
override suspend fun setupPreferences(savedInstanceState: Bundle?) {
val whatsNew = findPreference(R.string.whats_new)
whatsNew.summary = getString(R.string.version_string, BuildConfig.VERSION_NAME)
whatsNew.setOnPreferenceClickListener {
val fragmentManager: FragmentManager = parentFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_WHATS_NEW) == null) {
WhatsNewDialog().show(fragmentManager, FRAG_TAG_WHATS_NEW)
}
true
}
findPreference(R.string.whats_new).summary =
getString(R.string.version_string, BuildConfig.VERSION_NAME)
openUrl(R.string.whats_new, R.string.url_changelog)
findPreference(R.string.contact_developer)
.setOnPreferenceClickListener {

@ -1,18 +0,0 @@
package org.tasks.receivers;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import timber.log.Timber;
public class BootCompletedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
return;
}
Timber.d("onReceive(context, %s)", intent);
}
}

@ -0,0 +1,25 @@
package org.tasks.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.widget.AppWidgetManager
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class SystemEventReceiver : BroadcastReceiver() {
@Inject lateinit var appWidgetManager: AppWidgetManager
override fun onReceive(context: Context, intent: Intent) {
Timber.d("onReceive(context, %s)", intent)
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED,
Intent.ACTION_USER_PRESENT -> {
appWidgetManager.updateWidgets()
}
}
}
}

@ -227,7 +227,13 @@ class MicrosoftSynchronizer @Inject constructor(
microsoft.createTask(list.uuid!!, remoteTask)
} else {
Timber.d("Updating existing task: $task")
microsoft.updateTask(list.uuid!!, caldavTask.remoteId!!, remoteTask)
try {
microsoft.updateTask(list.uuid!!, caldavTask.remoteId!!, remoteTask)
} catch (e: NotFoundException) {
Timber.w(e, "Task deleted remotely, deleting locally: $task")
taskDeleter.delete(task.id)
return
}
}
caldavTask.remoteId = result.id
caldavTask.obj = "${result.id}.json"
@ -241,7 +247,13 @@ class MicrosoftSynchronizer @Inject constructor(
microsoft.createChecklistItem(list.uuid!!, caldavParent, remoteTask)
} else {
Timber.d("Updating existing checklist item: $task")
microsoft.updateChecklistItem(list.uuid!!, caldavParent, remoteTask)
try {
microsoft.updateChecklistItem(list.uuid!!, caldavParent, remoteTask)
} catch (e: NotFoundException) {
Timber.w(e, "Checklist item deleted remotely, deleting locally: $task")
taskDeleter.delete(task.id)
return
}
}
caldavTask.remoteId = result.id
caldavTask.remoteParent = caldavParent

@ -28,7 +28,6 @@ data class Tasks(
val lastModifiedDateTime: String? = null,
@EncodeDefault val completedDateTime: DateTime? = null,
@EncodeDefault val dueDateTime: DateTime? = null,
val linkedResources: List<LinkedResource>? = null,
@EncodeDefault val recurrence: Recurrence? = null,
@EncodeDefault val reminderDateTime: DateTime? = null,
val checklistItems: List<ChecklistItem>? = null,
@ -40,14 +39,6 @@ data class Tasks(
val contentType: String,
)
@Serializable
data class LinkedResource(
val applicationName: String,
val displayName: String?,
val externalId: String?,
val id: String,
)
@Serializable
data class Removed(
val reason: String,

@ -35,7 +35,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.TextStyle
@ -175,7 +176,6 @@ internal fun SearchBar(
onBack: () -> Unit
) {
val searchPattern = remember { viewModel.searchText }
val invitation = LocalContext.current.getString(R.string.enter_tag_name)
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
ImageVector.vectorResource(id = R.drawable.ic_outline_arrow_back_24px),
@ -191,7 +191,7 @@ internal fun SearchBar(
onValueChange = { viewModel.search(it) },
placeholder = {
Text(
text = invitation,
text = stringResource(R.string.enter_tag_name),
color = MaterialTheme.colorScheme.onSurface,
)
},
@ -232,11 +232,10 @@ internal fun PickerBox (
LazyColumn {
if (viewModel.tagToCreate.value != "") {
item(key = -1) {
val text = LocalContext.current.getString(R.string.new_tag) + " \"${viewModel.tagToCreate.value}\""
TagRow(
icon = TasksIcons.ADD,
iconColor = Color(LocalContext.current.getColor(R.color.icon_tint_with_alpha)),
text = text,
iconColor = colorResource(R.color.icon_tint_with_alpha),
text = "${stringResource(R.string.new_tag)} \"${viewModel.tagToCreate.value}\"",
onClick = { newItem(viewModel.searchText.value) }
)
}

@ -44,25 +44,60 @@ class DateTime {
private constructor(calendar: Calendar) : this(calendar.timeInMillis, calendar.timeZone)
fun startOfDay(): DateTime = DateTime(millis.startOfDay())
fun startOfMinute(): DateTime = DateTime(millis.startOfMinute())
fun startOfSecond(): DateTime = DateTime(millis.startOfSecond())
fun endOfMinute(): DateTime = DateTime(millis.endOfMinute())
fun noon(): DateTime = DateTime(millis.noon())
fun endOfDay(): DateTime = DateTime(millis.endOfDay())
fun withMillisOfDay(millisOfDay: Int): DateTime = DateTime(millis.withMillisOfDay(millisOfDay))
@JvmOverloads
fun startOfDay(timeZone: TimeZone = this.timeZone): DateTime = DateTime(
year = year,
month = monthOfYear,
day = dayOfMonth,
timeZone = timeZone
)
fun startOfMinute(): DateTime = DateTime(millis.startOfMinute(), timeZone)
fun startOfSecond(): DateTime = DateTime(millis.startOfSecond(), timeZone)
fun endOfMinute(): DateTime = DateTime(millis.endOfMinute(), timeZone)
fun noon(): DateTime = DateTime(
year = year,
month = monthOfYear,
day = dayOfMonth,
hour = 12,
timeZone = timeZone
)
fun endOfDay(): DateTime = DateTime(
year = year,
month = monthOfYear,
day = dayOfMonth,
hour = 23,
minute = 59,
second = 59,
timeZone = timeZone
)
fun withMillisOfDay(millisOfDay: Int): DateTime {
val hours = millisOfDay / 3600000
val minutes = (millisOfDay % 3600000) / 60000
val seconds = (millisOfDay % 60000) / 1000
val millis = millisOfDay % 1000
return DateTime(
year = year,
month = monthOfYear,
day = dayOfMonth,
hour = hours,
minute = minutes,
second = seconds,
millisecond = millis,
timeZone = timeZone
)
}
val offset: Long
get() = timeZone.getOffset(millis).toLong()
val millisOfDay: Int
get() = millis.millisOfDay
get() = hourOfDay * 3600000 + minuteOfHour * 60000 + secondOfMinute * 1000 + calendar[Calendar.MILLISECOND]
val year: Int
get() = calendar[Calendar.YEAR]
@ -145,11 +180,11 @@ class DateTime {
return add(Calendar.SECOND, -seconds)
}
fun minusDays(days: Int): DateTime = DateTime(millis.minusDays(days))
fun minusDays(days: Int): DateTime = add(Calendar.DATE, -days)
fun minusMinutes(minutes: Int): DateTime = DateTime(millis.minusMinutes(minutes))
fun minusMinutes(minutes: Int): DateTime = add(Calendar.MINUTE, -minutes)
fun minusMillis(millis: Long): DateTime = DateTime(this.millis.minusMillis(millis))
fun minusMillis(millis: Long): DateTime = DateTime(this.millis - millis, timeZone)
val isAfterNow: Boolean
get() = isAfter(currentTimeMillis())

@ -5,18 +5,17 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.fragment.compose.content
import androidx.hilt.navigation.compose.hiltViewModel
abstract class TaskEditControlFragment : Fragment() {
lateinit var viewModel: TaskEditViewModel
protected val viewModel: TaskEditViewModel by viewModels({ requireParentFragment() })
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = content {
viewModel = hiltViewModel<TaskEditViewModel>(viewModelStoreOwner = requireParentFragment())
Content()
}

@ -87,8 +87,8 @@ import javax.inject.Inject
@HiltViewModel
class TaskEditViewModel @Inject constructor(
@ApplicationContext private val context: Context,
savedStateHandle: SavedStateHandle,
@param:ApplicationContext private val context: Context,
private val savedStateHandle: SavedStateHandle,
private val taskDao: TaskDao,
private val taskDeleter: TaskDeleter,
private val timerPlugin: TimerPlugin,
@ -116,9 +116,24 @@ class TaskEditViewModel @Inject constructor(
private val resources = context.resources
private var cleared = false
private val task: Task = savedStateHandle.get<Task>(TaskEditFragment.EXTRA_TASK)
?.apply { notes = notes?.stripCarriageReturns() } // copying here broke tests 🙄
?: throw IllegalArgumentException("task is null")
private val task: Task = run {
val argTask = savedStateHandle.get<Task>(TaskEditFragment.EXTRA_TASK)
?: throw IllegalArgumentException("task is null")
val wasRecreated = savedStateHandle.contains(EXTRA_WAS_INIT)
.also { savedStateHandle[EXTRA_WAS_INIT] = true }
if (wasRecreated) {
runBlocking {
if (argTask.isNew) {
taskDao.fetch(argTask.uuid)
} else {
taskDao.fetch(argTask.id)
}
} ?: argTask
} else {
argTask
}
}.apply { notes = notes?.stripCarriageReturns() } // copying here broke tests 🙄
private val _originalState = MutableStateFlow(
TaskEditViewState(
@ -364,12 +379,11 @@ class TaskEditViewModel @Inject constructor(
taskDao.createNew(subtask)
alarmDao.insert(subtask.getDefaultAlarms())
firebase?.addTask("subtasks")
val filter = selectedList
when {
filter.isGoogleTasks -> {
selectedList.isGoogleTasks -> {
val googleTask = CaldavTask(
task = subtask.id,
calendar = filter.uuid,
calendar = selectedList.uuid,
remoteId = null,
)
subtask.parent = task.id
@ -595,7 +609,7 @@ class TaskEditViewModel @Inject constructor(
val attachments = async {
taskAttachmentDao
.getAttachments(task.id)
.filter { FileHelper.fileExists(context, Uri.parse(it.uri)) }
.filter { FileHelper.fileExists(context, it.uri.toUri()) }
.toPersistentSet()
}
val alarms = async {
@ -630,6 +644,8 @@ class TaskEditViewModel @Inject constructor(
}
companion object {
private const val EXTRA_WAS_INIT = "extra_was_init"
// one spark tasks for windows adds these
fun String?.stripCarriageReturns(): String? = this?.replace("\\r\\n?".toRegex(), "\n")

@ -10,38 +10,27 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.tasks.R
import org.tasks.compose.throttleLatest
import org.tasks.injection.ApplicationScope
import timber.log.Timber
import java.util.concurrent.atomic.AtomicLong
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AppWidgetManager @Inject constructor(
@param:ApplicationContext private val context: Context,
@ApplicationScope private val scope: CoroutineScope,
@param:ApplicationScope private val scope: CoroutineScope,
) {
private val appWidgetManager: AppWidgetManager? = AppWidgetManager.getInstance(context)
private val _generation = AtomicLong(0)
val generation: Long get() = _generation.get()
private val updateChannel = Channel<Long>(Channel.CONFLATED)
private val appWidgetManager: AppWidgetManager? by lazy {
AppWidgetManager.getInstance(context)
}
private val updateChannel = Channel<Unit>(Channel.CONFLATED)
init {
updateChannel
.consumeAsFlow()
.throttleLatest(1000)
.onEach { gen ->
if (gen == _generation.get()) {
val appWidgetIds = widgetIds
Timber.d("updateWidgets: ${appWidgetIds.joinToString { it.toString() }}")
appWidgetManager?.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.list_view)
} else {
Timber.d("Skipping stale widget update")
}
}
.onEach { rebuildWidgets(*widgetIds) }
.launchIn(scope)
}
@ -50,18 +39,19 @@ class AppWidgetManager @Inject constructor(
?.getAppWidgetIds(ComponentName(context, TasksWidget::class.java))
?: intArrayOf()
fun reconfigureWidgets(vararg appWidgetIds: Int) {
val newGeneration = _generation.incrementAndGet()
val ids = appWidgetIds.takeIf { it.isNotEmpty() } ?: widgetIds
Timber.d("reconfigureWidgets(${ids.joinToString()}) generation=$newGeneration")
fun rebuildWidgets(vararg appWidgetIds: Int) {
if (appWidgetIds.isEmpty()) {
return
}
Timber.d("rebuildWidgets(${appWidgetIds.joinToString()})")
val intent = Intent(context, TasksWidget::class.java)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
.apply { action = AppWidgetManager.ACTION_APPWIDGET_UPDATE }
context.sendBroadcast(intent)
}
fun updateWidgets() {
updateChannel.trySend(_generation.get())
updateChannel.trySend(Unit)
}
fun exists(id: Int) = appWidgetManager?.getAppWidgetInfo(id) != null

@ -24,7 +24,7 @@ class RequestPinWidgetReceiver : BroadcastReceiver() {
val widgetPreferences = WidgetPreferences(context, preferences, widgetId)
widgetPreferences.setFilter(filter)
widgetPreferences.setColor(color)
appWidgetManager.reconfigureWidgets(widgetId)
appWidgetManager.rebuildWidgets(widgetId)
}
companion object {

@ -6,23 +6,29 @@ import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.format.Formatter
import android.view.View
import android.widget.RemoteViews
import androidx.core.net.toUri
import androidx.core.widget.RemoteViewsCompat
import com.todoroo.andlib.utility.AndroidUtilities.atLeastS
import com.todoroo.astrid.activity.MainActivity.Companion.FINISH_AFFINITY
import com.todoroo.astrid.subtasks.SubtasksHelper
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.runBlocking
import org.tasks.R
import org.tasks.compose.FilterSelectionActivity
import org.tasks.data.dao.TaskDao
import org.tasks.extensions.estimateParcelSize
import org.tasks.extensions.setBackgroundColor
import org.tasks.extensions.setColorFilter
import org.tasks.extensions.setRipple
import org.tasks.filters.Filter
import org.tasks.intents.TaskIntents
import org.tasks.markdown.MarkdownProvider
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter
import org.tasks.themes.ThemeColor
import timber.log.Timber
import javax.inject.Inject
@ -32,15 +38,20 @@ class TasksWidget : AppWidgetProvider() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject @ApplicationContext lateinit var context: Context
@Inject lateinit var widgetManager: org.tasks.widget.AppWidgetManager
@Inject lateinit var subtasksHelper: SubtasksHelper
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var chipProvider: WidgetChipProvider
@Inject lateinit var markdownProvider: MarkdownProvider
@Inject lateinit var headerFormatter: HeaderFormatter
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
Timber.d("onUpdate appWidgetIds=${appWidgetIds.joinToString { it.toString() }}")
val generation = widgetManager.generation
appWidgetIds.forEach { id ->
try {
val options = appWidgetManager.getAppWidgetOptions(id)
appWidgetManager.updateAppWidget(id, createWidget(context, id, options, generation))
val remoteViews = createWidget(context, id, options)
Timber.d("Widget $id main layout: ${Formatter.formatShortFileSize(context, remoteViews.estimateParcelSize().toLong())}")
appWidgetManager.updateAppWidget(id, remoteViews)
} catch (e: Exception) {
Timber.e(e)
}
@ -54,24 +65,24 @@ class TasksWidget : AppWidgetProvider() {
newOptions: Bundle
) {
Timber.d("onAppWidgetOptionsChanged appWidgetId=$appWidgetId")
appWidgetManager.updateAppWidget(
appWidgetId,
createWidget(context, appWidgetId, newOptions, widgetManager.generation)
)
val remoteViews = createWidget(context, appWidgetId, newOptions)
Timber.d("Widget $appWidgetId main layout: ${Formatter.formatShortFileSize(context, remoteViews.estimateParcelSize().toLong())}")
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
private fun createWidget(context: Context, id: Int, options: Bundle, generation: Long): RemoteViews {
private fun createWidget(context: Context, id: Int, options: Bundle): RemoteViews {
val widgetPreferences = WidgetPreferences(context, preferences, id)
val settings = widgetPreferences.getWidgetHeaderSettings()
widgetPreferences.setCompact(
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) < COMPACT_MAX
)
val filterId = widgetPreferences.filterId
val filter = runBlocking {
defaultFilterProvider.getFilterFromPreference(widgetPreferences.filterId)
defaultFilterProvider.getFilterFromPreference(filterId)
}
Timber.d("createWidget id=$id generation=$generation filter=$filter")
Timber.d("createWidget id=$id filter=$filter")
return RemoteViews(context.packageName, R.layout.scrollable_widget).apply {
val remoteViews = RemoteViews(context.packageName, R.layout.scrollable_widget).apply {
if (settings.showHeader) {
setViewVisibility(R.id.widget_header, View.VISIBLE)
setupHeader(settings, filter, id)
@ -89,15 +100,30 @@ class TasksWidget : AppWidgetProvider() {
opacity = widgetPreferences.footerOpacity,
)
setOnClickPendingIntent(R.id.empty_view, getOpenListIntent(context, filter, id))
val cacheBuster = "tasks://widget/$id/$generation".toUri()
setRemoteAdapter(
R.id.list_view,
Intent(context, TasksWidgetAdapter::class.java)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
.setData(cacheBuster)
)
setPendingIntentTemplate(R.id.list_view, getPendingIntentTemplate(context))
}
val builder = TasksWidgetBuilder(
subtasksHelper = subtasksHelper,
widgetPreferences = widgetPreferences,
filter = filter,
context = context,
widgetId = id,
taskDao = taskDao,
chipProvider = chipProvider,
markdown = markdownProvider.markdown(false),
headerFormatter = headerFormatter,
)
val items = runBlocking { builder.buildItems() }
RemoteViewsCompat.setRemoteAdapter(
context = context,
remoteViews = remoteViews,
appWidgetId = id,
viewId = R.id.list_view,
items = items
)
return remoteViews
}
private fun RemoteViews.setupHeader(

@ -1,51 +0,0 @@
package org.tasks.widget
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.widget.RemoteViewsService
import com.todoroo.astrid.subtasks.SubtasksHelper
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.runBlocking
import org.tasks.data.dao.TaskDao
import org.tasks.markdown.MarkdownProvider
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TasksWidgetAdapter : RemoteViewsService() {
@ApplicationContext @Inject lateinit var context: Context
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var subtasksHelper: SubtasksHelper
@Inject lateinit var chipProvider: WidgetChipProvider
@Inject lateinit var markdownProvider: MarkdownProvider
@Inject lateinit var headerFormatter: HeaderFormatter
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory? {
val widgetId = intent.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return null
val widgetPreferences = WidgetPreferences(context, preferences, widgetId)
val filterId = widgetPreferences.filterId
val filter = runBlocking {
defaultFilterProvider.getFilterFromPreference(filterId)
}
Timber.d("onGetViewFactory $filter")
return TasksWidgetViewFactory(
subtasksHelper,
widgetPreferences,
filter,
filterId,
applicationContext,
widgetId,
taskDao,
chipProvider,
markdownProvider.markdown(false),
headerFormatter,
)
}
}

@ -2,10 +2,10 @@ package org.tasks.widget
import android.content.Context
import android.content.Intent
import android.text.format.Formatter
import android.view.View
import android.widget.RemoteViews
import android.widget.RemoteViewsService.RemoteViewsFactory
import com.todoroo.andlib.utility.AndroidUtilities.atLeastAndroid16
import androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems
import com.todoroo.astrid.core.SortHelper
import com.todoroo.astrid.subtasks.SubtasksHelper
import kotlinx.coroutines.runBlocking
@ -18,6 +18,7 @@ import org.tasks.data.hasNotes
import org.tasks.data.isHidden
import org.tasks.data.isOverdue
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.extensions.estimateParcelSize
import org.tasks.extensions.setBackgroundResource
import org.tasks.extensions.setColorFilter
import org.tasks.extensions.setMaxLines
@ -40,26 +41,23 @@ import org.tasks.ui.CheckBoxProvider.Companion.getCheckboxRes
import timber.log.Timber
import kotlin.math.max
internal class TasksWidgetViewFactory(
internal class TasksWidgetBuilder(
private val subtasksHelper: SubtasksHelper,
private val widgetPreferences: WidgetPreferences,
private val filter: Filter,
private val filterId: String?,
private val context: Context,
private val widgetId: Int,
private val taskDao: TaskDao,
private val chipProvider: WidgetChipProvider,
private val markdown: Markdown,
private val headerFormatter: HeaderFormatter,
) : RemoteViewsFactory {
private val taskLimit = if (atLeastAndroid16()) 50 + 1 else Int.MAX_VALUE
) {
private val indentPadding = (20 * context.resources.displayMetrics.density).toInt()
private val settings = widgetPreferences.getWidgetListSettings()
private val hPad = context.resources.getDimension(R.dimen.widget_padding).toInt()
private val disableGroups = !filter.supportsSorting()
|| (filter.supportsManualSort() && widgetPreferences.isManualSort)
|| (filter is AstridOrderingFilter && widgetPreferences.isAstridSort)
private var tasks = SectionedDataSource()
private val onSurface = context.getColor(if (settings.isDark) R.color.white_87 else R.color.black_87)
private val onSurfaceVariant = context.getColor(if (settings.isDark) R.color.white_60 else R.color.black_60)
@ -67,66 +65,6 @@ internal class TasksWidgetViewFactory(
chipProvider.isDark = settings.isDark
}
override fun onCreate() {
Timber.d("onCreate widgetId:$widgetId filter:$filter")
}
override fun onDataSetChanged() {
Timber.v("onDataSetChanged $filter")
if (widgetPreferences.filterId != filterId) {
Timber.d("Skipping stale factory: expected $filterId, current ${widgetPreferences.filterId}")
return
}
runBlocking {
val collapsed = widgetPreferences.collapsed
tasks = SectionedDataSource(
taskDao.fetchTasks(getQuery(filter)),
disableGroups,
settings.groupMode,
widgetPreferences.subtaskMode,
collapsed,
widgetPreferences.completedTasksAtBottom,
)
collapsed.toMutableSet().let {
if (it.retainAll(tasks.getSectionValues().toSet())) {
widgetPreferences.collapsed = it
}
}
}
}
override fun onDestroy() {
Timber.d("onDestroy widgetId:$widgetId")
}
override fun getCount() = tasks.size.coerceAtMost(taskLimit)
override fun getViewAt(position: Int): RemoteViews? = tasks.let {
when {
position == taskLimit - 1 && it.size > taskLimit -> buildFooter()
it.isHeader(position) -> buildHeader(it.getSection(position))
position < it.size -> buildUpdate(it.getItem(position))
else -> null
}
}
override fun getLoadingView(): RemoteViews = newRemoteView()
override fun getViewTypeCount(): Int = 3
override fun getItemId(position: Int) = tasks.let {
when {
position == taskLimit - 1 && it.size > taskLimit -> 0
it.isHeader(position) -> it.getSection(position).value
position < it.size -> it.getItem(position).id
else -> 0
}
}
override fun hasStableIds(): Boolean = true
private fun newRemoteView() = RemoteViews(BuildConfig.APPLICATION_ID, R.layout.widget_row)
private fun buildFooter(): RemoteViews {
return RemoteViews(BuildConfig.APPLICATION_ID, R.layout.widget_footer).apply {
setTextSize(R.id.widget_view_more, settings.textSize)
@ -192,7 +130,7 @@ internal class TasksWidgetViewFactory(
!settings.showDueDates && task.isOverdue -> context.getColor(R.color.overdue)
else -> onSurface
}
newRemoteView().apply {
RemoteViews(BuildConfig.APPLICATION_ID, R.layout.widget_row).apply {
strikethrough(R.id.widget_text, task.isCompleted)
setTextSize(R.id.widget_text, settings.textSize)
if (settings.showDueDates) {
@ -300,7 +238,7 @@ internal class TasksWidgetViewFactory(
private suspend fun getQuery(filter: Filter): String {
subtasksHelper.applySubtasksToWidgetFilter(filter, widgetPreferences)
return getQuery(widgetPreferences, filter)
return getQuery(widgetPreferences, filter, MAX_ITEMS)
}
private fun formatDueDate(row: RemoteViews, task: TaskContainer) = with(row) {
@ -349,4 +287,72 @@ internal class TasksWidgetViewFactory(
setViewVisibility(dueDateRes, View.GONE)
}
}
suspend fun buildItems(): RemoteCollectionItems {
var totalParcelSize = 0
val collapsed = widgetPreferences.collapsed
val tasks = SectionedDataSource(
taskDao.fetchTasks(getQuery(filter)),
disableGroups,
settings.groupMode,
widgetPreferences.subtaskMode,
collapsed,
widgetPreferences.completedTasksAtBottom,
)
collapsed.toMutableSet().let {
if (it.retainAll(tasks.getSectionValues().toSet())) {
widgetPreferences.collapsed = it
}
}
Timber.d("buildItems loaded ${tasks.size} items for widget $widgetId")
data class WidgetItem(val id: Long, val view: RemoteViews, val isHeader: Boolean)
val items = mutableListOf<WidgetItem>()
var truncatedDueToSize = false
for (position in 0 until tasks.size) {
val isHeader = tasks.isHeader(position)
val id = if (isHeader) {
tasks.getSection(position).value
} else {
tasks.getItem(position).id
}
val view = if (isHeader) {
buildHeader(tasks.getSection(position))
} else {
buildUpdate(tasks.getItem(position))
} ?: continue
val viewSize = view.estimateParcelSize()
if (totalParcelSize + viewSize > MAX_PARCEL_SIZE) {
Timber.d("Stopping at position $position due to size limit")
truncatedDueToSize = true
break
}
items.add(WidgetItem(id, view, isHeader))
totalParcelSize += viewSize
}
val builder = RemoteCollectionItems.Builder()
.setHasStableIds(true)
.setViewTypeCount(VIEW_TYPE_COUNT)
items.forEach { builder.addItem(it.id, it.view) }
if (truncatedDueToSize) {
builder.addItem(FOOTER_ID, buildFooter())
}
Timber.d("Built ${items.size} items, totalSize=${Formatter.formatShortFileSize(context, totalParcelSize.toLong())}, truncated=$truncatedDueToSize")
return builder.build()
}
companion object {
const val VIEW_TYPE_COUNT = 3
const val MAX_ITEMS = 100
const val MAX_PARCEL_SIZE = 200_000 // 200KB
const val FOOTER_ID = 0L
}
}

@ -11,7 +11,12 @@ import androidx.core.graphics.createBitmap
import androidx.core.net.toUri
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.sizeDp
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import org.tasks.BuildConfig
import org.tasks.analytics.Firebase
import org.tasks.icons.OutlinedGoogleMaterial
import timber.log.Timber
import java.io.File
@ -19,6 +24,12 @@ import java.io.FileOutputStream
class WidgetIconProvider : ContentProvider() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetIconProviderEntryPoint {
val firebase: Firebase
}
override fun onCreate() = true
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
@ -76,10 +87,17 @@ class WidgetIconProvider : ContentProvider() {
bitmap.recycle()
} catch (e: Exception) {
Timber.e(e, "Failed to generate icon: $iconName")
hilt().firebase.reportException(e)
file.delete()
}
}
private fun hilt() =
EntryPointAccessors.fromApplication(
context!!.applicationContext,
WidgetIconProviderEntryPoint::class.java
)
private fun getCacheFile(iconName: String): File {
val context = context ?: throw IllegalStateException("Context is null")
val cacheDir = File(context.cacheDir, "widget_icons")

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ScrollView
android:id="@+id/scrollView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/action_block">
<TextView
android:id="@+id/changelog"
style="@style/TextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.2"
android:padding="@dimen/keyline_second" />
</ScrollView>
<LinearLayout
android:id="@+id/action_block"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_alignParentBottom="true">
<View style="@style/horizontal_divider" />
<com.google.android.material.button.MaterialButton
android:id="@+id/dismiss_button"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/keyline_first"
android:layout_marginEnd="@dimen/keyline_first"
android:text="@string/got_it"
android:textColor="@color/text_secondary" />
</LinearLayout>
</RelativeLayout>

@ -564,7 +564,6 @@
<string name="android_auto_backup">خدمة النسخ الاحتياطي على Android</string>
<string name="permission_read_tasks">الوصول الكامل إلى قاعدة بيانات التطبيق</string>
<string name="reset_sort_order">إعادة تعيين ترتيب الفرز</string>
<string name="got_it">فهمتك!</string>
<string name="filter_any_due_date">أي تاريخ استحقاق</string>
<string name="filter_overdue">متأخر</string>
<string name="custom_filter_criteria">معايير التصفية</string>
@ -677,7 +676,7 @@
<string name="now">اﻵن</string>
<string name="TEA_creation_date">تاريخ الإنشاء</string>
<string name="sign_in_to_tasks_disclosure">سيتم إرسال عنوان البريد الإلكتروني ومعرف الحساب الخاصان بك وتخزينهما من قبل Tasks.org. هذه المعلومات سيتم استخدامها من أجل المصادقة ومن أجل تزويدك بالإعلانات المهمة المتعلقة بالخدمة. لن يتم مشاركة هذه المعلومات مع أحد.</string>
<string name="default_reminder">تذكير افتراضي</string>
<string name="default_reminder">تذكير تلقائي افتراضي</string>
<string name="rmd_time_description">أظهر الإشعارات من أجل المهام بدون أوقات محددة</string>
<string name="completed">مكتمل</string>
<string name="snackbar_task_completed">اكتملت المهمة</string>
@ -809,4 +808,5 @@
<string name="continue_without_sync">المتابعة بدون مزامنة</string>
<string name="delete_tasks_warning">%s سيتم حذفها. لا يمكن التراجع عن ذلك!</string>
<string name="help_me_choose">ساعدني في الاختيار</string>
<string name="widget_view_more_tasks">عرض المزيد من المهام</string>
</resources>

@ -463,7 +463,6 @@
<string name="upgrade_blurb_2">Прекарвам хиляди часове в работа по Tasks и публикувам изходният код безплатно. За да поддържам работата си, някои възможности изискват абонамент</string>
<string name="widget_due_date_hidden">Скрито</string>
<string name="icon">Икона</string>
<string name="got_it">Добре</string>
<string name="select_all">Избор на всички</string>
<string name="name_your_price">Посочете своя цена</string>
<string name="decsync_selection_description">Синхронизация чрез файлове</string>

@ -611,7 +611,6 @@
<string name="donate_today">Doneu avui</string>
<string name="donate_maybe_later">Potser més tard</string>
<string name="support_development_subscribe">Desbloquegeu funcionalitats addicionals i suporteu programari de codi obert</string>
<string name="got_it">Ja ho tinc!</string>
<string name="sort_start_group">Inici %s</string>
<string name="sort_due_group">Venciment %s</string>
<string name="sort_created_group">S\'ha creat %s</string>

@ -472,7 +472,6 @@
<string name="invalid_username_or_password">Neplatné uživatelské jméno nebo heslo</string>
<string name="davx5_selection_description">Synchronizujte své úkoly pomocí aplikace DAVx⁵</string>
<string name="building_notifications">Vytvářejí se oznámení</string>
<string name="got_it">Rozumím!</string>
<string name="support_development_subscribe">Odemkněte si další funkce a podpořte software s otevřeným zdrojovým kódem</string>
<string name="enjoying_tasks">Líbí se vám Tasks\?</string>
<string name="filter_eisenhower_box_4">Ani naléhavé, ani důležité</string>

@ -84,7 +84,6 @@
<string name="open_last_viewed_list">Åbn sidst viste liste</string>
<string name="on_launch">Ved opstart</string>
<string name="sort_modified_group">Redigeret %s</string>
<string name="got_it">Forstået!</string>
<string name="support_development_subscribe">Lås op for ekstra funktioner og støt open source-software</string>
<string name="enjoying_tasks">Nyder du Tasks\?</string>
<string name="filter_eisenhower_box_4">Ikke vigtigt og ikke presserende</string>

@ -478,7 +478,6 @@
<string name="filter_eisenhower_box_3">Unwichtig und dringend</string>
<string name="filter_eisenhower_box_2">Wichtig und nicht dringend</string>
<string name="filter_eisenhower_box_1">Wichtig und dringend</string>
<string name="got_it">Verstanden!</string>
<string name="action_new_task">Neue Aufgabe</string>
<string name="support_development_subscribe">Schalte zusätzliche Funktionen frei und unterstütze freie Software</string>
<string name="astrid_sort_order_summary">Astrids manuelle Sortiermethode für „Meine Aufgaben“, „Heute“ und Schlagwörter aktivieren. Diese Sortiermethode wird in einer zukünftigen Version „Meine Sortierung“ ersetzen</string>

@ -91,7 +91,7 @@
<string name="rmd_NoA_snooze">Αναβολή</string>
<string name="rmd_EPr_quiet_hours_start_title">Ξεκίνησε η ώρα ησυχίας </string>
<string name="rmd_EPr_quiet_hours_end_title">Τέλος ώρας κοινής ησυχίας</string>
<string name="rmd_EPr_rmd_time_title">Προκαθορισμένη υπενθύμιση</string>
<string name="rmd_EPr_rmd_time_title">Ώρα υπενθύμισης</string>
<string name="rmd_EPr_rmd_time_desc">Ειδοποιήσεις για εργασίες χωρίς ώρα λήξης θα φαίνονται στις %s</string>
<string name="rmd_EPr_defaultRemind_title">Τυχαίες υπενθυμίσεις</string>
<string name="default_random_reminder_disabled">απενεργοποιημένο</string>
@ -126,4 +126,604 @@
<string name="discard_confirmation">Θέλετε σίγουρα να απορρίψετε τις αλλαγές;</string>
<string name="cancel">Ακύρωση</string>
<string name="ok">OK</string>
</resources>
<string name="display_name">Εμφανιζόμενο όνομα</string>
<string name="default_list">Προεπιλεγμένη λίστα</string>
<string name="help_and_feedback">Βοήθεια &amp; Καταχώρηση απόψεων</string>
<string name="app_settings">Ρυθμίσεις Εφαρμογής</string>
<string name="customize_drawer">Προσαρμογή συρταριού</string>
<string name="customize_drawer_summary">Τραβήξτε και αφήστε για ανακατάταξη των αντικειμένων του μενού</string>
<string name="astrid_sort_order_summary">Ενεργοποιήστε τη χειροκίνητη λειτουργία ταξινόμησης του Astrid για τις κατηγορίες \'Οι Εργασίες μου\', \'Σήμερα\', και τις ετικέτες. Αυτή η μέθοδος ταξινόμησης θα αντικατασταθεί από την \'Η σειρά μου\' σε μελλοντική ενημέρωση της εφαρμογής</string>
<string name="SSD_sort_start">Ανά ημερομηνία έναρξης</string>
<string name="sort_created">Ανά ώρα δημιουργίας</string>
<string name="sort_list">Ανά λίστα</string>
<string name="sort_completed">Ανά ώρα ολοκλήρωσης</string>
<string name="FLA_new_filter">Δημιουργία νέου φίλτρου</string>
<string name="TEA_add_subtask">Προσθήκη υποεργασίας</string>
<string name="save">Αποθήκευση</string>
<string name="start_date">Ημερομηνία Έναρξης</string>
<string name="TEA_control_repeat">Επανάληψη</string>
<string name="TEA_control_gcal">Ημερολόγιο</string>
<string name="TEA_control_location">Τοποθεσία</string>
<string name="TEA_creation_date">Ημερομηνία Δημιουργίας</string>
<string name="none">Κανένα</string>
<string name="task_list_options">Επιλογές λίστας Εργασιών</string>
<string name="always_display_full_date">Εμφάνιση Πλήρους Ημερομηνίας</string>
<string name="EPr_reset_preferences">Επαναφορά προτιμήσεων</string>
<string name="EPr_reset_preferences_warning">Θα γίνει επαναφορά των προτιμήσεων στις προεπιλεγμένες τιμές</string>
<string name="EPr_delete_task_data">Διαγραφή δεδομένων εργασίας</string>
<string name="EPr_delete_task_data_warning">Όλες οι εργασίες θα διαγραφούν μόνιμα</string>
<string name="task_defaults">Προεπιλογές Εργασιών</string>
<string name="default_start_date">Προεπιλεγμένη Ημερομηνία έναρξης</string>
<string name="default_due_date">Προεπιλεγμένη προθεσμία ολοκλήρωσης</string>
<string name="EPr_default_location_reminder_title">Υπενθυμίσεις προεπιλεγμένης τοποθεσίας</string>
<string name="default_location">Προεπιλεγμένη τοποθεσία</string>
<string name="priority_high">Υψηλή</string>
<string name="priority_medium">Μεσαία</string>
<string name="priority_low">Χαμηλή</string>
<string name="no_reminders">Καθόλου υπενθυμίσεις</string>
<string name="default_location_reminder_on_arrival">Κατά την άφιξη</string>
<string name="default_location_reminder_on_departure">Κατά την αναχώρηση</string>
<string name="default_location_reminder_on_arrival_or_departure">Κατά την άφιξη και την αναχώρηση</string>
<string name="BFE_Active">Οι Εργασίες μου</string>
<string name="BFE_Recent">Πρόσφατα επεξεργασμένα</string>
<string name="CFC_startBefore_text">Έναρξη από: ?</string>
<string name="CFC_startBefore_name">Έναρξη από…</string>
<string name="no_start_date">Καθόλου ημερομηνία έναρξης</string>
<string name="filter_any_start_date">Οποιαδήποτε ημερομηνία έναρξης</string>
<string name="CFC_tag_text">Ετικέτα: ?</string>
<string name="CFC_tag_name">Ετικέτα…</string>
<string name="CFC_tag_contains_name">Το όνομα της ετικέτας περιλαμβάνει…</string>
<string name="CFC_tag_contains_text">Το όνομα της ετικέτας περιλαμβάνει: ?</string>
<string name="calendar_event_not_found">Δεν βρέθηκε συμβάν στο Ημερολόγιο</string>
<string name="CFC_list_name">Στη λίστα…</string>
<string name="ring_once">Μόνο χτύπημα</string>
<string name="ring_five_times">Χτύπημα πέντε φορές</string>
<string name="ring_nonstop">Ασταμάτητο χτύπημα</string>
<string name="snooze_all">Αναβολή όλων</string>
<string name="persistent_notifications">Επαναλαμβανόμενες ειδοποιήσεις</string>
<string name="persistent_notifications_description">Οι επαναλαμβανόμενες ειδοποιήσεις δεν μπορούν να εξαλειφθούν</string>
<string name="swipe_to_snooze_title">Τραβήξτε για αναβολή</string>
<string name="swipe_to_snooze_description">Ώρα αναβολής</string>
<string name="show_completed">Εμφάνιση ολοκληρωμένων</string>
<string name="swipe_to_snooze_time_description">Μια διαγραμμένη ειδοποίηση θα αναβληθεί και δημιουργηθεί εκ νέου %s</string>
<string name="swipe_to_snooze_time_immediately">κατευθείαν</string>
<string name="swipe_to_snooze_time_15_minutes">μετά από 15 λεπτά</string>
<string name="swipe_to_snooze_time_30_minutes">μετά από 30 λεπτά</string>
<string name="swipe_to_snooze_time_1_hour">μετά από 1 ώρα</string>
<string name="swipe_to_snooze_time_24_hours">μετά από 24 ώρες</string>
<string name="repeat_option_every_day">Κάθε μέρα</string>
<string name="repeat_option_every_week">Κάθε εβδομάδα</string>
<string name="repeat_option_every_month">Κάθε μήνα</string>
<string name="repeat_option_every_year">Κάθε χρόνο</string>
<string name="repeat_option_custom">Προσαρμογή…</string>
<plurals name="repeat_occurrence">
<item quantity="one">Περιστατικό</item>
<item quantity="other">Περιστατικά</item>
</plurals>
<plurals name="task_count">
<item quantity="one">%d εργασία</item>
<item quantity="other">%d εργασίες</item>
</plurals>
<plurals name="subtask_count">
<item quantity="one">%d υποεργασία</item>
<item quantity="other">%d υποεργασίες</item>
</plurals>
<plurals name="list_count">
<item quantity="one">%d λίστα</item>
<item quantity="other">%d λίστες</item>
</plurals>
<plurals name="repeat_times">
<item quantity="one">ώρα</item>
<item quantity="other">ώρες</item>
</plurals>
<plurals name="repeat_minutes">
<item quantity="one">λεπτό</item>
<item quantity="other">λεπτά</item>
</plurals>
<plurals name="reminder_minutes">
<item quantity="one">Λεπτό</item>
<item quantity="other">Λεπτά</item>
</plurals>
<plurals name="repeat_n_minutes">
<item quantity="one">%d λεπτό</item>
<item quantity="other">%d λεπτά</item>
</plurals>
<plurals name="repeat_hours">
<item quantity="one">ώρα</item>
<item quantity="other">ώρες</item>
</plurals>
<plurals name="reminder_hours">
<item quantity="one">Ώρα</item>
<item quantity="other">Ώρες</item>
</plurals>
<plurals name="repeat_n_hours">
<item quantity="one">%d ώρα</item>
<item quantity="other">%d ώρες</item>
</plurals>
<plurals name="repeat_days">
<item quantity="one">μέρα</item>
<item quantity="other">μέρες</item>
</plurals>
<plurals name="reminder_days">
<item quantity="one">Μέρα</item>
<item quantity="other">Μέρες</item>
</plurals>
<plurals name="repeat_n_days">
<item quantity="one">%d μέρα</item>
<item quantity="other">%d μέρες</item>
</plurals>
<plurals name="repeat_weeks">
<item quantity="one">εβδομάδα</item>
<item quantity="other">εβδομάδες</item>
</plurals>
<plurals name="reminder_week">
<item quantity="one">Εβδομάδα</item>
<item quantity="other">Εβδομάδες</item>
</plurals>
<plurals name="repeat_n_weeks">
<item quantity="one">%d εβδομάδα</item>
<item quantity="other">%d εβδομάδες</item>
</plurals>
<plurals name="repeat_months">
<item quantity="one">μήνας</item>
<item quantity="other">μήνες</item>
</plurals>
<plurals name="repeat_n_months">
<item quantity="one">%d μήνας</item>
<item quantity="other">%d μήνες</item>
</plurals>
<plurals name="repeat_years">
<item quantity="one">χρόνος</item>
<item quantity="other">χρόνια</item>
</plurals>
<plurals name="repeat_n_years">
<item quantity="one">%d χρόνος</item>
<item quantity="other">%d χρόνια</item>
</plurals>
<string name="repeat_type_due">ημερομηνία προθεσμίας</string>
<string name="repeat_type_completion">ημερομηνία ολοκλήρωσης</string>
<string name="repeat_type_completion_capitalized">Ημερομηνία ολοκλήρωσης</string>
<string name="repeat_snackbar">%1$s επαναπρογραμματισμός για %2$s</string>
<string name="new_tag">Δημιουργία νέας ετικέτας</string>
<string name="new_list">Δημιουργία νέας λίστας</string>
<string name="delete_tag_confirmation">Διαγραφή %s?</string>
<string name="delete_comment">σχόλιο</string>
<string name="comment">Σχόλιο</string>
<plurals name="Ntasks">
<item quantity="one">1 εργασία</item>
<item quantity="other">%d εργασίες</item>
</plurals>
<string name="now">Τώρα</string>
<string name="next_sunday">Επόμενη Κυρ</string>
<string name="next_monday">Επόμενη Δευτ</string>
<string name="next_tuesday">Επόμενη Τρι</string>
<string name="next_wednesday">Επόμενη Τετ</string>
<string name="next_thursday">Επόμενη Πεμ</string>
<string name="next_friday">Επόμενη Παρ</string>
<string name="next_saturday">Επόμενο Σαβ</string>
<string name="yesterday">Εχθές</string>
<string name="widget_show_header">Εμφάνιση επικεφαλίδας</string>
<string name="widget_show_settings">Εμφάνιση ρυθμίσεων</string>
<string name="widget_show_title">Εμφάνιση τίτλου</string>
<string name="widget_show_menu">Εμφάνιση μενού</string>
<string name="widget_show_dividers">Εμφάνιση χωρισμάτων</string>
<string name="notifications">Ειδοποιήσεις</string>
<string name="quiet_hours">Ώρες ησυχίας</string>
<string name="quiet_hours_in_effect">Οι ώρες ησυχίας είναι ενεργές</string>
<string name="attachment_directory">Φάκελος συνημμένου</string>
<string name="backup_directory">Φάκελος εφεδρικού αντιγράφου</string>
<string name="google_drive_backup">Αντίγραφο ασφαλείας Google Drive</string>
<string name="miscellaneous">Διάφορα</string>
<string name="subtasks">Υποεργασίες</string>
<string name="enabled">Ενεργοποιημένο</string>
<string name="font_size">Μέγεθος γραμματοσειράς</string>
<string name="header_spacing">Κενά μεταξύ γραμμάτων</string>
<string name="row_spacing">Κενά μεταξύ σειρών</string>
<string name="icon">Εικονίδιο</string>
<string name="launcher_icon">Εικονίδιο Launcher (προωθητή)</string>
<string name="theme_black">Μαύρο</string>
<string name="theme_light">Λευκό</string>
<string name="theme_dark">Σκοτεινό</string>
<string name="theme_wallpaper">Ταπετσαρία</string>
<string name="theme_day_night">Μέρα/Νύχτα</string>
<string name="theme_system_default">Προεπιλογή Συστήματος</string>
<string name="theme_dynamic">Δυναμικό</string>
<string name="language">Γλώσσα</string>
<string name="restart_required">Επανεκκινήστε τις Εργασίες για να εφαρμοστεί αυτή η αλλαγή</string>
<string name="copy_multiple_tasks_confirmation">%s αντιγράφηκε</string>
<string name="delete_multiple_tasks_confirmation">%s διαγράφηκε</string>
<string name="delete_selected_tasks">Διαγραφή επιλεγμένων εργασιών;</string>
<string name="copy_selected_tasks">Αντιγραφή επιλεγμένων εργασιών;</string>
<string name="date_and_time">Ημερομηνία και ώρα</string>
<string name="add_account">Προσθήκη λογαριασμού</string>
<string name="continue_without_sync">Συνέχεια χωρίς συγχρονισμό</string>
<string name="help_me_choose">Βοήθησε με να διαλέξω</string>
<string name="user">Χρήστης</string>
<string name="password">Κωδικός</string>
<string name="url">Ηλεκτρονική διεύθυνση (URL)</string>
<string name="error_adding_account">Σφάλμα: %s</string>
<string name="notification_disable_battery_optimizations_description">Οι βελτιστοποιήσεις μπαταρίας ενδέχεται να καθυστερούν τις ειδοποιήσεις</string>
<string name="bundle_notifications">Ειδοποιήσεις πακέτου</string>
<string name="default_reminder">Προεπιλεγμένη υπενθύμιση</string>
<string name="badges">Μετάλλια</string>
<string name="list">Λίστα</string>
<string name="repeats_from">Επανάληψη από</string>
<string name="repeats_single">Επανάληψη %s</string>
<string name="repeats_single_on">Επανάληψη %1$s στις %2$s</string>
<string name="repeats_single_until">Επανάληψη %1$s, τελειώνει στις %2$s</string>
<string name="repeats_single_number_of_times">Επανάληψη %1$s, συμβαίνει %2$d %3$s</string>
<string name="repeats_single_on_until">Επανάληψη %1$s στις %2$s, τελειώνει στις %3$s</string>
<string name="repeats_single_on_number_of_times">Επανάληψη %1$s στις %2$s, συμβαίνει %3$d %4$s</string>
<string name="repeats_minutely">κάθε λεπτό</string>
<string name="repeats_hourly">κάθε ώρα</string>
<string name="repeats_daily">καθημερινά</string>
<string name="repeats_weekly">εβδομαδιαία</string>
<string name="repeats_monthly">μηνιαία</string>
<string name="repeats_yearly">ετησίως</string>
<string name="repeats_custom_recurrence">Προσαρμογή επανάληψης</string>
<string name="repeats_every">Επανάληψη κάθε</string>
<string name="repeats_weekly_on">Επανάληψη στις</string>
<string name="repeats_never">Ποτέ</string>
<string name="repeats_on">Στις</string>
<string name="repeats_after">Μετά από</string>
<string name="repeats_ends">Τελειώνει</string>
<string name="repeats_plural">Επανάληψη κάθε %s</string>
<string name="repeats_plural_on">Επανάληψη κάθε %1$s στις %2$s</string>
<string name="repeats_plural_until">Επανάληψη κάθε %1$s, τελειώνει στις %2$s</string>
<string name="repeats_plural_number_of_times">Επανάληψη κάθε %1$s, συμβαίνει %2$d %3$s</string>
<string name="repeats_plural_on_until">Επανάληψη κάθε %1$s στις %2$s, τελειώνει στις %3$s</string>
<string name="repeats_plural_on_number_of_times">Επανάληψη κάθε %1$s στις %2$s, συμβαίνει %3$d %4$s</string>
<string name="list_separator_with_space">", "</string>
<string name="dont_add_to_calendar">Να μην προστεθεί στο ημερολόγιο</string>
<string name="default_calendar">Προεπιλεγμένο ημερολόγιο</string>
<string name="rmd_time_description">Να εμφανίζονται ειδοποιήσεις για εργασίες χωρίς τις προθεσμίες</string>
<string name="badges_description">Εμφάνιση αριθμού εργασιών στο εικονίδιο εκκίνησης της εφαρμογής Tasks. Δεν υποστηρίζουν όλοι οι προωθητές (launchers) μετάλλια/ εικονίδια.</string>
<string name="bundle_notifications_summary">Συνδυασμός πολλαπλών ενημερώσεων σε μία</string>
<string name="repeat_monthly_every_day_of_nth_week">κάθε %1$s %2$s</string>
<string name="repeat_monthly_on_day_number">Κάθε μήνα την ημέρα %1d</string>
<string name="repeat_monthly_on_the_nth_weekday">Κάθε μήνα στις %1$s %2$s</string>
<string name="repeat_monthly_first_week">πρώτη</string>
<string name="repeat_monthly_second_week">δεύτερη</string>
<string name="repeat_monthly_third_week">τρίτη</string>
<string name="repeat_monthly_fourth_week">τέταρτη</string>
<string name="repeat_monthly_fifth_week">πέμπτη</string>
<string name="repeat_monthly_last_week">τελευταία</string>
<string name="tasker_create_task">Δημιουργία εργασίας</string>
<string name="tasker_list_notification">Ειδοποίηση λίστας</string>
<string name="help">Βοήθεια</string>
<string name="caldav_home_set_not_found">Δεν βρέθηκε αρχικό σετ</string>
<string name="network_error">Η σύνδεση απέτυχε</string>
<string name="upgrade_to_pro">Αναβάθμιση σε συνδρομή</string>
<string name="subscription">Συνδρομή εφαρμογής</string>
<string name="manage_subscription">Τροποποίηση συνδρομής</string>
<string name="refresh_purchases">Ανανέωση συναλλαγών</string>
<string name="button_subscribe">Εγγραφή στη συνδρομή</string>
<string name="button_unsubscribe">Ακύρωση συνδρομής</string>
<string name="about">Σχετικά</string>
<string name="license_summary">Η εφαρμογή Tasks είναι ελεύθερο λογισμικό ανοιχτού κώδικα, με άδεια χρήσης GNU General Public License v3.0</string>
<string name="pro_dashclock_extension">Επέκταση ρολογιού ταμπλό</string>
<string name="requires_pro_subscription">Χρειάζεται συνδρομή (pro)</string>
<string name="this_feature_requires_a_subscription">Αυτό το χαρακτηριστικό χρειάζεται συνδρομή</string>
<string name="logout">Αποσύνδεση</string>
<string name="delete_tasks_warning">%s θα διαγραφεί. Αυτό δεν γίνεται να αναιρεθεί!</string>
<string name="logout_warning">Όλα τα δεδομένα για αυτό το λογαριασμό θα αφαιρεθούν από τη συσκευή σας</string>
<string name="cannot_access_account">Δεν υπάρχει πρόσβαση στο λογαριασμό</string>
<string name="reinitialize_account">Εκ νέου αρχικοποίηση</string>
<string name="action_create_new_task">Δημιουργία νέας εργασίας</string>
<string name="action_new_task">Νέα εργασία</string>
<string name="show_description">Εμφάνιση περιγραφής</string>
<string name="show_full_description">Εμφάνιση ολόκληρης της περιγραφής</string>
<string name="linkify">Εμφάνιση συνδέσμων</string>
<string name="linkify_description">Προσθήκη συνδέσμων σε ιστοσελίδες, διευθύνσεις και τηλεφωνικούς αριθμούς</string>
<string name="location_remind_arrival">Υπενθύμιση κατά την άφιξη</string>
<string name="location_remind_departure">Υπενθύμιση κατά την αναχώρηση</string>
<string name="visit_website">Επίσκεψη ιστοσελίδας</string>
<string name="location_arrived">Άφιξη στην τοποθεσία %s</string>
<string name="location_departed">Αναχώρησε %s</string>
<string name="building_notifications">Δημιουργία ειδοποιήσεων</string>
<string name="choose_a_location">Επιλέξτε μια τοποθεσία</string>
<string name="pick_this_location">Επιλογή αυτής της τοποθεσίας</string>
<string name="or_choose_a_location">Ή επιλέξτε μια τοποθεσία</string>
<string name="missing_permissions">Λείπουν άδειες</string>
<string name="background_location_permission_required">Η εφαρμογή Tasks συλλέγει δεδομένα τοποθεσίας για να ενεργοποιεί υπενθυμίσεις που βασίζονται στην τοποθεσία, ακόμα και όταν η εφαρμογή είναι κλειστή ή σε αδράνεια.</string>
<string name="location_permission_required_location">Χρειάζονται άδειες τοποθεσίας για την εύρεση της τωρινής σας τοποθεσίας</string>
<string name="open_map">Άνοιγμα χάρτη</string>
<string name="choose_new_location">Επιλογή νέας τοποθεσίας</string>
<string name="third_party_licenses">Άδειες τρίτων</string>
<string name="banner_app_updated_title">Η εφαρμογή ενημερώθηκε</string>
<string name="banner_app_updated_description">Η Tasks μόλις ενημερώθηκε στο %s. Θα θέλατε να δείτε της σημειώσεις έκδοσης;</string>
<string name="whats_new">Τι καινούργιο υπάρχει</string>
<string name="version_string">Έκδοση %s</string>
<string name="invalid_backup_file">Μη έγκυρο αρχείο αντιγράφου ασφαλείας</string>
<string name="google_tasks_add_to_top">Νέες εργασίες στο πάνω μέρος</string>
<string name="name_your_price">Καθορίστε την τιμή σας</string>
<string name="expand_subtasks">Άνοιγμα υποεργασιών</string>
<string name="collapse_subtasks">Κλείσιμο υποεργασιών</string>
<string name="subtasks_multilevel_google_task">Οι πολυεπίπεδες υποεργασίες δεν υποστηρίζονται από το Google Tasks</string>
<string name="subtasks_multilevel_microsoft">Οι πολυεπίπεδες υποεργασίες δεν υποστηρίζονται από το Microsoft To Do</string>
<string name="enter_tag_name">Εισαγωγή ονόματος ετικέτας</string>
<string name="choose_synchronization_service">Επιλογή πλατφόρμας</string>
<string name="tasks_org_description">Συγχρονίστε τις εργασίες σας με το Tasks.org</string>
<string name="google_tasks_selection_description">Βασική υπηρεσία που συγχρονίζει με το λογαριασμό σας Google</string>
<string name="caldav_selection_description">Συγχρονισμός βασισμένος σε ανοιχτά πρότυπα διαδικτύου</string>
<string name="etesync_selection_description">Κρυπτογραφημένος συγχρονισμός από άκρο σε άκρο</string>
<string name="decsync_selection_description">Συγχρονισμός βάσει αρχείων</string>
<string name="microsoft_selection_description">Συγχρονίστε με τον προσωπικό σας λογαριασμό Microsoft</string>
<string name="davx5_selection_description">Συγχρονίστε τις εργασίες σας με την εφαρμογή DAVx⁵</string>
<string name="show_advanced_settings">Εμφάνιση σύνθετων ρυθμίσεων</string>
<string name="caldav_account_description">Απαιτείται λογαριασμός σε πάροχο υπηρεσιών CalDAV ή σε αυτοφιλοξενούμενο διακομιστή. Βρείτε έναν πάροχο υπηρεσιών μεταβαίνοντας στη διεύθυνση tasks.org/caldav</string>
<string name="etesync_account_description">Απαιτείται λογαριασμός στο EteSync.com ή σε αυτο-φιλοξενούμενο διακομιστή</string>
<string name="preferences_look_and_feel">Όψη και αίσθηση</string>
<string name="preferences_advanced">Συνθέτες</string>
<string name="documentation">Έγγραφα</string>
<string name="wearable_notifications">Ενημερώσεις φορητών συσκευών</string>
<string name="wearable_notifications_summary">Εμφάνιση ειδοποιήσεων στις φορητές σας συσκευές</string>
<string name="troubleshooting">Αντιμετώπιση προβλημάτων</string>
<string name="notification_troubleshooting_summary">Πατήστε εδώ αν έχετε πρόβλημα με τις ειδοποιήσεις</string>
<string name="disable_battery_optimizations">Απενεργοποίηση βελτιστοποιήσεων μπαταρίας</string>
<string name="more_settings">Περισσότερες ρυθμίσεις</string>
<string name="more_notification_settings_summary">Ήχος κλήσης, δόνηση, και άλλα</string>
<string name="invalid_username_or_password">Μη έγκυρο όνομα χρήστη ή κωδικός</string>
<string name="color_wheel">Τροχός χρωμάτων</string>
<string name="upgrade_blurb_1">Γεια! Με λένε Άλεξ. Είμαι ο ανεξάρτητος προγραμματιστής πίσω από την εφαρμογή Tasks</string>
<string name="upgrade_blurb_2">Έχω ξοδέψει χιλιάδες ώρες δουλεύοντας στο Tasks, και δημοσιεύω όλο τον πηγαίο κώδικα στο διαδίκτυο δωρεάν. Για να υποστηρίξω τη δουλειά μου, κάποια χαρακτηριστικά χρειάζονται συνδρομή</string>
<string name="back">Πίσω</string>
<string name="chips">Τσιπς</string>
<string name="chip_appearance">Εμφάνιση τσιπ</string>
<string name="places">Μέρη/ τοποθεσίες</string>
<string name="place_settings">Ρυθμίσεις μέρους/ τοποθεσίας</string>
<string name="navigation_drawer">Συρτάρι πλοήγησης</string>
<string name="hide_unused_tags">Απόκρυψη μη χρησιμοποιημένων ετικετών</string>
<string name="hide_unused_places">Απόκρυψη μη χρησιμοποιημένων μερών/ τοποθεσιών</string>
<string name="chip_appearance_text_and_icon">Κείμενο και εικονίδιο</string>
<string name="chip_appearance_text_only">Κείμενο μόνο</string>
<string name="chip_appearance_icon_only">Εικονίδιο μόνο</string>
<string name="shortcut_pick_time">Επιλογή ώρας</string>
<string name="auto_dismiss_datetime">Αυτόματο κλείσιμο επιλογέα ημερομηνίας ώρας</string>
<string name="auto_dismiss_datetime_list">Λίστα εργασιών</string>
<string name="auto_dismiss_datetime_list_summary">Αυτόματο κλείσιμο όταν γίνεται επιλογή από τη λίστα εργασιών</string>
<string name="auto_dismiss_datetime_edit">Επεξεργασία εργασίας</string>
<string name="auto_dismiss_datetime_edit_summary">Αυτόματο κλείσιμο όταν γίνεται επιλογή από την επεξεργασία εργασίας</string>
<string name="auto_dismiss_datetime_widget">Γραφικό στοιχείο</string>
<string name="auto_dismiss_datetime_widget_summary">Αυτόματο κλείσιμο όταν γίνεται επιλογή από το γραφικό στοιχείο</string>
<string name="auto_dismiss_datetime_summary">Κλείσιμο επιλογέα ημερομηνίας ώρας μετά την επιλογή μιας ημερομηνίας ή ώρας</string>
<string name="calendar_event_created">Δημιουργήθηκε συμβάν ημερολογίου για %s</string>
<string name="select_all">Επιλογή όλων</string>
<string name="share">Μοιραστείτε το</string>
<string name="widget_id">Ταυτότητα γραφικού στοιχείου: %d</string>
<string name="settings_default">Προεπιλογή</string>
<string name="compact">Συμπαγής</string>
<string name="custom_filter_criteria">Φιλτράρισμα κριτηρίων</string>
<string name="filter_overdue">Εκπρόθεσμα</string>
<string name="filter_today_only">Σήμερα μόνο</string>
<string name="filter_any_due_date">Οποιαδήποτε προθεσμία</string>
<string name="filter_after_today">Μετά από σήμερα</string>
<string name="filter_no_tags">Καθόλου ετικέτες</string>
<string name="add_tags">Προσθήκη ετικετών</string>
<string name="filter_high_priority">Υψηλή προτεραιότητα</string>
<string name="filter_medium_priority">Μεσαία προτεραιότητα</string>
<string name="filter_low_priority">Χαμηλή προτεραιότητα</string>
<string name="filter_no_priority">Καθόλου προτεραιότητα</string>
<string name="filter_eisenhower_box_1">Σημαντικό και επείγον</string>
<string name="filter_eisenhower_box_2">Σημαντικό , αλλά όχι επείγον</string>
<string name="filter_eisenhower_box_3">Όχι σημαντικό , αλλά επείγον</string>
<string name="filter_eisenhower_box_4">Όχι σημαντικό και όχι επείγον</string>
<string name="enjoying_tasks">Σας αρέσει η εφαρμογή Tasks;</string>
<string name="donate_nag">Σκεφτείτε να δείξετε την υποστήριξη σας με μια δωρεά!</string>
<string name="donate_today">Κάντε δωρεά σήμερα</string>
<string name="donate_maybe_later">Ίσως αργότερα</string>
<string name="support_development_subscribe">Ξεκλειδώστε πρόσθετες λειτουργίες και υποστηρίξτε λογισμικό ανοιχτού κώδικα</string>
<string name="sort_start_group">Αρχή %s</string>
<string name="sort_due_group">Προθεσμία %s</string>
<string name="sort_created_group">Δημιουργήθηκε %s</string>
<string name="sort_modified_group">Τροποποιήθηκε %s</string>
<string name="sort_completion_group">Ολοκλήρωση %s</string>
<string name="on_launch">Κατά την εκκίνηση</string>
<string name="open_last_viewed_list">Άνοιγμα λίστας όσων προβλήθηκαν τελευταία</string>
<string name="local_lists">Τοπικές λίστες</string>
<string name="lists">Λίστες</string>
<string name="reset_sort_order">Επαναφορά σειράς ταξινόμησης</string>
<string name="permission_read_tasks">Πλήρης πρόσβαση στη βάση δεδομένων του Tasks</string>
<string name="automatic_backups">Αυτόματα αντίγραφα ασφαλείας</string>
<string name="android_auto_backup">Υπηρεσία αντιγράφου ασφαλείας Android</string>
<string name="android_auto_backup_device_summary">Πρέπει επίσης να επιλέξετε την υπηρεσία αντιγράφων ασφαλείας μέσω των ρυθμίσεων της συσκευής σας. Δεν παρέχουν όλες οι συσκευές υπηρεσία αντιγράφων ασφαλείας.</string>
<string name="last_backup">Τελευταίο αντίγραφο ασφαλείας: %s</string>
<string name="last_backup_never">ποτέ</string>
<string name="device_settings">Ρυθμίσεις συσκευής</string>
<string name="account">Λογαριασμός</string>
<string name="foreground_location">Τοποθεσία στο προσκήνιο</string>
<string name="background_location">Τοποθεσία στο παρασκήνιο</string>
<string name="backups_ignore_warnings">Να αγνοηθούν οι προειδοποιήσεις</string>
<string name="backups_ignore_warnings_summary">Αγνοήστε τις προειδοποιήσεις δημιουργίας αντιγράφων ασφαλείας εάν δεν χρειάζεστε αντίγραφα ασφαλείας ή εάν έχετε τη δική σας λύση δημιουργίας αντιγράφων ασφαλείας</string>
<string name="backup_location_warning">ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Τα αρχεία που βρίσκονται στο %s θα διαγραφούν εάν απεγκατασταθεί το Tasks! Παρακαλούμε επιλέξτε μια προσαρμοσμένη θέση για να αποτρέψετε το Android από τη διαγραφή των αρχείων σας.</string>
<string name="multi_select_reschedule">Επαναπρογραμματισμός</string>
<string name="date_picker_multiple">Πολλαπλά</string>
<string name="custom_filter_has_subtask">Περιλαμβάνει υποεργασίες</string>
<string name="custom_filter_is_subtask">Είναι υποεργασία</string>
<string name="custom_filter_has_reminder">Περιλαμβάνει υπενθύμιση</string>
<string name="your_subscription_expired">Η συνδρομή σας έχει λήξει. Γίνετε συνδρομητής τώρα για να συνεχίσετε την υπηρεσία.</string>
<string name="insufficient_subscription">Ανεπαρκές επίπεδο συνδρομής. Παρακαλούμε αναβαθμίστε τη συνδρομή σας για να συνεχίσετε την υπηρεσία.</string>
<string name="insufficient_sponsorship">Δεν βρέθηκε κατάλληλη χορηγία GitHub</string>
<string name="no_google_play_subscription">Δεν βρέθηκε κατάλληλη συνδρομή στο Google Play</string>
<string name="price_per_year">$%s/χρόνο</string>
<string name="price_per_month">$%s/μήνα</string>
<string name="price_per_month_with_currency">%s/μήνα</string>
<string name="price_per_year_with_currency">%s/χρόνο</string>
<string name="current_subscription">Τρέχουσα συνδρομή: %s</string>
<string name="follow_reddit">Γίνετε μέλος του r/tasks</string>
<string name="follow_twitter">Ακολουθήστε το @tasks_org</string>
<string name="social">Κοινωνικά δίκτυα</string>
<string name="support">Υποστήριξη</string>
<string name="issue_tracker">Παρακολούθηση ζητημάτων</string>
<string name="open_source">Ανοιχτός κώδικας</string>
<string name="privacy">Ιδιωτικότητα</string>
<string name="authorization_cancelled">Η εξουσιοδότηση ακυρώθηκε</string>
<string name="google_play_subscribers">Συνδρομητές Google Play</string>
<string name="github_sponsors">Χορηγοί GitHub</string>
<string name="sign_in_with_google">Συνδεθείτε μέσω Google</string>
<string name="sign_in_with_github">Συνδεθείτε μέσω GitHub</string>
<string name="authentication_required">Απαιτείται πιστοποίηση ταυτότητας</string>
<string name="github_sponsor">Σπόνσορας</string>
<string name="migrate">Μετακίνηση</string>
<string name="migrating_tasks">Εργασίες μετακίνησης</string>
<string name="migrate_count">Μετακίνηση %s στο Tasks.org</string>
<string name="above_average">Άνω του μέσου όρου</string>
<string name="save_percent">Αποθήκευση %d%%</string>
<string name="sign_in_to_tasks">Συνδεθείτε στο Tasks.org</string>
<string name="sign_in_to_tasks_disclosure">Η διεύθυνση ηλεκτρονικού ταχυδρομείου και το αναγνωριστικό του λογαριασμού σας θα μεταδοθούν και θα αποθηκευτούν από το Tasks.org. Οι πληροφορίες αυτές θα χρησιμοποιηθούν για την εξακρίβωση της ταυτότητας και για την παροχή σημαντικών ανακοινώσεων σχετικά με την υπηρεσία. Αυτές οι πληροφορίες δεν θα κοινοποιηθούν σε κανέναν.</string>
<string name="app_password">Κωδικός εφαρμογής</string>
<string name="app_passwords">Κωδικοί εφαρμογής</string>
<string name="app_passwords_more_info">Συγχρονίστε τις εργασίες και τα ημερολόγια σας με εφαρμογές ηλεκτρονικών υπολογιστών και κινητών συσκευών τρίτων. Πατήστε εδώ για περισσότερες πληροφορίες</string>
<string name="generate_new_password">Δημιουργία νέου κωδικού</string>
<string name="app_password_enter_description">Δώστε ένα όνομα στον κωδικό σας (προαιρετικό)</string>
<string name="app_password_created_at">Δημιουργήθηκε: %s</string>
<string name="app_password_last_access">Τελευταία χρήση: %s</string>
<string name="app_password_delete_confirmation">Οποιαδήποτε εφαρμογή χρησιμοποιεί αυτόν τον κωδικό θα αποσυνδεθεί</string>
<string name="app_password_save">Χρησιμοποιήστε αυτά τα διαπιστευτήρια για να ρυθμίσετε μια εφαρμογή τρίτων. Σας παραχωρούν πλήρη πρόσβαση στο λογαριασμό σας στο Tasks.org, μην τα γράψετε ή τα μοιραστείτε με κανέναν!</string>
<string name="copied_to_clipboard">%s αντιγράφηκε στο πρόχειρο</string>
<string name="tasks_org_account">Λογαριασμός Tasks.org</string>
<string name="tasks_org_account_required">Χρειάζεται λογαριασμός Tasks.org</string>
<string name="account_not_included">Δεν περιλαμβάνεται με τις συνδρομές \"Ονομάστε την τιμή σας\"</string>
<string name="list_members">Μέλη λίστας</string>
<string name="remove_user">Αφαίρεση χρήστη;</string>
<string name="remove_user_confirmation">%1$s δεν θα έχει πλέον πρόσβαση στο %2$s</string>
<string name="share_list">Κοινοποίηση λίστας</string>
<string name="invite">Πρόσκληση</string>
<string name="invite_declined">Η πρόσκληση απορρίφθηκε</string>
<string name="invite_awaiting_response">Αναμένεται απάντηση στην πρόσκληση</string>
<string name="invite_invalid">Μη έγκυρη πρόσκληση</string>
<string name="pro_free_trial">Οι νέοι συνδρομητές λαμβάνουν δωρεάν δοκιμή 7 ημερών. Ακυρώστε ανά πάσα στιγμή</string>
<string name="upgrade_more_customization">Περισσότερη προσαρμογή</string>
<string name="upgrade_more_customization_description">Ξεκλειδώστε όλα τα θέματα, χρώματα και εικονίδια</string>
<string name="upgrade_tasks_org_account_description">Συγχρονίστε με το Tasks.org και συνεργαστείτε με άλλους χρήστες</string>
<string name="upgrade_desktop_access">Πρόσβαση στην επιφάνεια εργασίας</string>
<string name="upgrade_desktop_access_description">Συγχρονισμός με τρίτους πελάτες όπως το Outlook και το Apple Reminders</string>
<string name="upgrade_open_source_description">Η συνδρομή σας υποστηρίζει τη συνεχή ανάπτυξη</string>
<string name="more_options">Περισσότερες επιλογές</string>
<string name="markdown">Αποτίμηση (Markdown)</string>
<string name="markdown_description">Ενεργοποίηση αποτίμησης (Markdown) στον τίτλο και την περιγραφή</string>
<string name="completion_sound">Αναπαραγωγή ήχου ολοκλήρωσης</string>
<string name="completed">Ολοκληρώθηκε</string>
<string name="snackbar_task_completed">Η εργασία ολοκληρώθηκε</string>
<string name="completed_tasks_at_bottom">Μετακίνηση ολοκληρωμένων εργασιών στο κάτω μέρος</string>
<string name="snackbar_tasks_completed">%d ολοκληρωμένες εργασίες</string>
<string name="alarm_before_start">%s πριν την έναρξη</string>
<string name="alarm_after_start">%s μετά την έναρξη</string>
<string name="alarm_before_due">%s πριν την προθεσμία</string>
<string name="alarm_after_due">%s μετά την προθεσμία</string>
<string name="snoozed_until">Αναβλήθηκε μέχρι %s</string>
<string name="custom_notification">Προσαρμοσμένη ειδοποίηση</string>
<string name="caldav_server_unknown">Άγνωστο</string>
<string name="caldav_server_other">Άλλο</string>
<string name="caldav_server_type">Τύπος διακομιστή</string>
<string name="dismiss">Απορρίψτε</string>
<string name="hint_customize_edit_title">Πάρα πολλές πληροφορίες;</string>
<string name="hint_customize_edit_body">Μπορείτε να προσαρμόσετε αυτή την οθόνη αναδιατάσσοντας ή αφαιρώντας πεδία</string>
<string name="enable_reminders">Ενεργοποίηση υπενθυμίσεων</string>
<string name="enable_reminders_description">Οι υπενθυμίσεις είναι απενεργοποιημένες στις Ρυθμίσεις Android</string>
<string name="enable_alarms">Ενημερωθείτε την κατάλληλη στιγμή</string>
<string name="enable_alarms_description">Για να βεβαιωθείτε ότι θα ειδοποιηθείτε την κατάλληλη στιγμή, παραχωρήστε άδεια δημιουργίας υπενθυμίσεων και ειδοποιήσεων στις Ρυθμίσεις</string>
<string name="sign_in">Συνδεθείτε</string>
<string name="consent_agree">Συμφωνώ</string>
<string name="consent_deny">Όχι τώρα</string>
<string name="sort_sorting">Ταξινόμηση ανά</string>
<string name="sort_grouping">Δημιουργία γκρουπ</string>
<string name="sort_ascending">Αύξουσα</string>
<string name="sort_descending">Φθίνουσα</string>
<string name="sort_not_available">Δεν είναι διαθέσιμο για ετικέτες, φίλτρα ή μέρη</string>
<string name="add_shortcut_to_home_screen">Προσθήκη συντόμευσης στην αρχική οθόνη</string>
<string name="add_widget_to_home_screen">Προσθήκη γραφικού στοιχείου στην αρχική οθόνη</string>
<string name="cost_free">Κόστος: Δωρεάν</string>
<string name="cost_money">Κόστος: $</string>
<string name="cost_more_money">Κόστος: $$$</string>
<string name="multiline_title">Επιτρέψτε τους τίτλους πολλαπλών γραμμών</string>
<string name="multiline_title_on">Πατήστε το πλήκτρο Enter για να προσθέσετε διακοπές γραμμής</string>
<string name="multiline_title_off">Πατήστε Τέλος για να αποθηκεύσετε την εργασία</string>
<string name="sync_warning_microsoft_title">Σχετικά με τον συγχρονισμό Microsoft To Do</string>
<string name="sync_warning_microsoft">Δεν συγχρονίζονται όλες οι λεπτομέρειες εργασίας με το Microsoft To Do.</string>
<string name="sync_warning_google_tasks_title">Σχετικά με τον συγχρονισμό Google Task</string>
<string name="sync_warning_google_tasks">Δεν συγχρονίζονται όλες οι λεπτομέρειες εργασίας με το Google Tasks</string>
<string name="button_learn_more">Μάθετε περισσότερα</string>
<string name="widget_view_more_tasks">Εμφάνιση περισσότερων εργασιών</string>
<string name="show_unstarted">Εμφάνιση όσων δεν έχουν ξεκινήσει</string>
<string name="widget_show_checkboxes">Εμφάνιση πλαισίων ελέγχου (κουτάκια)</string>
<string name="customize_edit_screen">Προσαρμογή οθόνης επεξεργασίας</string>
<string name="customize_edit_screen_summary">Αναδιάταξη ή κατάργηση πεδίων</string>
<string name="source_code">Πηγαίος κώδικας</string>
<string name="translations">Συνεισφέρετε μεταφράσεις</string>
<string name="contact_developer">Επικοινωνία με τον προγραμματιστή</string>
<string name="send_application_logs">Αποστολή αρχείων καταγραφής εφαρμογών</string>
<string name="rate_tasks">Βαθμολογήστε το Tasks</string>
<string name="quiet_hours_summary">Καθόλου υπενθυμίσεις κατά τη διάρκεια των ωρών ησυχίας</string>
<string name="randomly_every">Τυχαία κάθε %s</string>
<string name="randomly">Τυχαία</string>
<string name="pick_a_date_and_time">Επιλέξτε μια ημερομηνία και ώρα</string>
<string name="when_overdue">Όταν έχει καθυστερήσει</string>
<string name="when_due">Πότε πρέπει</string>
<string name="when_started">Πότε ξεκίνησε</string>
<string name="geofence_radius">Ακτίνα γεωφράγματος</string>
<string name="location_radius_meters">%s μ</string>
<string name="tags">Ετικέτες</string>
<string name="change_priority">Αλλαγή προτεραιότητας</string>
<string name="filters">Φίλτρα</string>
<string name="date_shortcut_hour">Για μια ώρα</string>
<string name="date_shortcut_morning">Πρωί</string>
<string name="date_shortcut_afternoon">Μεσημέρι</string>
<string name="date_shortcut_evening">Απόγευμα</string>
<string name="date_shortcut_night">Βράδυ</string>
<string name="date_shortcut_tomorrow_morning">Αύριο το πρωί</string>
<string name="date_shortcut_tomorrow_afternoon">Αύριο το μεσημέρι</string>
<string name="date_shortcut_tomorrow_evening">Αύριο το απόγευμα</string>
<string name="date_shortcut_tomorrow_night">Αύριο το βράδυ</string>
<string name="date_shortcut_must_come_before">%1$s πρέπει να είναι πριν από το %2$s</string>
<string name="date_shortcut_must_come_after">%1$s πρέπει να είναι μετά από το %2$s</string>
<string name="discard_changes">Απόρριψη αλλαγών;</string>
<string name="menu_discard_changes">Απόρριψη αλλαγών</string>
<string name="discard">Απόρριψη</string>
<string name="tag_settings">Ρυθμίσεις ετικέτας</string>
<string name="list_settings">Ρυθμίσεις λίστας</string>
<string name="delete">Διαγραφή</string>
<string name="copy">Αντιγραφή</string>
<string name="move">Μετακίνηση αλλού</string>
<string name="filter_settings">Ρυθμίσεις φίλτρων</string>
<string name="filter_criteria_unstarted">Δεν έχει ξεκινήσει</string>
<string name="no_app_found">Καμία εφαρμογή δεν μπορούσε να χειριστεί αυτό το αίτημα</string>
<string name="add_attachment">Προσθήκη συνημμένου</string>
<string name="take_a_picture">Βγάλτε μια φωτογραφία</string>
<string name="pick_from_gallery">Επιλογή από τη συλλογή</string>
<string name="pick_from_storage">Επιλογή από την αποθήκη αρχείων</string>
<string name="privacy_policy">Πολιτική απορρήτου</string>
<string name="send_anonymous_statistics">Βελτιώστε το Tasks</string>
<string name="send_anonymous_statistics_summary">Στείλτε ανώνυμα στατιστικά στοιχεία χρήσης και αναφορές σφαλμάτων για να συμβάλετε στη βελτίωση του Tasks. Δεν θα συλλεχθούν προσωπικά δεδομένα.</string>
<string name="tag_already_exists">Η ετικέτα υπάρχει ήδη</string>
<string name="name_cannot_be_empty">Το όνομα δεν μπορεί να είναι κενό</string>
<string name="username_required">Απαιτείται όνομα χρήστη</string>
<string name="password_required">Απαιτείται κωδικός</string>
<string name="url_required">Απαιτείται διεύθυνση URL</string>
<string name="url_host_name_required">Απαιτείται όνομα κεντρικού υπολογιστή</string>
<string name="url_invalid_scheme">Πρέπει να αρχίζει με http(s)://</string>
<string name="no_title">(Χωρίς τίτλο)</string>
<string name="back_button_saves_task">Το κουμπί \"Πίσω\" αποθηκεύει την εργασία</string>
<string name="show_edit_screen_without_unlock">Εμφάνιση οθόνης επεξεργασίας χωρίς ξεκλείδωμα</string>
<string name="show_edit_screen_without_unlock_summary">Ενεργοποιεί τη χρήση του πλακιδίου Γρήγορων Ρυθμίσεων χωρίς ξεκλείδωμα συσκευής</string>
<string name="default_tags">Προεπιλεγμένες ετικέτες</string>
<string name="default_recurrence">Προεπιλεγμένη επανάληψη</string>
<string name="filter">Φίλτρο</string>
<string name="opacity">Αδιαφάνεια</string>
<string name="opacity_header">Αδιαφάνεια κεφαλίδας</string>
<string name="opacity_row">Αδιαφάνεια σειράς</string>
<string name="opacity_footer">Αδιαφάνεια υποσέλιδου</string>
<string name="theme">Θέμα</string>
<string name="color">Χρώμα</string>
<string name="restart_now">Επανεκκίνηση τώρα</string>
<string name="restart_later">Αργότερα</string>
<string name="settings_localization">Εντοπισμός</string>
<string name="widget_settings">Ρυθμίσεις γραφικού στοιχείου</string>
<string name="widget_header_settings">Ρυθμίσεις κεφαλίδας</string>
<string name="widget_row_settings">Ρυθμίσεις σειράς</string>
<string name="widget_open_list">Άνοιγμα λίστας</string>
<string name="widget_due_date_after_title">Μετά τον τίτλο</string>
<string name="widget_due_date_below_title">Κάτω από τον τίτλο</string>
<string name="widget_due_date_hidden">Κρυμμένο</string>
<string name="clear_completed_tasks_confirmation">Διαγραφή ολοκληρωμένων εργασιών;</string>
</resources>

@ -547,7 +547,6 @@
<string name="repeats_single_on">Ripetiĝas %1$s je %2$s</string>
<string name="google_drive_backup">Savkopio de Google Drive</string>
<string name="repeats_single_until">Ripetiĝas %1$s, finiĝos %2$s</string>
<string name="got_it">Komprenite!</string>
<string name="backups_ignore_warnings_summary">Ignori avertojn pri savkopioj se vi ne bezonas savkopiojn aŭ jam havas propran savkopian solvon</string>
<string name="error_adding_account">Eraro: %s</string>
<string name="logout_warning">Ĉiu de la datumo de ĉi tiu konto foriĝos el via aparato</string>

@ -490,7 +490,6 @@
<string name="filter_today_only">Hoy sólo</string>
<string name="filter_overdue">Atrasada</string>
<string name="sort_created">En el momento de la creación</string>
<string name="got_it">¡Entendido!</string>
<string name="support_development_subscribe">Desbloquee funciones adicionales y apoye el software de código abierto</string>
<string name="enjoying_tasks">¿Disfruta Tasks\?</string>
<string name="whats_new">Qué hay de nuevo</string>

@ -446,7 +446,6 @@
<item quantity="other">aastat</item>
</plurals>
<string name="widget_due_date_after_title">Pealkirja järel</string>
<string name="got_it">Selge!</string>
<string name="default_recurrence">Vaikekordus</string>
<string name="authorization_cancelled">Autoriseerimine tühisatud</string>
<string name="migrating_tasks">Ülesanded on kolimisel</string>

@ -479,7 +479,6 @@
<string name="widget_show_dividers">Erakutsi banatzaileak</string>
<string name="widget_show_menu">Erakutsi menua</string>
<string name="sort_created">Sorrera dataren arabera</string>
<string name="got_it">Ulertuta!</string>
<string name="support_development_subscribe">Desblokeatu ezaugarri gehigarriak eta babestu software librea</string>
<string name="enjoying_tasks">Tasks gogoko\?</string>
<string name="whats_new">Zer dago berri</string>

@ -426,7 +426,6 @@
<string name="widget_show_menu">Näytä valikko</string>
<string name="sort_created">Luontiajan mukaan</string>
<string name="widget_show_dividers">Näytä jakajat</string>
<string name="got_it">Selvä!</string>
<string name="action_new_task">Uusi tehtävä</string>
<string name="pro_dashclock_extension">Dashclock-laajennus</string>
<string name="upgrade_to_pro">Päivitä pro versioon</string>

@ -490,7 +490,6 @@
<string name="filter_today_only">Uniquement aujourd\'hui</string>
<string name="filter_overdue">En retard</string>
<string name="sort_created">Par date de création</string>
<string name="got_it">J\'ai compris !</string>
<string name="support_development_subscribe">Déverrouillez des fonctionnalités supplémentaires et soutenez les logiciels à code source ouvert</string>
<string name="enjoying_tasks">Appréciez-vous Tasks \?</string>
<string name="whats_new">Nouveautés</string>

@ -598,7 +598,6 @@
<string name="donate_today">Doa hoxe</string>
<string name="donate_maybe_later">Quizais máis tarde</string>
<string name="support_development_subscribe">Desbloquea funcións adicionais e apoia software de código aberto</string>
<string name="got_it">Entendido!</string>
<string name="sort_completion_group">Completado %s</string>
<string name="on_launch">Ao iniciar</string>
<string name="permission_read_tasks">Acceso completo á base de datos de Tasks</string>

@ -421,7 +421,6 @@
<string name="widget_due_date_after_title">לאחר הכותרת</string>
<string name="chip_appearance_icon_only">סמל בלבד</string>
<string name="back">חזרה</string>
<string name="got_it">הבנתי!</string>
<string name="support_development_subscribe">פתיחת תכונות נוספות ותמיכה בתוכנות קוד פתוח</string>
<string name="filter_no_priority">אין עדיפות</string>
<string name="filter_low_priority">עדיפות נמוכה</string>

@ -479,7 +479,6 @@
<string name="sort_created_group">Stvoreno %s</string>
<string name="sort_due_group">Rok %s</string>
<string name="sort_start_group">Pokreni %s</string>
<string name="got_it">Razumijem!</string>
<string name="support_development_subscribe">Otključaj dodatne funkcije i podrži softver otvorenog koda</string>
<string name="enjoying_tasks">Sviđa ti se Tasks\?</string>
<string name="filter_eisenhower_box_4">Nije važno i nije hitno</string>

@ -474,7 +474,6 @@
<string name="filter_today_only">Csak ma</string>
<string name="filter_overdue">Határidőn túli</string>
<string name="sort_created">Létrehozás ideje alapján</string>
<string name="got_it">Megértettem!</string>
<string name="support_development_subscribe">További finkciók feloldása és a nyílt szoftver támogatása</string>
<string name="enjoying_tasks">Tetszik a Tasks\?</string>
<string name="whats_new">Újdonságok</string>

@ -476,7 +476,6 @@
<string name="sort_created_group">Dibuat %s</string>
<string name="sort_due_group">Jatuh tempo %s</string>
<string name="sort_start_group">Mulai %s</string>
<string name="got_it">Ok Kaka!</string>
<string name="support_development_subscribe">Temukan fitur tambahan dan dukung perangkat lunak sumber terbuka</string>
<string name="enjoying_tasks">Menikmati Tasks\?</string>
<string name="filter_eisenhower_box_4">Tidak penting dan tidak mendesak</string>

@ -490,7 +490,6 @@
<string name="on_launch">All\'avvio</string>
<string name="sort_modified_group">Modificato %s</string>
<string name="sort_created_group">Creato %s</string>
<string name="got_it">Capito!</string>
<string name="support_development_subscribe">Sblocca funzionalità aggiuntive e supporta il software open source</string>
<string name="enjoying_tasks">Ti piace Tasks\?</string>
<string name="filter_no_priority">Senza priorità</string>

@ -383,7 +383,6 @@
<string name="name_your_price">価格はあなた次第</string>
<string name="cancel">キャンセル</string>
<string name="email">電子メール</string>
<string name="got_it">わかりました!</string>
<string name="add_tags">タグを追加</string>
<string name="filter_no_tags">タグなし</string>
<string name="hide_unused_tags">使われていないタグを隠す</string>

@ -429,7 +429,6 @@
<string name="theme_system_default">시스템 기본설정</string>
<string name="menu_discard_changes">변경사항 폐기</string>
<string name="support_development_subscribe">부가 기능을 해제하고 오픈소스 소프트웨어를 지원하십시오</string>
<string name="got_it">확인했습니다!</string>
<string name="compact">촘촘하게</string>
<string name="settings_default">기본값</string>
<string name="sort_modified_group">%s 변경</string>

@ -594,7 +594,6 @@
<string name="sort_created_group">Sukurta %s</string>
<string name="sort_due_group">Terminas %s</string>
<string name="sort_start_group">Pradžia %s</string>
<string name="got_it">Supratau!</string>
<string name="support_development_subscribe">Atrakinkite papildomas funkcijas ir palaikykite atvirojo kodo programinę įrangą</string>
<string name="enjoying_tasks">Ar jums patinka Tasks\?</string>
<string name="calendar_event_created">Kalendoriaus įvykis sukurtas %s</string>

@ -479,7 +479,6 @@
<string name="auto_dismiss_datetime_edit">Gjøremålsredigering</string>
<string name="place_settings">Stedsinnstillinger</string>
<string name="places">Steder</string>
<string name="got_it">Skjønner!</string>
<string name="sort_modified_group">Endret %s</string>
<string name="sort_created_group">Opprettet %s</string>
<string name="reset_sort_order">Tilbakestill sorteringsrekkefølge</string>

@ -475,7 +475,6 @@
<string name="filter_overdue">Vervallen</string>
<string name="sort_created">Op aanmaaktijd</string>
<string name="support_development_subscribe">Ontgrendel extra mogelijkheden en ondersteun opensource software</string>
<string name="got_it">Begrepen!</string>
<string name="whats_new">Wat is nieuw</string>
<string name="enjoying_tasks">Gebruik je Tasks graag\?</string>
<string name="action_new_task">Nieuwe taak</string>

@ -163,7 +163,6 @@
<string name="color_wheel">ରଙ୍ଗ ଚକ୍ର</string>
<string name="custom_filter_and">ଏଵଂ</string>
<string name="filter_overdue">ଅତିଦେୟ</string>
<string name="got_it">ବୁଝିଗଲି!</string>
<string name="last_backup">ଗତ ବ୍ୟାକଅପ୍: %s</string>
<string name="invite_awaiting_response">ନିମନ୍ତ୍ରଣ ପ୍ରତିକ୍ରିୟାର ଅପେକ୍ଷାରେ</string>
<string name="consent_agree">ସହମତ</string>

@ -490,7 +490,6 @@
<string name="custom_filter_and">ORAZ</string>
<string name="custom_filter_criteria">Kryteria filtrowania</string>
<string name="sort_created">Wg czasu utworzenia</string>
<string name="got_it">Rozumiem!</string>
<string name="support_development_subscribe">Odblokuj dodatkowe funkcje i wesprzyj oprogramowanie open source</string>
<string name="enjoying_tasks">Podoba Ci się Tasks\?</string>
<string name="whats_new">Co nowego</string>

@ -482,7 +482,6 @@
<string name="on_launch">Ao iniciar</string>
<string name="sort_modified_group">Modificado em %s</string>
<string name="sort_created_group">Criado em %s</string>
<string name="got_it">Entendi!</string>
<string name="support_development_subscribe">Desbloquear funcionalidades adicionais e oferecer suporte a software de código aberto</string>
<string name="enjoying_tasks">Curtindo Tasks\?</string>
<string name="filter_eisenhower_box_4">Não é importante e não é urgente</string>

@ -478,7 +478,6 @@
<string name="permission_read_tasks">Acesso completo à base de dados do Tasks</string>
<string name="reset_sort_order">Repor ordenação</string>
<string name="open_last_viewed_list">Abrir a última lista vista</string>
<string name="got_it">Entendi!</string>
<string name="support_development_subscribe">Desbloqueie funcionalidades adicionais e apoie programas de código-fonte aberto</string>
<string name="enjoying_tasks">Gosta do Tasks\?</string>
<string name="filter_eisenhower_box_4">Não é importante e não é urgente</string>
@ -701,7 +700,7 @@
<string name="button_learn_more">Saiba mais</string>
<string name="add_widget_to_home_screen">Adicionar widget à tela inicial</string>
<string name="enable_alarms">Receba notificações na hora certa</string>
<string name="banner_app_updated_title">Aplicativo atualizado</string>
<string name="banner_app_updated_title">Aplicação atualizada</string>
<string name="cost_money">Custo: R$</string>
<string name="multiline_title">Permitir títulos em várias linhas</string>
<string name="multiline_title_on">Pressione a tecla Enter para adicionar quebras de linha</string>
@ -715,11 +714,11 @@
<string name="donate_today">Doe hoje</string>
<string name="customize_drawer_summary">Arraste e solte para reorganizar os itens do menu</string>
<string name="swipe_to_snooze_time_description">Uma notificação desmarcada será adiada e recriada em %s</string>
<string name="app_settings">Configurações do aplicativo</string>
<string name="app_settings">Configurações da aplicação</string>
<string name="comment">Comentário</string>
<string name="add_shortcut_to_home_screen">Adicionar atalho para a tela inicial</string>
<string name="cost_free">Custo: Grátis</string>
<string name="send_application_logs">Enviar logs do aplicativo</string>
<string name="send_application_logs">Enviar logs da aplicação</string>
<string name="help_me_choose">Ajude-me a escolher</string>
<string name="continue_without_sync">Continuar sem sync</string>
<string name="delete_tasks_warning">%s será apagado. Isso não poderá ser desfeito!</string>

@ -482,7 +482,6 @@
<string name="sort_created_group">Creat %s</string>
<string name="sort_due_group">Scadență %s</string>
<string name="sort_start_group">Start %s</string>
<string name="got_it">Am înțeles!</string>
<string name="support_development_subscribe">Deblochează funcții suplimentare și sprijină software-ul cu sursă deschisă</string>
<string name="enjoying_tasks">Îți place Tasks\?</string>
<string name="filter_eisenhower_box_4">Nu este important și nu este urgent</string>

@ -491,7 +491,6 @@
<string name="custom_filter_and">И</string>
<string name="custom_filter_criteria">Критерии фильтрации</string>
<string name="sort_created">Дата создания</string>
<string name="got_it">Понятно!</string>
<string name="support_development_subscribe">Разблокировать дополнительные функции и поддержать ПО с открытым исходным кодом</string>
<string name="enjoying_tasks">Нравится Tasks\?</string>
<string name="whats_new">Что нового</string>

@ -118,7 +118,6 @@
<string name="permission_read_tasks">Tasks දත්ත ගබඩාවට පූර්ණ ප්‍රවේශය</string>
<string name="reset_sort_order">වර්ග කිරීමේ අනුපිළිවෙල නැවත සකසන්න</string>
<string name="lists">ලැයිස්තු</string>
<string name="got_it">තේරුම් ගත්තා!</string>
<string name="support_development_subscribe">අමතර විශේෂාංග අගුළු ඇරීම සහ විවෘත මෘදුකාංග සඳහා සහාය වීම</string>
<string name="enjoying_tasks">Tasks සතුටුදායකද\?</string>
<string name="filter_eisenhower_box_4">වැදගත් නොවන සහ හදිසි නොවන</string>

@ -506,7 +506,6 @@
<string name="next_tuesday">Budúci utorok</string>
<string name="date_shortcut_tomorrow_night">Zajtra v noci</string>
<string name="default_location">Predvolené miesto</string>
<string name="got_it">Rozumiem!</string>
<string name="sort_completion_group">Dokončené %s</string>
<string name="reset_sort_order">Obnoviť triedenie</string>
<string name="repeat_monthly_on_day_number">%1d. deň v mesiaci</string>

@ -607,7 +607,6 @@
<string name="donate_today">Донирај данас</string>
<string name="donate_maybe_later">Можда касније</string>
<string name="support_development_subscribe">Откључај додатне функционалности и подржи софтвер отвореног кода</string>
<string name="got_it">Јасно!</string>
<string name="sort_start_group">Старт %s</string>
<string name="sort_due_group">Доспеће %s</string>
<string name="sort_created_group">Креирано %s</string>

@ -506,7 +506,6 @@
<string name="on_launch">Vid start</string>
<string name="sort_due_group">Förfaller %s</string>
<string name="sort_start_group">Starta %s</string>
<string name="got_it">Förstått!</string>
<string name="support_development_subscribe">Lås upp ytterligare funktioner och stöd programvara med öppen källkod</string>
<string name="enjoying_tasks">Gillar du Tasks\?</string>
<string name="filter_eisenhower_box_4">Inte viktigt och inte brådskande</string>

@ -137,7 +137,6 @@
<string name="on_launch">தொடங்கும்போது</string>
<string name="sort_modified_group">மாற்றியமைக்கப்பட்ட %s</string>
<string name="sort_created_group">%s உருவாக்கப்பட்டது</string>
<string name="got_it">அறிந்துகொண்டேன்!</string>
<string name="enjoying_tasks">பணிகளை அனுபவிக்கிறீர்களா\?</string>
<string name="filter_eisenhower_box_4">முக்கியமற்றது மற்றும் அவசரமற்றது</string>
<string name="filter_eisenhower_box_3">முக்கியமற்றது மற்றும் அவசரமானது</string>

@ -119,7 +119,6 @@
<string name="sort_created_group">สร้าง %s แล้ว</string>
<string name="sort_due_group">ครบกําหนด %s</string>
<string name="sort_start_group">เริ่ม %s</string>
<string name="got_it">เข้าใจแล้ว!</string>
<string name="support_development_subscribe">ปลดล็อกคุณสมบัติเพิ่มเติมและสนับสนุนซอฟต์แวร์โอเพ่นซอร์ส</string>
<string name="enjoying_tasks">สนุกกับงาน\?</string>
<string name="filter_eisenhower_box_4">ไม่สําคัญและไม่เร่งด่วน</string>

@ -474,7 +474,6 @@
<string name="custom_filter_and">VE</string>
<string name="custom_filter_criteria">Süzme ölçütü</string>
<string name="sort_created">Oluşturulma zamanına göre</string>
<string name="got_it">Anladım!</string>
<string name="support_development_subscribe">Ek özellikleri açın ve açık kaynaklı yazılımı destekleyin</string>
<string name="enjoying_tasks">Tasks\'ı Beğeniyor Musunuz\?</string>
<string name="whats_new">Yenilikler</string>

@ -511,7 +511,6 @@
<string name="on_launch">Під час запуску</string>
<string name="sort_modified_group">Змінено %s</string>
<string name="sort_created_group">Створено %s</string>
<string name="got_it">Зрозуміло!</string>
<string name="support_development_subscribe">Розблокувати додаткові функції та підтримати програмне забезпечення з відкритим джерельним кодом</string>
<string name="enjoying_tasks">Подобається Tasks\?</string>
<string name="filter_eisenhower_box_4">Не важливе і не термінове</string>

@ -141,7 +141,6 @@
<string name="sort_created_group">Đã tạo %s</string>
<string name="sort_due_group">Hạn %s</string>
<string name="sort_start_group">Bắt đầu %s</string>
<string name="got_it">Hiểu rồi!</string>
<string name="support_development_subscribe">Mở khoá các tính năng bổ sung và hỗ trợ phần mềm mã nguồn mở</string>
<string name="enjoying_tasks">Đang tận hưởng Tasks\?</string>
<string name="filter_eisenhower_box_4">Không quan trọng và không gấp</string>
@ -659,4 +658,7 @@
<string name="sort_descending">Giảm dần</string>
<string name="sort_completed">Theo thời gian hoàn thành</string>
<string name="change_priority">Thay đổi mức độ ưu tiên</string>
<string name="app_settings">Cài đặt ứng dụng</string>
<string name="customize_drawer">Tùy chỉnh ngăn kéo</string>
<string name="customize_drawer_summary">Kéo và thả để sắp xếp lại các mục danh sách</string>
</resources>

@ -469,7 +469,6 @@
<string name="filter_today_only">仅今日</string>
<string name="filter_overdue">已过期</string>
<string name="sort_created">按创建时间</string>
<string name="got_it">知道了!</string>
<string name="support_development_subscribe">解锁额外功能并支持开源软件</string>
<string name="enjoying_tasks">享受Tasks\?</string>
<string name="whats_new">更新日志</string>

@ -303,7 +303,6 @@
<string name="lists">清單</string>
<string name="open_last_viewed_list">開器最後觀看的清單</string>
<string name="on_launch">啟動時</string>
<string name="got_it">知道了!</string>
<string name="support_development_subscribe">解鎖附加功能和支持開源軟體</string>
<string name="enjoying_tasks">喜歡 Tasks 嗎?</string>
<string name="filter_eisenhower_box_4">不重要不急迫</string>

@ -36,8 +36,9 @@
<string name="url_filters">https://tasks.org/filters</string>
<string name="url_documentation">https://tasks.org/</string>
<string name="url_issue_tracker">https://github.com/tasks/tasks/issues</string>
<string name="url_changelog">https://tasks.org/changelog</string>
<string name="url_source_code">https://tasks.org/source</string>
<string name="url_privacy_policy">https://tasks.org/privacy.html</string>
<string name="url_privacy_policy">https://tasks.org/privacy</string>
<string name="url_backups">https://tasks.org/backups</string>
<string name="url_translations">https://tasks.org/translations</string>
<string name="url_notifications">https://tasks.org/notifications</string>

@ -593,7 +593,6 @@ File %1$s contained %2$s.\n\n
<string name="donate_today">Donate today</string>
<string name="donate_maybe_later">Maybe later</string>
<string name="support_development_subscribe">Unlock additional features and support open source software</string>
<string name="got_it">Got it!</string>
<string name="sort_start_group">Start %s</string>
<string name="sort_due_group">Due %s</string>
<string name="sort_created_group">Created %s</string>

@ -1,9 +1,12 @@
package org.tasks.time
import org.junit.Assert.*
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.Freeze
import org.tasks.TestUtilities.withTZ
import java.util.TimeZone
import java.util.concurrent.TimeUnit
class DateTimeTest {
@ -216,7 +219,9 @@ class DateTimeTest {
@Test
fun testMinusMinutes() {
assertEquals(
DateTime(2015, 11, 4, 23, 59, 0), DateTime(2015, 11, 5, 0, 1, 0).minusMinutes(2))
DateTime(2015, 11, 4, 23, 59, 0, timeZone = DateTime.UTC),
DateTime(2015, 11, 5, 0, 1, 0, timeZone = DateTime.UTC).minusMinutes(2)
)
}
@Test
@ -289,16 +294,21 @@ class DateTimeTest {
@Test
fun testMinusMillis() {
assertEquals(
DateTime(2015, 11, 6, 16, 18, 20, 452),
DateTime(2015, 11, 6, 16, 18, 21, 374).minusMillis(922))
DateTime(2015, 11, 6, 16, 18, 20, 452, DateTime.UTC),
DateTime(2015, 11, 6, 16, 18, 21, 374, DateTime.UTC).minusMillis(922)
)
}
@Test
fun testMinusDays() {
assertEquals(
DateTime(2015, 11, 6, 16, 19, 16), DateTime(2015, 12, 4, 16, 19, 16).minusDays(28))
DateTime(2015, 11, 6, 16, 19, 16, timeZone = DateTime.UTC),
DateTime(2015, 12, 4, 16, 19, 16, timeZone = DateTime.UTC).minusDays(28)
)
assertEquals(
DateTime(2015, 11, 6, 16, 19, 16), DateTime(2015, 11, 7, 16, 19, 16).minusDays(1))
DateTime(2015, 11, 6, 16, 19, 16, timeZone = DateTime.UTC),
DateTime(2015, 11, 7, 16, 19, 16, timeZone = DateTime.UTC).minusDays(1)
)
}
@Test
@ -344,14 +354,127 @@ class DateTimeTest {
@Test
fun testStartOfMinute() {
assertEquals(
DateTime(2017, 9, 3, 0, 51, 0, 0),
DateTime(2017, 9, 3, 0, 51, 13, 427).startOfMinute())
DateTime(2017, 9, 3, 0, 51, 0, 0, DateTime.UTC),
DateTime(2017, 9, 3, 0, 51, 13, 427, DateTime.UTC).startOfMinute()
)
}
@Test
fun testEndOfMinute() {
assertEquals(
DateTime(2017, 9, 22, 14, 47, 59, 999),
DateTime(2017, 9, 22, 14, 47, 14, 453).endOfMinute())
DateTime(2017, 9, 22, 14, 47, 59, 999, DateTime.UTC),
DateTime(2017, 9, 22, 14, 47, 14, 453, DateTime.UTC).endOfMinute()
)
}
@Test
fun startOfDayPreservesTimezone() {
val utcDateTime = DateTime(2024, 12, 20, 14, 30, timeZone = DateTime.UTC)
val result = utcDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20, timeZone = DateTime.UTC), result)
}
@Test
fun startOfDayInDefaultTimezone() {
val dateTime = DateTime(2024, 12, 20, 14, 30)
val result = dateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20), result)
}
@Test
fun startOfDayWithUTCTimezone() {
withTZ("America/Chicago") { // UTC-6
val utcDateTime = DateTime(2024, 12, 20, 15, timeZone = DateTime.UTC)
val result = utcDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20, timeZone = DateTime.UTC), result)
}
}
@Test
fun startOfDayBeforeUTC() {
withTZ("America/New_York") { // UTC-5
val nyDateTime = DateTime(2024, 12, 20, 15)
val result = nyDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20), result)
}
}
@Test
fun startOfDayAfterUTC() {
withTZ("Europe/Berlin") { // UTC+1
val berlinDateTime = DateTime(2024, 12, 20, 15)
val result = berlinDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20), result)
}
}
@Test
fun startOfDayWithDateBoundaryWrap() {
withTZ("Pacific/Auckland") { // UTC+13
val aucklandDateTime = DateTime(2024, 12, 20, 12)
val result = aucklandDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20), result)
}
}
@Test
fun startOfDayRespectsTimezoneNotSystemDefault() {
withTZ("America/New_York") { // UTC-5
val berlinTz = TimeZone.getTimeZone("Europe/Berlin") // UTC+1
val berlinDateTime = DateTime(2024, 12, 20, 1, timeZone = berlinTz)
val result = berlinDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20, timeZone = berlinTz), result)
}
}
@Test
fun startOfDayWithExplicitTimezone() {
withTZ("Europe/Berlin") { // UTC+1
val localMidnight = DateTime(2024, 12, 20)
val utcMidnight = localMidnight.startOfDay(DateTime.UTC)
assertEquals(DateTime(2024, 12, 20, timeZone = DateTime.UTC), utcMidnight)
}
}
@Test
fun startOfDayWithExplicitTimezoneFromAuckland() {
withTZ("Pacific/Auckland") { // UTC+13
val localMidnight = DateTime(2024, 12, 20)
val utcMidnight = localMidnight.startOfDay(DateTime.UTC)
assertEquals(DateTime(2024, 12, 20, timeZone = DateTime.UTC), utcMidnight)
}
}
@Test
fun startOfDayWithExplicitTimezoneFromHonolulu() {
withTZ("Pacific/Honolulu") { // UTC-10
val localMidnight = DateTime(2024, 12, 20)
val utcMidnight = localMidnight.startOfDay(DateTime.UTC)
assertEquals(DateTime(2024, 12, 20, timeZone = DateTime.UTC), utcMidnight)
}
}
@Test
fun noon() {
assertEquals(
DateTime(2024, 12, 20, 12, timeZone = DateTime.UTC),
DateTime(2024, 12, 20, 8, 30, 45, 123, DateTime.UTC).noon()
)
}
@Test
fun endOfDay() {
assertEquals(
DateTime(2024, 12, 20, 23, 59, 59, timeZone = DateTime.UTC),
DateTime(2024, 12, 20, 8, 30, 45, 123, DateTime.UTC).endOfDay()
)
}
@Test
fun millisOfDayRespectsTimezone() {
val instant = DateTime(2024, 12, 20, 12, timeZone = DateTime.UTC)
val berlinDateTime = DateTime(instant.millis, TimeZone.getTimeZone("Europe/Berlin")) // UTC+1
assertEquals(12 * 3600000, instant.millisOfDay)
assertEquals(13 * 3600000, berlinDateTime.millisOfDay)
}
}
}

@ -15,13 +15,9 @@ kotlin {
applyDefaultHierarchyTemplate()
androidTarget {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
freeCompilerArgs.addAll("-P", "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=org.tasks.CommonParcelize")
}
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
jvm()
sourceSets {

@ -68,7 +68,7 @@ abstract class GoogleTaskDao {
@Query("UPDATE caldav_tasks SET gt_moved = 1 WHERE cd_task = :task and cd_calendar = :list")
internal abstract suspend fun setMoved(task: Long, list: String)
@Query("SELECT caldav_tasks.* FROM caldav_tasks INNER JOIN caldav_lists ON cdl_uuid = cd_calendar INNER JOIN caldav_accounts ON cda_uuid = cdl_account WHERE cd_task = :taskId AND cd_deleted = 0 AND cda_account_type = $TYPE_GOOGLE_TASKS LIMIT 1")
@Query("SELECT caldav_tasks.* FROM caldav_tasks INNER JOIN caldav_lists ON cdl_uuid = cd_calendar INNER JOIN caldav_accounts ON cda_uuid = cdl_account WHERE cd_task = :taskId AND cd_deleted = 0 AND cda_account_type = $TYPE_GOOGLE_TASKS")
abstract suspend fun getByTaskId(taskId: Long): CaldavTask?
@Update
@ -80,8 +80,8 @@ abstract class GoogleTaskDao {
@Delete
abstract suspend fun delete(deleted: CaldavTask)
@Query("SELECT * FROM caldav_tasks WHERE cd_remote_id = :remoteId LIMIT 1")
abstract suspend fun getByRemoteId(remoteId: String): CaldavTask?
@Query("SELECT * FROM caldav_tasks WHERE cd_remote_id = :remoteId AND cd_calendar = :calendar")
abstract suspend fun getByRemoteId(remoteId: String, calendar: String): CaldavTask?
@Query("""
SELECT caldav_tasks.*
@ -115,11 +115,11 @@ ORDER BY `order` DESC
)
abstract suspend fun getPrevious(listId: String, parent: Long, order: Long): String?
@Query("SELECT cd_remote_id FROM caldav_tasks WHERE cd_task = :task")
abstract suspend fun getRemoteId(task: Long): String?
@Query("SELECT cd_remote_id FROM caldav_tasks WHERE cd_task = :task AND cd_calendar = :calendar")
abstract suspend fun getRemoteId(task: Long, calendar: String): String?
@Query("SELECT cd_task FROM caldav_tasks WHERE cd_remote_id = :remoteId")
abstract suspend fun getTask(remoteId: String): Long?
@Query("SELECT cd_task FROM caldav_tasks WHERE cd_remote_id = :remoteId AND cd_calendar = :calendar")
abstract suspend fun getTask(remoteId: String, calendar: String): Long?
@Query(
"""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,9 @@
* Widget performance improvements
* What's New now opens changelog on GitHub
* Fix automatically opening keyboard for new tasks
* Fix parent-child cycle causing crash
* Fix state restoration issue
* Fix all day calendar entries created on previous day
* Fix Microsoft To Do and Google Task sync errors
* Fix multiple icons missing from widget
* Update translations

@ -0,0 +1 @@
Initial WearOS release - work in progress!

@ -1,17 +1,17 @@
[versions]
versionCode = "140824" # increment by 2
versionName = "14.8.4"
versionCode = "140828" # increment by 2
versionName = "14.8.5"
agp = "8.13.2"
android-compileSdk = "36"
android-minSdk = "26"
android-targetSdk = "35"
accompanist = "0.37.3"
activity-compose = "1.11.0"
activity-compose = "1.12.2"
appauth = "0.11.1"
appcompat = "1.7.1"
cert4android = "7814052"
coil = "2.7.0"
compose = "2025.11.01"
compose = "2025.12.01"
constraintlayout = "2.2.1"
dagger-hilt = "2.57.2"
dashclock-api = "2.0.0"
@ -21,22 +21,19 @@ etebase = "2.3.2"
firebase = "33.16.0"
firebase-crashlytics-gradle = "3.0.6"
google-oauth2 = "1.39.0"
google-api-drive = "v3-rev20251019-2.0.0"
google-api-drive = "v3-rev20251210-2.0.0"
google-api-tasks = "v1-rev20251102-2.0.0"
google-services = "4.4.4"
grpc = "1.73.0"
hilt = "1.3.0"
horologist = "0.7.15"
ical4android = "fcb0311ca7"
jchronic = "0.2.6"
jems = "1.33"
junit-junit = "4.13.2"
junit = "1.3.0"
kotlin = "2.1.21"
kotlin = "2.2.20"
kotlinx-coroutines = "1.10.2"
ktor = "3.3.3"
leakcanary = "2.14"
lib-recur = "0.11.4"
lifecycle = "2.10.0"
locale = "1.0.4"
make-it-easy = "4.0.1"
@ -54,7 +51,6 @@ play-services-oss-licenses = "17.3.0"
preference = "1.2.1"
protobuf = "4.33.2"
recyclerview = "1.4.0"
rfc5545-datetime = "0.2.4"
room = "2.8.4"
shortcut-badger = "1.1.22"
timber = "5.0.1"
@ -74,8 +70,9 @@ androidx-compose = { module = "androidx.compose:compose-bom", version.ref = "com
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout-android", version = "1.2.0" }
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
androidx-core-remoteviews = { group = "androidx.core", name = "core-remoteviews", version = "1.1.0" }
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.2.0" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version = "1.1.7" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version = "1.2.0" }
androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version = "1.8.9" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt" }
androidx-hilt-navigation = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt" }
@ -113,10 +110,13 @@ dagger-hilt-gradle = { module = "com.google.dagger:hilt-android-gradle-plugin",
dagger-hilt-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "dagger-hilt" }
dashclock-api = { module = "com.google.android.apps.dashclock:dashclock-api", version.ref = "dashclock-api" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
dmfs-jems = { module = "org.dmfs:jems", version.ref = "jems" }
#noinspection NewerVersionAvailable
dmfs-jems = { module = "org.dmfs:jems", version = "1.33" }
dmfs-opentasks-provider = { module = "com.github.tasks.opentasks:opentasks-provider", version.ref = "opentasks" }
dmfs-recur = { module = "org.dmfs:lib-recur", version.ref = "lib-recur" }
dmfs-rfc5545-datetime = { module = "org.dmfs:rfc5545-datetime", version.ref = "rfc5545-datetime" }
#noinspection NewerVersionAvailable
dmfs-recur = { module = "org.dmfs:lib-recur", version = "0.11.4" }
#noinspection NewerVersionAvailable
dmfs-rfc5545-datetime = { module = "org.dmfs:rfc5545-datetime", version = "0.2.4" }
etebase = { module = "com.etebase:client", version.ref = "etebase" }
firebase = { module = "com.google.firebase:firebase-bom", version.ref = "firebase" }
firebase-config-ktx = { module = "com.google.firebase:firebase-config-ktx" }
@ -135,7 +135,8 @@ horologist-datalayer-core = { group = "com.google.android.horologist", name = "h
horologist-datalayer-grpc = { group = "com.google.android.horologist", name = "horologist-datalayer-grpc", version.ref = "horologist" }
horologist-datalayer-phone = { group = "com.google.android.horologist", name = "horologist-datalayer-phone", version.ref = "horologist" }
horologist-datalayer-watch = { group = "com.google.android.horologist", name = "horologist-datalayer-watch", version.ref = "horologist" }
jchronic = { module = "com.rubiconproject.oss:jchronic", version.ref = "jchronic" }
#noinspection NewerVersionAvailable
jchronic = { module = "com.rubiconproject.oss:jchronic", version = "0.2.6" }
junit = { module = "junit:junit", version.ref = "junit-junit" }
kermit = { module = "co.touchlab:kermit", version = "2.0.8" }
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
@ -209,8 +210,8 @@ kotlin-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version = "2.1.21-2.0.1" }
ksp = { id = "com.google.devtools.ksp", version = "2.2.20-2.0.3" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
protobuf = { id = "com.google.protobuf", version = "0.9.6" }
redacted = { id = "dev.zacsweers.redacted", version = "1.13.0" }
redacted = { id = "dev.zacsweers.redacted", version = "1.15.1" }

@ -25,8 +25,10 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
}

@ -254,6 +254,11 @@ object OutlinedGoogleMaterial2 : ITypeface {
gmo_zoom_out('\uf104'),
gmo_zoom_out_map('\uf103'),
gmo_local_grocery_store('\uf3e2'), // mapped to 'gmo_shopping_cart'
gmo_favorite_border('\ufa0b'), // mapped to 'gmo_favorite'
gmo_card_giftcard('\uf4e9'), // mapped to 'gmo_redeem'
gmo_wb_cloudy('\ufbd8'), // mapped to 'gmo_cloud'
gmo_nightlight_round('\uf667'), // mapped to 'gmo_nightlight'
gmo_tungsten('\uf152'), // mapped to 'gmo_wb_incandescent'
;
override val typeface: ITypeface by lazy { OutlinedGoogleMaterial2 }

@ -7,6 +7,30 @@
<string name="tmrw">Αύριο</string>
<string name="tomorrow">Αύριο</string>
<string name="yest">Χθές</string>
<string name="yesterday">Χθές</string>
<string name="yesterday">Εχθές</string>
<string name="today">Σήμερα</string>
</resources>
<string name="search_no_results">Δεν βρέθηκαν αποτελέσματα</string>
<string name="requires_pro_subscription">Δυνατότητα χρήσης μέσω Συνδρομής</string>
<string name="subscription_required_description">Ξεκλειδώστε όλες τις δυνατότητες με τόσα λίγα όσο $1 USD/ χρόνο</string>
<string name="subscribe">Συνδρομή</string>
<string name="filter_my_tasks">Οι Εργασίες μου</string>
<string name="filter_notifications">Ειδοποιήσεις</string>
<string name="filter_snoozed">Υπενθυμίσεις που έχουν αναβληθεί</string>
<string name="filter_recently_modified">Πρόσφατα επεξεργασμένα</string>
<string name="default_list">Προεπιλεγμένη λίστα</string>
<string name="help_and_feedback">Βοήθεια &amp; Καταχώρηση απόψεων</string>
<string name="drawer_filters">Φίλτρα</string>
<string name="drawer_tags">Ετικέτες</string>
<string name="drawer_places">Μέρη</string>
<string name="drawer_local_lists">Τοπικές λίστες</string>
<string name="tomorrow_abbrev_lowercase">αύρ.</string>
<string name="tomorrow_lowercase">αύριο</string>
<string name="yesterday_abbrev_lowercase">χθες</string>
<string name="yesterday_lowercase">εχθές</string>
<string name="today_lowercase">σήμερα</string>
<string name="add_task">Προσθήκη εργασίας</string>
<string name="wear_install_app">Εγκατάσταση στο τηλέφωνο</string>
<string name="wear_unknown_error">Άγνωστο σφάλμα</string>
<string name="show_completed">Εμφάνιση ολοκληρωμένων</string>
<string name="show_unstarted">Εμφάνιση όσων δεν έχουν ξεκινήσει</string>
</resources>

@ -27,4 +27,6 @@
<string name="today_lowercase">hôm nay</string>
<string name="show_unstarted">Hiện mục chưa bắt đầu</string>
<string name="show_completed">Hiện mục đã hoàn thành</string>
</resources>
<string name="search_no_results">Không có kết quả</string>
<string name="add_task">Thêm nhiệm vụ</string>
</resources>

@ -40,19 +40,20 @@ object TaskListQuery {
fun getQuery(
preferences: QueryPreferences,
filter: Filter,
limit: Int? = null,
): String {
val start = currentTimeMillis()
return when {
filter.supportsManualSort() && preferences.isManualSort ->
getRecursiveQuery(filter, preferences)
getRecursiveQuery(filter, preferences, limit)
filter is AstridOrderingFilter && preferences.isAstridSort ->
getNonRecursiveQuery(filter, preferences)
getNonRecursiveQuery(filter, preferences, limit)
filter.supportsSorting() ->
getRecursiveQuery(filter, preferences)
getRecursiveQuery(filter, preferences, limit)
else -> getNonRecursiveQuery(filter, preferences)
else -> getNonRecursiveQuery(filter, preferences, limit)
}.also { Logger.v("TaskListQuery") { "Building query took ${currentTimeMillis() - start}ms" } }
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save