Merge branch 'tasks:main' into main

pull/3687/head
Samuel Born 4 months ago committed by GitHub
commit 4474963dce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -24,7 +24,7 @@ jobs:
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: release
path: .

@ -1 +1 @@
3.4.4
3.4.5

@ -1,3 +1,43 @@
### 14.8 (2025-08-02)
* Synchronize **list** icons for Tasks.org and CalDAV accounts
* Does not apply to Microsoft To Do, Google Tasks, DAVx5, EteSync, or DecSync
CC accounts
* Does not apply to tags or filters
* CalDAV server must support extensible properties, e.g. Nextcloud or sabre/dav
* Target Android 15
* Return to previous view after searching
* Remove shadow from date picker sheet
* Fix updating list names and colors for Tasks.org and CalDAV accounts
* Update translations
* Bulgarian - 109247019824
* Chinese (Simplified) - Sketch6580
* Czech - @Fjuro
* Dutch - @fvbommel
* Estonian - Priit Jõerüüt
* French - @FlorianLeChat
* German - @Colorful Rhino
* Hebrew - Xo
* Italian - @ppasserini
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
### 14.7.4 (2025-07-12)
* @devn1x: Fix escaping quotes in iCalendar [#3645](https://github.com/tasks/tasks/pull/3645)
* Limit widget to 25 items on Android 16+
* Android 16 nerfed widget performance 😢
* Fix bug when reconfiguring widget
* Fix default widget group sort order
* Update translations
* Catalan - pitroig
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* German - @Kachelkaiser
* Serbian - @vale-decem
* Swedish - Nick Wick
* Tamil - @TamilNeram
### 14.7.3 (2025-06-13)
* Fix dynamic color

@ -159,6 +159,7 @@
</queries>
<application
android:pageSizeCompat="enabled"
android:allowBackup="true"
android:backupAgent="org.tasks.backup.TasksBackupAgent"
android:backupInForeground="true"

@ -75,6 +75,10 @@ object AndroidUtilities {
return Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU
}
fun atLeastAndroid15(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM
}
fun atLeastAndroid16(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.BAKLAVA
}

@ -12,8 +12,12 @@ import static java.util.Arrays.asList;
import android.content.Context;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.widget.Toolbar;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -84,12 +88,24 @@ public class BeastModePreferences extends ThemedInjectingAppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
BeastModePrefActivityBinding binding = BeastModePrefActivityBinding.inflate(getLayoutInflater());
Toolbar toolbar = binding.toolbar.toolbar;
RecyclerView recyclerView = binding.recyclerView;
setContentView(binding.getRoot());
ViewCompat.setOnApplyWindowInsetsListener(
binding.getRoot(),
(v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
ViewGroup.MarginLayoutParams toolbarParams =
(ViewGroup.MarginLayoutParams) toolbar.getLayoutParams();
toolbarParams.topMargin = systemBars.top;
recyclerView.setPadding(0, 0, 0, systemBars.bottom);
return insets;
});
toolbar.setNavigationIcon(
getDrawable(R.drawable.ic_outline_arrow_back_24px));
toolbar.setNavigationOnClickListener(v -> finish());

@ -24,7 +24,7 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.LaunchedEffect
@ -241,7 +241,7 @@ class MainActivity : AppCompatActivity() {
}
)
val navigator = rememberListDetailPaneScaffoldNavigator(
calculatePaneScaffoldDirective(
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
windowAdaptiveInfo = currentWindowAdaptiveInfo(),
).copy(
horizontalPartitionSpacerSize = 0.dp,

@ -39,6 +39,7 @@ import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.SearchFilter
import org.tasks.filters.getIcon
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.TasksPreferences
@ -114,12 +115,9 @@ class MainActivityViewModel @Inject constructor(
)
}
updateFilters()
defaultFilterProvider.setLastViewedFilter(filter)
}
fun closeDrawer() {
_drawerOpen.update { false }
_state.update { it.copy(menuQuery = "") }
if (filter !is SearchFilter) {
defaultFilterProvider.setLastViewedFilter(filter)
}
}
fun setDrawerState(opened: Boolean) {
@ -238,4 +236,8 @@ class MainActivityViewModel @Inject constructor(
}
suspend fun getAccount(id: Long) = caldavDao.getAccount(id)
fun openLastViewedFilter() = viewModelScope.launch {
setFilter(defaultFilterProvider.getLastViewedFilter())
}
}

@ -285,7 +285,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
override fun handleOnBackPressed() {
if ((mainViewModel.state.value.filter as? SearchFilter)?.query?.isNotBlank() == true) {
lifecycleScope.launch {
mainViewModel.resetFilter()
mainViewModel.openLastViewedFilter()
}
if (search.isActionViewExpanded) {
search.collapseActionView()

@ -3,6 +3,9 @@ package com.todoroo.astrid.service
import android.content.Context
import android.net.Uri
import androidx.annotation.ColorRes
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.google.common.collect.ImmutableListMultimap
import com.google.common.collect.ListMultimap
import com.google.common.collect.Multimaps
@ -34,6 +37,8 @@ import org.tasks.data.entity.Filter
import org.tasks.data.entity.Tag
import org.tasks.data.entity.TagData
import org.tasks.filters.CaldavFilter
import org.tasks.jobs.UpgradeIconSyncWork
import org.tasks.jobs.networkConstraints
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
@ -145,6 +150,15 @@ class Upgrader @Inject constructor(
}
}
}
run(from, V14_8) {
WorkManager.getInstance(context).enqueueUniqueWork(
uniqueWorkName = "upload_icons",
existingWorkPolicy = ExistingWorkPolicy.KEEP,
request = OneTimeWorkRequestBuilder<UpgradeIconSyncWork>()
.setConstraints(networkConstraints)
.build()
)
}
preferences.setBoolean(R.string.p_just_updated, true)
} else {
setInstallDetails(to)
@ -407,6 +421,7 @@ class Upgrader @Inject constructor(
const val V12_8 = 120800
const val V14_5_4 = 140516
const val V14_6_1 = 140602
const val V14_8 = 140800
@JvmStatic
fun getAndroidColor(context: Context, index: Int): Int {

@ -17,6 +17,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.coroutineScope
import androidx.work.Configuration
import com.mikepenz.iconics.Iconics
import com.todoroo.andlib.utility.AndroidUtilities.atLeastAndroid15
import com.todoroo.andlib.utility.AndroidUtilities.atLeastR
import com.todoroo.astrid.service.Upgrader
import dagger.Lazy
@ -102,11 +103,17 @@ class TasksApplication : Application(), Configuration.Provider {
Timber.i("Astrid Startup. %s => %s", lastVersion, currentVersion)
if (atLeastR()) {
scope.launch {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val exitReasons = activityManager.getHistoricalProcessExitReasons(null, 0, 1)
logExitReasons(exitReasons)
}
}
if (atLeastAndroid15()) {
val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
activityManager.addApplicationStartInfoCompletionListener(mainExecutor) { startInfo ->
Timber.d("Application was force stopped: ${startInfo.wasForceStopped()}")
}
}
// invoke upgrade service
if (lastVersion != currentVersion) {

@ -8,10 +8,16 @@ import android.os.Bundle
import android.text.TextUtils
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.activity.enableEdgeToEdge
import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -70,8 +76,18 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityCaldavAccountSettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
binding.toolbar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = systemBars.top
}
binding.rootLayout.updatePadding(bottom = systemBars.bottom)
insets
}
caldavAccount = if (savedInstanceState == null) intent.getParcelableExtra(EXTRA_CALDAV_DATA) else savedInstanceState.getParcelable(EXTRA_CALDAV_DATA)
serverType = mutableStateOf(
savedInstanceState?.getInt(EXTRA_SERVER_TYPE, SERVER_UNKNOWN)

@ -78,11 +78,10 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
showProgressIndicator()
createCalendar(caldavAccount, name, baseViewModel.color)
}
nameChanged() || colorChanged() -> {
nameChanged() || colorChanged() || iconChanged() -> {
showProgressIndicator()
updateNameAndColor(caldavAccount, caldavCalendar!!, name, baseViewModel.color)
}
iconChanged() -> updateCalendar()
else -> finish()
}
}
@ -150,7 +149,7 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
)
caldavDao.update(result)
setResult(
Activity.RESULT_OK,
RESULT_OK,
Intent(TaskListFragment.ACTION_RELOAD)
.putExtra(
MainActivity.OPEN_FILTER,

@ -37,7 +37,7 @@ class CaldavCalendarViewModel @Inject constructor(
): CaldavCalendar? =
doRequest {
val url = withContext(Dispatchers.IO) {
provider.forAccount(caldavAccount).makeCollection(name, color)
provider.forAccount(caldavAccount).makeCollection(name, color, icon)
}
val calendar = CaldavCalendar(
uuid = UUIDHelper.newUUID(),
@ -67,7 +67,7 @@ class CaldavCalendarViewModel @Inject constructor(
) =
doRequest {
withContext(Dispatchers.IO) {
provider.forAccount(account, calendar.url!!).updateCollection(name, color)
provider.forAccount(account, calendar.url!!).updateCollection(name, color, icon)
}
val result = calendar.copy(
name = name,

@ -11,24 +11,32 @@ import at.bitfire.dav4jvm.XmlUtils.NS_CALDAV
import at.bitfire.dav4jvm.XmlUtils.NS_WEBDAV
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.*
import at.bitfire.dav4jvm.property.CalendarColor
import at.bitfire.dav4jvm.property.CalendarHomeSet
import at.bitfire.dav4jvm.property.CurrentUserPrincipal
import at.bitfire.dav4jvm.property.CurrentUserPrivilegeSet
import at.bitfire.dav4jvm.property.DisplayName
import at.bitfire.dav4jvm.property.GetCTag
import at.bitfire.dav4jvm.property.ResourceType
import at.bitfire.dav4jvm.property.ResourceType.Companion.CALENDAR
import org.tasks.data.UUIDHelper
import at.bitfire.dav4jvm.property.SupportedCalendarComponentSet
import at.bitfire.dav4jvm.property.SyncToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.property.CalendarIcon
import org.tasks.caldav.property.Invite
import org.tasks.caldav.property.OCInvite
import org.tasks.caldav.property.OCOwnerPrincipal
import org.tasks.caldav.property.PropertyUtils.NS_OWNCLOUD
import org.tasks.caldav.property.ShareAccess
import org.tasks.data.UUIDHelper
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_NEXTCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OWNCLOUD
@ -101,9 +109,12 @@ open class CaldavClient(
.findHomeset()
}
suspend fun calendars(interceptor: (Interceptor.Chain) -> okhttp3.Response): List<Response> =
suspend fun calendars(interceptor: (okhttp3.Response) -> okhttp3.Response = { it }): List<Response> =
DavResource(
httpClient.newBuilder().addNetworkInterceptor(interceptor).build(),
httpClient
.newBuilder()
.addNetworkInterceptor { interceptor(it.proceed(it.request())) }
.build(),
httpUrl!!
)
.propfind(1, *calendarProperties)
@ -120,33 +131,44 @@ open class CaldavClient(
}
@Throws(IOException::class, XmlPullParserException::class, HttpException::class)
suspend fun makeCollection(displayName: String, color: Int): String = withContext(Dispatchers.IO) {
suspend fun makeCollection(displayName: String, color: Int, icon: String?): String = withContext(Dispatchers.IO) {
val davResource = DavResource(httpClient, httpUrl!!.resolve(UUIDHelper.newUUID() + "/")!!)
val mkcolString = getMkcolString(displayName, color)
davResource.mkCol(mkcolString) {}
if (icon?.isNotBlank() == true) {
davResource.proppatch(CalendarIcon.NAME, icon)
}
davResource.location.toString()
}
@Throws(IOException::class, XmlPullParserException::class, HttpException::class)
suspend fun updateCollection(displayName: String, color: Int): String =
suspend fun updateCollection(displayName: String, color: Int, icon: String?): String =
withContext(Dispatchers.IO) {
with(DavResource(httpClient, httpUrl!!)) {
proppatch(
setProperties = mutableMapOf(DisplayName.NAME to displayName).apply {
if (color != 0) {
put(
CalendarColor.NAME,
String.format("#%06X%02X", color and 0xFFFFFF, color ushr 24)
)
}
},
removeProperties = if (color == 0) listOf(CalendarColor.NAME) else emptyList(),
callback = { _, _ -> },
)
proppatch(DisplayName.NAME, displayName)
if (color != 0) {
proppatch(
CalendarColor.NAME,
String.format("#%06X%02X", color and 0xFFFFFF, color ushr 24)
)
}
if (icon?.isNotBlank() == true) {
proppatch(CalendarIcon.NAME, icon)
}
location.toString()
}
}
@Throws(IOException::class, XmlPullParserException::class, HttpException::class)
suspend fun updateIcon(url: HttpUrl, icon: String?, onFailure: () -> Unit) =
withContext(Dispatchers.IO) {
with(DavResource(httpClient, url)) {
if (icon?.isNotBlank() == true) {
proppatch(CalendarIcon.NAME, icon, onFailure)
}
}
}
@Throws(IOException::class, XmlPullParserException::class)
private fun getMkcolString(displayName: String, color: Int): String {
val xmlPullParserFactory = XmlPullParserFactory.newInstance()
@ -284,18 +306,19 @@ open class CaldavClient(
private val MEDIATYPE_SHARING = "application/davsharing+xml".toMediaType()
private val calendarProperties = arrayOf(
ResourceType.NAME,
DisplayName.NAME,
SupportedCalendarComponentSet.NAME,
GetCTag.NAME,
CalendarColor.NAME,
SyncToken.NAME,
ShareAccess.NAME,
Invite.NAME,
OCOwnerPrincipal.NAME,
OCInvite.NAME,
CurrentUserPrivilegeSet.NAME,
CurrentUserPrincipal.NAME,
ResourceType.NAME,
DisplayName.NAME,
SupportedCalendarComponentSet.NAME,
GetCTag.NAME,
CalendarColor.NAME,
SyncToken.NAME,
ShareAccess.NAME,
Invite.NAME,
OCOwnerPrincipal.NAME,
OCInvite.NAME,
CurrentUserPrivilegeSet.NAME,
CurrentUserPrincipal.NAME,
CalendarIcon.NAME,
)
private suspend fun DavResource.propfind(
@ -311,5 +334,22 @@ open class CaldavClient(
cont.resumeWith(Result.success(responses))
}
}
fun DavResource.proppatch(
property: Property.Name,
value: String,
onFailure: () -> Unit = {},
) {
proppatch(
setProperties = mapOf(property to value),
removeProperties = emptyList(),
callback = { response, _ ->
if (!response.isSuccess()) {
Timber.e("${response.status} when updating $property: ${response.error}")
onFailure()
}
},
)
}
}
}
}

@ -38,12 +38,12 @@ import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.caldav.iCalendar.Companion.fromVtodo
import org.tasks.caldav.property.CalendarIcon
import org.tasks.caldav.property.Invite
import org.tasks.caldav.property.OCAccess
import org.tasks.caldav.property.OCInvite
import org.tasks.caldav.property.OCOwnerPrincipal
import org.tasks.caldav.property.OCUser
import org.tasks.caldav.property.PropertyUtils.register
import org.tasks.caldav.property.ShareAccess
import org.tasks.caldav.property.ShareAccess.Companion.NOT_SHARED
import org.tasks.caldav.property.ShareAccess.Companion.NO_ACCESS
@ -136,8 +136,7 @@ class CaldavSynchronizer @Inject constructor(
private suspend fun synchronize(account: CaldavAccount) {
val caldavClient = provider.forAccount(account)
var serverType = account.serverType
val resources = caldavClient.calendars { chain ->
val response = chain.proceed(chain.request())
val resources = caldavClient.calendars { response ->
if (serverType == SERVER_UNKNOWN) {
serverType = getServerType(account, response.headers)
}
@ -155,8 +154,10 @@ class CaldavSynchronizer @Inject constructor(
val url = resource.href.toString()
var calendar = caldavDao.getCalendarByUrl(account.uuid!!, url)
val remoteName = resource[DisplayName::class.java]!!.displayName
val calendarColor = resource[CalendarColor::class.java]
val color = resource[CalendarColor::class.java]?.color ?: 0
val access = resource.accessLevel
val icon = resource[CalendarIcon::class.java]?.icon?.takeIf { it.isNotBlank() }
if (access == ACCESS_UNKNOWN) {
firebase.logEvent(
R.string.event_sync_unknown_access,
@ -164,7 +165,6 @@ class CaldavSynchronizer @Inject constructor(
(resource[ShareAccess::class.java]?.access?.toString() ?: "???")
)
}
val color = calendarColor?.color ?: 0
if (calendar == null) {
calendar = CaldavCalendar(
name = remoteName,
@ -173,15 +173,20 @@ class CaldavSynchronizer @Inject constructor(
uuid = UUIDHelper.newUUID(),
color = color,
access = access,
icon = icon,
)
caldavDao.insert(calendar)
} else if (calendar.name != remoteName
|| calendar.color != color
|| calendar.access != access
|| calendar.color != color
|| calendar.access != access
|| (icon != null && calendar.icon != icon)
) {
calendar.color = color
calendar.name = remoteName
calendar.access = access
calendar = calendar.copy(
color = color,
name = remoteName,
access = access,
icon = icon ?: calendar.icon,
)
caldavDao.update(calendar)
localBroadcastManager.broadcastRefreshList()
}
@ -436,10 +441,13 @@ class CaldavSynchronizer @Inject constructor(
fun registerFactories() {
PropertyRegistry.register(
ShareAccess.Factory(),
Invite.Factory(),
OCOwnerPrincipal.Factory(),
OCInvite.Factory(),
listOf(
ShareAccess.Factory(),
Invite.Factory(),
OCOwnerPrincipal.Factory(),
OCInvite.Factory(),
CalendarIcon.Factory,
)
)
}
@ -488,4 +496,4 @@ class CaldavSynchronizer @Inject constructor(
else -> INVITE_UNKNOWN
}
}
}
}

@ -41,7 +41,6 @@ import org.tasks.data.entity.Alarm.Companion.TYPE_RANDOM
import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.Place
import org.tasks.data.entity.TagData
@ -208,7 +207,7 @@ class iCalendar @Inject constructor(
val task = existing?.task
?.let { taskDao.fetch(it) }
?: taskCreator.createWithValues("").apply {
readOnly = calendar.access == ACCESS_READ_ONLY
readOnly = calendar.readOnly()
taskDao.createNew(this)
}
val caldavTask =

@ -0,0 +1,32 @@
package org.tasks.caldav.property
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.PropertyFactory
import at.bitfire.dav4jvm.XmlUtils
import org.xmlpull.v1.XmlPullParser
import timber.log.Timber
data class CalendarIcon(
val icon: String,
): Property {
companion object Companion {
@JvmField
val NAME = Property.Name(PropertyUtils.NS_TASKS, "x-calendar-icon")
}
object Factory: PropertyFactory {
override fun getName() = NAME
override fun create(parser: XmlPullParser): CalendarIcon? {
XmlUtils.readText(parser)?.takeIf { it.isNotBlank() }?.let {
try {
return CalendarIcon(it)
} catch (e: IllegalArgumentException) {
Timber.e(e, "Couldn't parse icon: $it")
}
}
return null
}
}
}

@ -1,10 +1,6 @@
package org.tasks.caldav.property
import at.bitfire.dav4jvm.PropertyFactory
import at.bitfire.dav4jvm.PropertyRegistry
object PropertyUtils {
const val NS_TASKS = "http://org.tasks/ns/"
const val NS_OWNCLOUD = "http://owncloud.org/ns"
fun PropertyRegistry.register(vararg factories: PropertyFactory) = register(factories.toList())
}
}

@ -84,7 +84,6 @@ fun DatePickerBottomSheet(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(),
shadowElevation = 8.dp,
color = MaterialTheme.colorScheme.surface,
) {
Row(

@ -5,7 +5,9 @@ import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -60,6 +62,9 @@ fun ListSettingsScaffold(
)
}
TopAppBar(
windowInsets = TopAppBarDefaults.windowInsets.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = color,
navigationIconContentColor = contentColor,

@ -3,6 +3,7 @@ package org.tasks.injection
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.todoroo.andlib.utility.AndroidUtilities.atLeastAndroid16
import kotlinx.coroutines.runBlocking
import org.tasks.analytics.Firebase
import timber.log.Timber
@ -14,7 +15,11 @@ abstract class BaseWorker(
) : Worker(context, workerParams) {
override fun doWork(): Result {
Timber.d("${javaClass.simpleName} $id $inputData")
if (atLeastAndroid16()) {
Timber.d("${javaClass.simpleName} $id $inputData attempt=$runAttemptCount ${if (runAttemptCount > 0) "stopReason=$stopReason" else ""}")
} else {
Timber.d("${javaClass.simpleName} $id $inputData attempt=$runAttemptCount")
}
return try {
runBlocking {
run()

@ -34,7 +34,7 @@ class MigrateLocalWork @AssistedInject constructor(
caldavDao.getCalendarsByAccount(fromAccount.uuid!!).forEach {
caldavDao.update(
it.copy(
url = caldavClient.makeCollection(it.name!!, it.color),
url = caldavClient.makeCollection(it.name!!, it.color, it.icon),
account = caldavAccount.uuid,
)
)

@ -0,0 +1,55 @@
package org.tasks.jobs
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.tasks.analytics.Firebase
import org.tasks.caldav.CaldavClientProvider
import org.tasks.caldav.property.CalendarIcon
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.BaseWorker
import timber.log.Timber
@HiltWorker
class UpgradeIconSyncWork @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
firebase: Firebase,
private val clientProvider: CaldavClientProvider,
private val caldavDao: CaldavDao,
) : BaseWorker(context, workerParams, firebase) {
override suspend fun run(): Result {
var response = Result.success()
caldavDao
.getAccounts(CaldavAccount.TYPE_TASKS, CaldavAccount.TYPE_CALDAV)
.forEach { account ->
Timber.d("Uploading icons for $account")
val caldavClient = clientProvider.forAccount(account)
caldavClient.calendars().forEach { remote ->
val url = remote.href
val calendar = caldavDao
.getCalendarByUrl(account.uuid!!, url.toString())
?.takeIf { !it.readOnly() && it.icon?.isNotBlank() == true }
?: run {
Timber.d("No icon set for $url")
return@forEach
}
val icon = remote[CalendarIcon::class.java]?.icon
if (icon?.isNotBlank() == true) {
Timber.d("Remote icon already set for $url")
return@forEach
}
Timber.d("Uploading icon to ${calendar.icon} for $url")
caldavClient.updateIcon(
url = url,
icon = calendar.icon,
onFailure = { response = Result.retry() }
)
}
}
return response
}
}

@ -136,7 +136,7 @@ class WorkManagerImpl(
.setConstraints(networkConstraints)
workManager.enqueueUniquePeriodicWork(
TAG_BACKGROUND_SYNC,
ExistingPeriodicWorkPolicy.KEEP,
ExistingPeriodicWorkPolicy.UPDATE,
builder.build()
)
} else {
@ -183,7 +183,7 @@ class WorkManagerImpl(
throttle.run {
workManager.enqueueUniquePeriodicWork(
TAG_REMOTE_CONFIG,
ExistingPeriodicWorkPolicy.KEEP,
ExistingPeriodicWorkPolicy.UPDATE,
PeriodicWorkRequest.Builder(
RemoteConfigWork::class.java, REMOTE_CONFIG_INTERVAL_HOURS, TimeUnit.HOURS)
.setConstraints(networkConstraints)
@ -207,9 +207,6 @@ class WorkManagerImpl(
enqueue(builder)
}
private val networkConstraints: Constraints
get() = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
override fun updatePurchases() =
enqueueUnique(TAG_UPDATE_PURCHASES, UpdatePurchaseWork::class.java)
@ -260,3 +257,6 @@ class WorkManagerImpl(
private fun <B : WorkRequest.Builder<B, *>, W : WorkRequest> WorkRequest.Builder<B, W>.setInputData(
vararg pairs: Pair<String, Any?>
): B = setInputData(workDataOf(*pairs))
val networkConstraints: Constraints
get() = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

@ -8,6 +8,7 @@ import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@ -15,6 +16,9 @@ import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.widget.ContentLoadingProgressBar
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
@ -66,6 +70,7 @@ class LocationPickerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListe
private lateinit var loadingIndicator: ContentLoadingProgressBar
private lateinit var chooseRecentLocation: View
private lateinit var recyclerView: RecyclerView
private lateinit var selectThisLocation: View
@Inject lateinit var theme: Theme
@Inject lateinit var locationDao: LocationDao
@ -88,6 +93,7 @@ class LocationPickerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListe
private lateinit var search: MenuItem
private var searchJob: Job? = null
private val viewModel: PlaceSearchViewModel by viewModels()
private var systemBarsBottom = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -95,6 +101,13 @@ class LocationPickerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListe
window.statusBarColor = ContextCompat.getColor(this, android.R.color.transparent)
val binding = ActivityLocationPickerBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
systemBarsBottom = systemBars.bottom
insets
}
toolbar = binding.toolbar
appBarLayout = binding.appBarLayout
toolbarLayout = binding.collapsingToolbarLayout
@ -105,6 +118,7 @@ class LocationPickerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListe
loadingIndicator = binding.loadingIndicator
chooseRecentLocation = binding.chooseRecentLocation
recyclerView = binding.recentLocations
selectThisLocation = binding.selectThisLocation
val configuration = resources.configuration
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.smallestScreenWidthDp < 480) {
@ -318,9 +332,15 @@ class LocationPickerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListe
params.height = height
chooseRecentLocation.visibility = View.GONE
collapseToolbar()
selectThisLocation.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = systemBarsBottom
}
} else {
params.height = height * 75 / 100
chooseRecentLocation.visibility = View.VISIBLE
selectThisLocation.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = 0
}
}
}

@ -3,7 +3,12 @@ package org.tasks.preferences
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@ -22,8 +27,21 @@ abstract class BasePreferences : ThemedInjectingAppCompatActivity(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
val binding = ActivityPreferencesBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
val systemBars = insets.getSystemWindowInsets()
toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = systemBars.top
}
binding.settings.updatePadding(bottom = systemBars.bottom)
insets
}
toolbar = binding.toolbar.toolbar
if (savedInstanceState == null) {
val rootPreference = getRootPreference()

@ -56,7 +56,7 @@ class DefaultFilterProvider @Inject constructor(
fun setLastViewedFilter(filter: Filter) = setFilterPreference(filter, R.string.p_last_viewed_list)
private suspend fun getLastViewedFilter() = getFilterFromPreference(R.string.p_last_viewed_list)
suspend fun getLastViewedFilter() = getFilterFromPreference(R.string.p_last_viewed_list)
suspend fun getDefaultOpenFilter() = getFilterFromPreference(R.string.p_default_open_filter)
@ -84,7 +84,7 @@ class DefaultFilterProvider @Inject constructor(
private suspend fun getAnyList(): CaldavFilter {
val filter = caldavDao
.getCalendars()
.filterNot { it.access == ACCESS_READ_ONLY }
.filterNot { it.readOnly() }
.getOrNull(0)
?.let { list ->
list.account

@ -4,15 +4,19 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
@ -69,6 +73,8 @@ class TagPickerActivity : ThemedInjectingAppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val intent = intent
taskIds = intent.getSerializableExtra(EXTRA_TASKS) as ArrayList<Long>?
if (savedInstanceState == null) {
@ -140,8 +146,11 @@ internal fun TagPicker(
getTagIcon: (TagData) -> String,
getTagColor: (TagData) -> Color
) {
Box ( modifier = Modifier.fillMaxSize() )
{
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
) {
Column (modifier = Modifier.padding(horizontal = 12.dp)) {
Box( modifier = Modifier.fillMaxWidth() ) {
SearchBar(viewModel, onBackClicked)

@ -2,8 +2,15 @@ package org.tasks.widget
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
@ -48,6 +55,8 @@ class ShortcutConfigActivity : ThemedInjectingAppCompatActivity(), ColorPaletteP
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
window.statusBarColor = ContextCompat.getColor(this, android.R.color.transparent)
val binding = ActivityWidgetShortcutLayoutBinding.inflate(layoutInflater)
binding.let {
toolbar = it.toolbar.toolbar
@ -62,6 +71,15 @@ class ShortcutConfigActivity : ThemedInjectingAppCompatActivity(), ColorPaletteP
}
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = systemBars.top
}
binding.body.root.updatePadding(bottom = systemBars.bottom)
insets
}
toolbar.setTitle(R.string.FSA_label)
toolbar.navigationIcon = getDrawable(R.drawable.ic_outline_save_24px)
toolbar.setNavigationOnClickListener { save() }

@ -119,6 +119,8 @@ internal class TasksWidgetViewFactory(
private fun buildFooter(): RemoteViews {
return RemoteViews(BuildConfig.APPLICATION_ID, R.layout.widget_footer).apply {
setTextSize(R.id.widget_view_more, settings.textSize)
setTextColor(R.id.widget_view_more, onSurface)
setOnClickFillInIntent(
R.id.widget_view_more,
Intent(WidgetClickActivity.OPEN_TASK_LIST)

@ -558,7 +558,7 @@
<string name="device_settings">Настройки на устройството</string>
<string name="support_development_subscribe">Отключване на допълнителни възможности и подкрепа на софтуера с отворен код</string>
<string name="sort_start_group">Започната %s</string>
<string name="sort_due_group">Завършена %s</string>
<string name="sort_due_group">За %s</string>
<string name="on_launch">При стартиране</string>
<string name="sort_created_group">Създадена %s</string>
<string name="sort_modified_group">Променена %s</string>
@ -721,4 +721,5 @@
<string name="delete_tasks_warning">Задачата %s ще бъде премахната. Действието е необратимо!</string>
<string name="continue_without_sync">Без синхронизиране</string>
<string name="help_me_choose">Помощ в избора</string>
<string name="widget_view_more_tasks">Повече задачи</string>
</resources>

@ -743,4 +743,5 @@
<string name="help_me_choose">Pomozte mi s výběrem</string>
<string name="delete_tasks_warning">Úkol %s bude odstraněn. Tato akce je nevratná!</string>
<string name="continue_without_sync">Pokračovat bez synchronizace</string>
<string name="widget_view_more_tasks">Zobrazit další úkoly</string>
</resources>

@ -721,4 +721,5 @@
<string name="continue_without_sync">Fortsæt uden synkronisering</string>
<string name="help_me_choose">Hjælp mig med at vælge</string>
<string name="delete_tasks_warning">%s bliver slettet. Dette kan ikke fortrydes!</string>
<string name="widget_view_more_tasks">Vis flere opgaver</string>
</resources>

@ -721,4 +721,5 @@
<string name="help_me_choose">Hilf mir bei der Auswahl</string>
<string name="continue_without_sync">Weiter ohne Synchronisierung</string>
<string name="delete_tasks_warning">%s wird/werden gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
<string name="widget_view_more_tasks">Mehr Aufgaben anzeigen</string>
</resources>

@ -721,4 +721,5 @@
<string name="continue_without_sync">Jätka ilma sünkroniseerimata</string>
<string name="help_me_choose">Aita mul valida</string>
<string name="delete_tasks_warning">%s saab olema kustutatud. Seda tegevust ei saa tagasi pöörata!</string>
<string name="widget_view_more_tasks">Vaata veel ülesandeid</string>
</resources>

@ -743,4 +743,5 @@
<string name="help_me_choose">Aidez-moi à choisir</string>
<string name="delete_tasks_warning">%s sera supprimé. Cette opération est irréversible!</string>
<string name="multiline_title_off">Appuyer sur Terminé pour enregistrer la tâche</string>
<string name="widget_view_more_tasks">Voir plus de tâches</string>
</resources>

@ -721,4 +721,5 @@
<string name="continue_without_sync">Folytatás szinkronizálás nélkül</string>
<string name="help_me_choose">Segítséget kérek a választásban</string>
<string name="multiline_title_off">Kattints a Kész gombra a feladat elmentéséhez</string>
<string name="widget_view_more_tasks">További feladatok megtekintése</string>
</resources>

@ -9,7 +9,7 @@
<string name="import_summary_title">Riepilogo del ripristino</string>
<string name="import_summary_message">Il file %1$s contiene %2$s. \n \n %3$s importate, \n %4$s già esistenti \n %5$s con errori</string>
<string name="import_progress_read">Lettura attività %d…</string>
<string name="read_permission_label">Permessi di Tasks</string>
<string name="read_permission_label">accedere a Tasks</string>
<string name="discard_confirmation">Vuoi davvero annullare tutte le modifiche\?</string>
<string name="keep_editing">No, continua la modifica</string>
<string name="DLG_delete_this_task_question">Eliminare questa attività\?</string>
@ -451,7 +451,7 @@
<string name="location_radius_meters">%s m</string>
<string name="subtasks">Attività secondaria</string>
<string name="TEA_timer_controls">Timer</string>
<string name="chip_appearance">Aspetto icona</string>
<string name="chip_appearance">Aspetto etichetta</string>
<string name="chips">Smart Chips</string>
<string name="custom_filter_not">NON</string>
<string name="custom_filter_or">O</string>
@ -743,4 +743,5 @@
<string name="continue_without_sync">Continua senza sincronizzazione</string>
<string name="help_me_choose">Aiutami a scegliere</string>
<string name="delete_tasks_warning">%s verrà cancellato. L\'operazione non può essere annullata!</string>
<string name="widget_view_more_tasks">Visualizza ulteriori attività</string>
</resources>

@ -745,4 +745,5 @@
<string name="continue_without_sync">להמשיך ללא סנכרון</string>
<string name="help_me_choose">עזור לי לבחור</string>
<string name="delete_tasks_warning">%s יימחק. לא ניתן לבטל זאת!</string>
<string name="widget_view_more_tasks">הצג עוד משימות</string>
</resources>

@ -721,4 +721,5 @@
<string name="continue_without_sync">Doorgaan zonder synchronisatie</string>
<string name="help_me_choose">Help me kiezen</string>
<string name="delete_tasks_warning">%s wordt verwijderd. Dit kan niet ongedaan worden gemaakt!</string>
<string name="widget_view_more_tasks">Meer taken zien</string>
</resources>

@ -7,7 +7,7 @@
<string name="backup_BAc_export">Utwórz kopię zapasową teraz</string>
<string name="export_toast">Zapisano %1$s do %2$s.</string>
<string name="import_summary_title">Podsumowanie odzyskiwania</string>
<string name="import_summary_message">Plik %1$s zawiera %2$s.\n\n %3$s zaimportowanych,\n %4$s już istnieje\n %5$s zawiera błędy\n</string>
<string name="import_summary_message">Plik %1$s zawiera %2$s.\n\n %3$s zaimportowanych,\n %4$s już istnieje\n %5$s zawiera błędy</string>
<string name="import_progress_read">Czytanie zadania %d…</string>
<string name="read_permission_label">Uprawnienia Tasks</string>
<string name="discard_confirmation">Czy jesteś pewien, że chcesz porzucić zmiany?</string>
@ -703,4 +703,20 @@
<string name="sort_ascending">Rosnąco</string>
<string name="sort_descending">Malejąco</string>
<string name="sort_completed">Wg czasu zakończenia</string>
<string name="app_settings">Ustawienia aplikacji</string>
<string name="swipe_to_snooze_title">Przesuń aby uśpić</string>
<string name="swipe_to_snooze_time_immediately">natychmiast</string>
<string name="swipe_to_snooze_time_15_minutes">po 15 minutach</string>
<string name="swipe_to_snooze_time_30_minutes">po 30 minutach</string>
<string name="swipe_to_snooze_time_1_hour">po 1 godzinie</string>
<string name="swipe_to_snooze_time_24_hours">po 24 godzinach</string>
<string name="swipe_to_snooze_description">Czas drzemki</string>
<string name="delete_comment">komentarz</string>
<string name="comment">Komentarz</string>
<string name="yesterday">Wczoraj</string>
<string name="send_application_logs">Wyślij logi aplikacji</string>
<string name="change_priority">Zmień priorytet</string>
<string name="theme_dynamic">Dynamiczny</string>
<string name="continue_without_sync">Kontynuuj bez synchronizacji</string>
<string name="help_me_choose">Pomóż mi wybrać</string>
</resources>

@ -38,8 +38,8 @@
<string name="TEA_timer_elap">Decorrido %s</string>
<string name="due_date">Data de vencimento</string>
<string name="due_time">Na hora de vencimento</string>
<string name="day_before_due">Dias antes do vencimento</string>
<string name="week_before_due">Semanas antes do vencimento</string>
<string name="day_before_due">Um dia antes do vencimento</string>
<string name="week_before_due">Semana antes do vencimento</string>
<string name="TEA_control_repeat">Repetir</string>
<string name="TEA_control_gcal">Calendário</string>
<string name="TEA_control_importance">Prioridade</string>
@ -392,7 +392,7 @@
<string name="auto_dismiss_datetime_list_summary">Fechar automaticamente ao selecionar na lista de tarefas</string>
<string name="auto_dismiss_datetime_list">Lista de tarefas</string>
<string name="auto_dismiss_datetime">Fechar automaticamente o seletor de data e hora</string>
<string name="shortcut_pick_time">Escolha um horário</string>
<string name="shortcut_pick_time">Escolher hora</string>
<string name="no_time">Sem horário</string>
<string name="no_date">Sem data</string>
<string name="chip_appearance_icon_only">Somente o ícone</string>

@ -721,4 +721,5 @@
<string name="delete_tasks_warning">%s silinecek. Bu geri alınamaz!</string>
<string name="continue_without_sync">Eşzamanlamadan sürdür</string>
<string name="help_me_choose">Seçmeme yardım et</string>
<string name="widget_view_more_tasks">Daha çok görev gör</string>
</resources>

@ -751,4 +751,5 @@
<string name="continue_without_sync">Продовжити без синхронізації</string>
<string name="help_me_choose">Допоможіть обрати</string>
<string name="delete_tasks_warning">%s буде видалено. Дію неможливо скасувати!</string>
<string name="widget_view_more_tasks">Переглянути інші завдання</string>
</resources>

@ -710,4 +710,5 @@
<string name="continue_without_sync">继续但不同步</string>
<string name="help_me_choose">帮我选择</string>
<string name="delete_tasks_warning">%s 将会删除。此操作无法撤销!</string>
<string name="widget_view_more_tasks">查看更多任务</string>
</resources>

@ -1,17 +1,18 @@
package org.tasks.caldav.property
import at.bitfire.dav4jvm.PropertyRegistry
import org.junit.Assert.*
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.tasks.caldav.property.PropertyUtils.register
import org.tasks.caldav.property.ShareAccess.Companion.SHARED_OWNER
import org.tasks.caldav.property.TestPropertyUtils.toProperty
class InviteTest {
@Before
fun setUp() {
PropertyRegistry.register(ShareAccess.Factory(), Invite.Factory())
PropertyRegistry.register(listOf(ShareAccess.Factory(), Invite.Factory()))
}
@Test

@ -24,7 +24,7 @@ buildscript {
}
tasks.getByName<Wrapper>("wrapper") {
gradleVersion = "8.14.2"
gradleVersion = "8.14.3"
distributionType = Wrapper.DistributionType.ALL
}

@ -49,4 +49,6 @@ data class CaldavCalendar(
@JvmField val NAME = TABLE.column("cdl_name")
@JvmField val ORDER = TABLE.column("cdl_order")
}
fun readOnly(): Boolean = access == ACCESS_READ_ONLY
}

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,5 @@
* Fix escaping quotes in iCalendar
* Limit widget to 25 items on Android 16+
* Fix bug when reconfiguring widget
* Fix default widget group sort order
* Update translations

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

@ -0,0 +1,10 @@
* Synchronize **list** icons for Tasks.org and CalDAV accounts
* Does not apply to Microsoft To Do, Google Tasks, DAVx5, EteSync, or DecSync
CC accounts
* Does not apply to tags or filters
* CalDAV server must support extensible properties, e.g. Nextcloud or sabre/dav
* Target Android 15
* Return to previous view after searching
* Remove shadow from date picker sheet
* Fix updating list names and colors for Tasks.org and CalDAV accounts
* Update translations

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

@ -1,43 +1,43 @@
[versions]
versionCode = "140710" # increment by 2
versionName = "14.7.3"
agp = "8.11.0"
versionCode = "140802" # increment by 2
versionName = "14.8"
agp = "8.12.0"
android-compileSdk = "36"
android-minSdk = "26"
android-targetSdk = "34"
android-targetSdk = "35"
accompanist = "0.37.3"
activity-compose = "1.10.1"
appauth = "0.11.1"
appcompat = "1.7.1"
cert4android = "7814052"
coil = "2.7.0"
compose = "2025.06.01"
compose = "2025.07.00"
constraintlayout = "2.2.1"
dagger-hilt = "2.56.2"
dagger-hilt = "2.57"
dashclock-api = "2.0.0"
dav4jvm = "2.2.1"
desugar_jdk_libs = "2.1.5"
etebase = "2.3.2"
firebase = "33.16.0"
firebase-crashlytics-gradle = "3.0.4"
firebase-crashlytics-gradle = "3.0.6"
google-oauth2 = "1.37.1"
google-api-drive = "v3-rev20250701-2.0.0"
google-api-drive = "v3-rev20250723-2.0.0"
google-api-tasks = "v1-rev20250518-2.0.0"
google-services = "4.4.3"
grpc = "1.73.0"
hilt = "1.2.0"
horologist = "0.6.23"
horologist = "0.7.15"
ical4android = "2fe63dd"
jchronic = "0.2.6"
jems = "1.33"
junit-junit = "4.13.2"
junit = "1.2.1"
junit = "1.3.0"
kotlin = "2.1.21"
kotlinx-coroutines = "1.10.2"
ktor = "3.1.3"
leakcanary = "2.14"
lib-recur = "0.11.4"
lifecycle = "2.9.1"
lifecycle = "2.9.2"
locale = "1.0.4"
make-it-easy = "4.0.1"
markwon = "4.6.2"
@ -46,11 +46,11 @@ mockito = "5.18.0"
okhttp = "4.12.0"
opentasks = "562fec5"
osmdroid = "6.1.20"
oss-licenses-plugin = "0.10.6"
oss-licenses-plugin = "0.10.7"
persistent-cookiejar = "1.0.1"
play-services-maps = "19.2.0"
play-services-location = "21.3.0"
play-services-oss-licenses = "17.1.0"
play-services-oss-licenses = "17.2.1"
preference = "1.2.1"
protobuf = "4.31.1"
recyclerview = "1.4.0"
@ -59,9 +59,9 @@ room = "2.7.2"
shortcut-badger = "1.1.22"
timber = "5.0.1"
swiperefreshlayout = "1.1.0"
work = "2.10.2"
androidx-test = "1.6.1"
androidx-test-runner = "1.6.2"
work = "2.10.3"
androidx-test = "1.7.0"
androidx-test-runner = "1.7.0"
xpp3 = "1.1.6"
wearCompose = "1.4.1"
@ -86,7 +86,7 @@ androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-ru
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
androidx-navigation = { module = "androidx.navigation:navigation-compose", version = "2.9.1" }
androidx-navigation = { module = "androidx.navigation:navigation-compose", version = "2.9.3" }
androidx-paging-compose = { module = "androidx.paging:paging-compose", version = "3.3.6" }
androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
@ -159,7 +159,7 @@ markwon-strikethrough = { module = "io.noties.markwon:ext-strikethrough", versio
markwon-tables = { module = "io.noties.markwon:ext-tables", version.ref = "markwon" }
markwon-tasklist = { module = "io.noties.markwon:ext-tasklist", version.ref = "markwon" }
material = { module = "com.google.android.material:material", version.ref = "material" }
microsoft-authentication = { module = "com.microsoft.identity.client:msal", version = "6.0.1" }
microsoft-authentication = { module = "com.microsoft.identity.client:msal", version = "7.0.0" }
mockito-android = { module = "org.mockito:mockito-android", version.ref = "mockito" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
@ -187,7 +187,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
wear-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "wearCompose" }
wear-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "wearCompose" }
wear-compose-navigation = { group = "androidx.wear.compose", name = "compose-navigation", version.ref = "wearCompose" }
wear-input = { group = "androidx.wear", name = "wear-input", version = "1.2.0-alpha02" }
wear-input = { group = "androidx.wear", name = "wear-input", version = "1.2.0-beta01" }
wear-tiles-proto = { group = "androidx.wear.tiles", name = "tiles-proto", version = "1.5.0" }
wear-tooling-preview = { group = "androidx.wear", name = "wear-tooling-preview", version = "1.0.0" }

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

@ -27,4 +27,5 @@
<string name="today_lowercase">dzisiaj</string>
<string name="show_completed">Pokaż ukończone</string>
<string name="show_unstarted">Pokaż nierozpoczęte</string>
</resources>
<string name="requires_pro_subscription">Funkcja Pro</string>
</resources>

@ -24,8 +24,8 @@
<string name="tomorrow_lowercase">завтра</string>
<string name="yest">Вчр</string>
<string name="yesterday_abbrev_lowercase">вчора</string>
<string name="yesterday">Вчора</string>
<string name="yesterday_lowercase">вчора</string>
<string name="yesterday">Учора</string>
<string name="yesterday_lowercase">учора</string>
<string name="today">Сьогодні</string>
<string name="today_lowercase">сьогодні</string>
<string name="add_task">Додати завдання</string>
@ -33,4 +33,4 @@
<string name="show_completed">Показати завершені</string>
<string name="wear_install_app">Установити на телефон</string>
<string name="wear_unknown_error">Невідома помилка</string>
</resources>
</resources>

Loading…
Cancel
Save