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 - uses: ruby/setup-ruby@v1
with: with:
bundler-cache: true bundler-cache: true
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v5
with: with:
name: release name: release
path: . 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) ### 14.7.3 (2025-06-13)
* Fix dynamic color * Fix dynamic color

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

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

@ -12,8 +12,12 @@ import static java.util.Arrays.asList;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.widget.Toolbar; 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.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -84,12 +88,24 @@ public class BeastModePreferences extends ThemedInjectingAppCompatActivity
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
BeastModePrefActivityBinding binding = BeastModePrefActivityBinding.inflate(getLayoutInflater()); BeastModePrefActivityBinding binding = BeastModePrefActivityBinding.inflate(getLayoutInflater());
Toolbar toolbar = binding.toolbar.toolbar; Toolbar toolbar = binding.toolbar.toolbar;
RecyclerView recyclerView = binding.recyclerView; RecyclerView recyclerView = binding.recyclerView;
setContentView(binding.getRoot()); 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( toolbar.setNavigationIcon(
getDrawable(R.drawable.ic_outline_arrow_back_24px)); getDrawable(R.drawable.ic_outline_arrow_back_24px));
toolbar.setNavigationOnClickListener(v -> finish()); 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.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole 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.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -241,7 +241,7 @@ class MainActivity : AppCompatActivity() {
} }
) )
val navigator = rememberListDetailPaneScaffoldNavigator( val navigator = rememberListDetailPaneScaffoldNavigator(
calculatePaneScaffoldDirective( calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
windowAdaptiveInfo = currentWindowAdaptiveInfo(), windowAdaptiveInfo = currentWindowAdaptiveInfo(),
).copy( ).copy(
horizontalPartitionSpacerSize = 0.dp, horizontalPartitionSpacerSize = 0.dp,

@ -39,6 +39,7 @@ import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider import org.tasks.filters.FilterProvider
import org.tasks.filters.NavigationDrawerSubheader import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.SearchFilter
import org.tasks.filters.getIcon import org.tasks.filters.getIcon
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.TasksPreferences import org.tasks.preferences.TasksPreferences
@ -114,12 +115,9 @@ class MainActivityViewModel @Inject constructor(
) )
} }
updateFilters() updateFilters()
if (filter !is SearchFilter) {
defaultFilterProvider.setLastViewedFilter(filter) defaultFilterProvider.setLastViewedFilter(filter)
} }
fun closeDrawer() {
_drawerOpen.update { false }
_state.update { it.copy(menuQuery = "") }
} }
fun setDrawerState(opened: Boolean) { fun setDrawerState(opened: Boolean) {
@ -238,4 +236,8 @@ class MainActivityViewModel @Inject constructor(
} }
suspend fun getAccount(id: Long) = caldavDao.getAccount(id) 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() { override fun handleOnBackPressed() {
if ((mainViewModel.state.value.filter as? SearchFilter)?.query?.isNotBlank() == true) { if ((mainViewModel.state.value.filter as? SearchFilter)?.query?.isNotBlank() == true) {
lifecycleScope.launch { lifecycleScope.launch {
mainViewModel.resetFilter() mainViewModel.openLastViewedFilter()
} }
if (search.isActionViewExpanded) { if (search.isActionViewExpanded) {
search.collapseActionView() search.collapseActionView()

@ -3,6 +3,9 @@ package com.todoroo.astrid.service
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.annotation.ColorRes 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.ImmutableListMultimap
import com.google.common.collect.ListMultimap import com.google.common.collect.ListMultimap
import com.google.common.collect.Multimaps 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.Tag
import org.tasks.data.entity.TagData import org.tasks.data.entity.TagData
import org.tasks.filters.CaldavFilter import org.tasks.filters.CaldavFilter
import org.tasks.jobs.UpgradeIconSyncWork
import org.tasks.jobs.networkConstraints
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis 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) preferences.setBoolean(R.string.p_just_updated, true)
} else { } else {
setInstallDetails(to) setInstallDetails(to)
@ -407,6 +421,7 @@ class Upgrader @Inject constructor(
const val V12_8 = 120800 const val V12_8 = 120800
const val V14_5_4 = 140516 const val V14_5_4 = 140516
const val V14_6_1 = 140602 const val V14_6_1 = 140602
const val V14_8 = 140800
@JvmStatic @JvmStatic
fun getAndroidColor(context: Context, index: Int): Int { fun getAndroidColor(context: Context, index: Int): Int {

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

@ -8,10 +8,16 @@ import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.activity.enableEdgeToEdge
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar 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.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -70,8 +76,18 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityCaldavAccountSettingsBinding.inflate(layoutInflater) binding = ActivityCaldavAccountSettingsBinding.inflate(layoutInflater)
setContentView(binding.root) 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) caldavAccount = if (savedInstanceState == null) intent.getParcelableExtra(EXTRA_CALDAV_DATA) else savedInstanceState.getParcelable(EXTRA_CALDAV_DATA)
serverType = mutableStateOf( serverType = mutableStateOf(
savedInstanceState?.getInt(EXTRA_SERVER_TYPE, SERVER_UNKNOWN) savedInstanceState?.getInt(EXTRA_SERVER_TYPE, SERVER_UNKNOWN)

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

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

@ -11,24 +11,32 @@ import at.bitfire.dav4jvm.XmlUtils.NS_CALDAV
import at.bitfire.dav4jvm.XmlUtils.NS_WEBDAV import at.bitfire.dav4jvm.XmlUtils.NS_WEBDAV
import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException 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 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.property.CalendarIcon
import org.tasks.caldav.property.Invite import org.tasks.caldav.property.Invite
import org.tasks.caldav.property.OCInvite import org.tasks.caldav.property.OCInvite
import org.tasks.caldav.property.OCOwnerPrincipal import org.tasks.caldav.property.OCOwnerPrincipal
import org.tasks.caldav.property.PropertyUtils.NS_OWNCLOUD import org.tasks.caldav.property.PropertyUtils.NS_OWNCLOUD
import org.tasks.caldav.property.ShareAccess import org.tasks.caldav.property.ShareAccess
import org.tasks.data.UUIDHelper
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_NEXTCLOUD import org.tasks.data.entity.CaldavAccount.Companion.SERVER_NEXTCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OWNCLOUD import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OWNCLOUD
@ -101,9 +109,12 @@ open class CaldavClient(
.findHomeset() .findHomeset()
} }
suspend fun calendars(interceptor: (Interceptor.Chain) -> okhttp3.Response): List<Response> = suspend fun calendars(interceptor: (okhttp3.Response) -> okhttp3.Response = { it }): List<Response> =
DavResource( DavResource(
httpClient.newBuilder().addNetworkInterceptor(interceptor).build(), httpClient
.newBuilder()
.addNetworkInterceptor { interceptor(it.proceed(it.request())) }
.build(),
httpUrl!! httpUrl!!
) )
.propfind(1, *calendarProperties) .propfind(1, *calendarProperties)
@ -120,33 +131,44 @@ open class CaldavClient(
} }
@Throws(IOException::class, XmlPullParserException::class, HttpException::class) @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 davResource = DavResource(httpClient, httpUrl!!.resolve(UUIDHelper.newUUID() + "/")!!)
val mkcolString = getMkcolString(displayName, color) val mkcolString = getMkcolString(displayName, color)
davResource.mkCol(mkcolString) {} davResource.mkCol(mkcolString) {}
if (icon?.isNotBlank() == true) {
davResource.proppatch(CalendarIcon.NAME, icon)
}
davResource.location.toString() davResource.location.toString()
} }
@Throws(IOException::class, XmlPullParserException::class, HttpException::class) @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) { withContext(Dispatchers.IO) {
with(DavResource(httpClient, httpUrl!!)) { with(DavResource(httpClient, httpUrl!!)) {
proppatch( proppatch(DisplayName.NAME, displayName)
setProperties = mutableMapOf(DisplayName.NAME to displayName).apply {
if (color != 0) { if (color != 0) {
put( proppatch(
CalendarColor.NAME, CalendarColor.NAME,
String.format("#%06X%02X", color and 0xFFFFFF, color ushr 24) String.format("#%06X%02X", color and 0xFFFFFF, color ushr 24)
) )
} }
}, if (icon?.isNotBlank() == true) {
removeProperties = if (color == 0) listOf(CalendarColor.NAME) else emptyList(), proppatch(CalendarIcon.NAME, icon)
callback = { _, _ -> }, }
)
location.toString() 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) @Throws(IOException::class, XmlPullParserException::class)
private fun getMkcolString(displayName: String, color: Int): String { private fun getMkcolString(displayName: String, color: Int): String {
val xmlPullParserFactory = XmlPullParserFactory.newInstance() val xmlPullParserFactory = XmlPullParserFactory.newInstance()
@ -296,6 +318,7 @@ open class CaldavClient(
OCInvite.NAME, OCInvite.NAME,
CurrentUserPrivilegeSet.NAME, CurrentUserPrivilegeSet.NAME,
CurrentUserPrincipal.NAME, CurrentUserPrincipal.NAME,
CalendarIcon.NAME,
) )
private suspend fun DavResource.propfind( private suspend fun DavResource.propfind(
@ -311,5 +334,22 @@ open class CaldavClient(
cont.resumeWith(Result.success(responses)) 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.analytics.Firebase
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.caldav.iCalendar.Companion.fromVtodo import org.tasks.caldav.iCalendar.Companion.fromVtodo
import org.tasks.caldav.property.CalendarIcon
import org.tasks.caldav.property.Invite import org.tasks.caldav.property.Invite
import org.tasks.caldav.property.OCAccess import org.tasks.caldav.property.OCAccess
import org.tasks.caldav.property.OCInvite import org.tasks.caldav.property.OCInvite
import org.tasks.caldav.property.OCOwnerPrincipal import org.tasks.caldav.property.OCOwnerPrincipal
import org.tasks.caldav.property.OCUser 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
import org.tasks.caldav.property.ShareAccess.Companion.NOT_SHARED import org.tasks.caldav.property.ShareAccess.Companion.NOT_SHARED
import org.tasks.caldav.property.ShareAccess.Companion.NO_ACCESS import org.tasks.caldav.property.ShareAccess.Companion.NO_ACCESS
@ -136,8 +136,7 @@ class CaldavSynchronizer @Inject constructor(
private suspend fun synchronize(account: CaldavAccount) { private suspend fun synchronize(account: CaldavAccount) {
val caldavClient = provider.forAccount(account) val caldavClient = provider.forAccount(account)
var serverType = account.serverType var serverType = account.serverType
val resources = caldavClient.calendars { chain -> val resources = caldavClient.calendars { response ->
val response = chain.proceed(chain.request())
if (serverType == SERVER_UNKNOWN) { if (serverType == SERVER_UNKNOWN) {
serverType = getServerType(account, response.headers) serverType = getServerType(account, response.headers)
} }
@ -155,8 +154,10 @@ class CaldavSynchronizer @Inject constructor(
val url = resource.href.toString() val url = resource.href.toString()
var calendar = caldavDao.getCalendarByUrl(account.uuid!!, url) var calendar = caldavDao.getCalendarByUrl(account.uuid!!, url)
val remoteName = resource[DisplayName::class.java]!!.displayName 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 access = resource.accessLevel
val icon = resource[CalendarIcon::class.java]?.icon?.takeIf { it.isNotBlank() }
if (access == ACCESS_UNKNOWN) { if (access == ACCESS_UNKNOWN) {
firebase.logEvent( firebase.logEvent(
R.string.event_sync_unknown_access, R.string.event_sync_unknown_access,
@ -164,7 +165,6 @@ class CaldavSynchronizer @Inject constructor(
(resource[ShareAccess::class.java]?.access?.toString() ?: "???") (resource[ShareAccess::class.java]?.access?.toString() ?: "???")
) )
} }
val color = calendarColor?.color ?: 0
if (calendar == null) { if (calendar == null) {
calendar = CaldavCalendar( calendar = CaldavCalendar(
name = remoteName, name = remoteName,
@ -173,15 +173,20 @@ class CaldavSynchronizer @Inject constructor(
uuid = UUIDHelper.newUUID(), uuid = UUIDHelper.newUUID(),
color = color, color = color,
access = access, access = access,
icon = icon,
) )
caldavDao.insert(calendar) caldavDao.insert(calendar)
} else if (calendar.name != remoteName } else if (calendar.name != remoteName
|| calendar.color != color || calendar.color != color
|| calendar.access != access || calendar.access != access
|| (icon != null && calendar.icon != icon)
) { ) {
calendar.color = color calendar = calendar.copy(
calendar.name = remoteName color = color,
calendar.access = access name = remoteName,
access = access,
icon = icon ?: calendar.icon,
)
caldavDao.update(calendar) caldavDao.update(calendar)
localBroadcastManager.broadcastRefreshList() localBroadcastManager.broadcastRefreshList()
} }
@ -436,10 +441,13 @@ class CaldavSynchronizer @Inject constructor(
fun registerFactories() { fun registerFactories() {
PropertyRegistry.register( PropertyRegistry.register(
listOf(
ShareAccess.Factory(), ShareAccess.Factory(),
Invite.Factory(), Invite.Factory(),
OCOwnerPrincipal.Factory(), OCOwnerPrincipal.Factory(),
OCInvite.Factory(), OCInvite.Factory(),
CalendarIcon.Factory,
)
) )
} }

@ -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.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar 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.CaldavTask
import org.tasks.data.entity.Place import org.tasks.data.entity.Place
import org.tasks.data.entity.TagData import org.tasks.data.entity.TagData
@ -208,7 +207,7 @@ class iCalendar @Inject constructor(
val task = existing?.task val task = existing?.task
?.let { taskDao.fetch(it) } ?.let { taskDao.fetch(it) }
?: taskCreator.createWithValues("").apply { ?: taskCreator.createWithValues("").apply {
readOnly = calendar.access == ACCESS_READ_ONLY readOnly = calendar.readOnly()
taskDao.createNew(this) taskDao.createNew(this)
} }
val caldavTask = 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 package org.tasks.caldav.property
import at.bitfire.dav4jvm.PropertyFactory
import at.bitfire.dav4jvm.PropertyRegistry
object PropertyUtils { object PropertyUtils {
const val NS_TASKS = "http://org.tasks/ns/"
const val NS_OWNCLOUD = "http://owncloud.org/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 modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.fillMaxWidth(), .fillMaxWidth(),
shadowElevation = 8.dp,
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
) { ) {
Row( Row(

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

@ -3,6 +3,7 @@ package org.tasks.injection
import android.content.Context import android.content.Context
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.todoroo.andlib.utility.AndroidUtilities.atLeastAndroid16
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import timber.log.Timber import timber.log.Timber
@ -14,7 +15,11 @@ abstract class BaseWorker(
) : Worker(context, workerParams) { ) : Worker(context, workerParams) {
override fun doWork(): Result { 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 { return try {
runBlocking { runBlocking {
run() run()

@ -34,7 +34,7 @@ class MigrateLocalWork @AssistedInject constructor(
caldavDao.getCalendarsByAccount(fromAccount.uuid!!).forEach { caldavDao.getCalendarsByAccount(fromAccount.uuid!!).forEach {
caldavDao.update( caldavDao.update(
it.copy( it.copy(
url = caldavClient.makeCollection(it.name!!, it.color), url = caldavClient.makeCollection(it.name!!, it.color, it.icon),
account = caldavAccount.uuid, 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) .setConstraints(networkConstraints)
workManager.enqueueUniquePeriodicWork( workManager.enqueueUniquePeriodicWork(
TAG_BACKGROUND_SYNC, TAG_BACKGROUND_SYNC,
ExistingPeriodicWorkPolicy.KEEP, ExistingPeriodicWorkPolicy.UPDATE,
builder.build() builder.build()
) )
} else { } else {
@ -183,7 +183,7 @@ class WorkManagerImpl(
throttle.run { throttle.run {
workManager.enqueueUniquePeriodicWork( workManager.enqueueUniquePeriodicWork(
TAG_REMOTE_CONFIG, TAG_REMOTE_CONFIG,
ExistingPeriodicWorkPolicy.KEEP, ExistingPeriodicWorkPolicy.UPDATE,
PeriodicWorkRequest.Builder( PeriodicWorkRequest.Builder(
RemoteConfigWork::class.java, REMOTE_CONFIG_INTERVAL_HOURS, TimeUnit.HOURS) RemoteConfigWork::class.java, REMOTE_CONFIG_INTERVAL_HOURS, TimeUnit.HOURS)
.setConstraints(networkConstraints) .setConstraints(networkConstraints)
@ -207,9 +207,6 @@ class WorkManagerImpl(
enqueue(builder) enqueue(builder)
} }
private val networkConstraints: Constraints
get() = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
override fun updatePurchases() = override fun updatePurchases() =
enqueueUnique(TAG_UPDATE_PURCHASES, UpdatePurchaseWork::class.java) 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( private fun <B : WorkRequest.Builder<B, *>, W : WorkRequest> WorkRequest.Builder<B, W>.setInputData(
vararg pairs: Pair<String, Any?> vararg pairs: Pair<String, Any?>
): B = setInputData(workDataOf(*pairs)) ): 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.os.Parcelable
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -15,6 +16,9 @@ import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat 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.core.widget.ContentLoadingProgressBar
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -66,6 +70,7 @@ class LocationPickerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListe
private lateinit var loadingIndicator: ContentLoadingProgressBar private lateinit var loadingIndicator: ContentLoadingProgressBar
private lateinit var chooseRecentLocation: View private lateinit var chooseRecentLocation: View
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var selectThisLocation: View
@Inject lateinit var theme: Theme @Inject lateinit var theme: Theme
@Inject lateinit var locationDao: LocationDao @Inject lateinit var locationDao: LocationDao
@ -88,6 +93,7 @@ class LocationPickerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListe
private lateinit var search: MenuItem private lateinit var search: MenuItem
private var searchJob: Job? = null private var searchJob: Job? = null
private val viewModel: PlaceSearchViewModel by viewModels() private val viewModel: PlaceSearchViewModel by viewModels()
private var systemBarsBottom = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -95,6 +101,13 @@ class LocationPickerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListe
window.statusBarColor = ContextCompat.getColor(this, android.R.color.transparent) window.statusBarColor = ContextCompat.getColor(this, android.R.color.transparent)
val binding = ActivityLocationPickerBinding.inflate(layoutInflater) val binding = ActivityLocationPickerBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
systemBarsBottom = systemBars.bottom
insets
}
toolbar = binding.toolbar toolbar = binding.toolbar
appBarLayout = binding.appBarLayout appBarLayout = binding.appBarLayout
toolbarLayout = binding.collapsingToolbarLayout toolbarLayout = binding.collapsingToolbarLayout
@ -105,6 +118,7 @@ class LocationPickerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListe
loadingIndicator = binding.loadingIndicator loadingIndicator = binding.loadingIndicator
chooseRecentLocation = binding.chooseRecentLocation chooseRecentLocation = binding.chooseRecentLocation
recyclerView = binding.recentLocations recyclerView = binding.recentLocations
selectThisLocation = binding.selectThisLocation
val configuration = resources.configuration val configuration = resources.configuration
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.smallestScreenWidthDp < 480) { && configuration.smallestScreenWidthDp < 480) {
@ -318,9 +332,15 @@ class LocationPickerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListe
params.height = height params.height = height
chooseRecentLocation.visibility = View.GONE chooseRecentLocation.visibility = View.GONE
collapseToolbar() collapseToolbar()
selectThisLocation.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = systemBarsBottom
}
} else { } else {
params.height = height * 75 / 100 params.height = height * 75 / 100
chooseRecentLocation.visibility = View.VISIBLE chooseRecentLocation.visibility = View.VISIBLE
selectThisLocation.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = 0
}
} }
} }

@ -3,7 +3,12 @@ package org.tasks.preferences
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar 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.fragment.app.Fragment
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
@ -22,8 +27,21 @@ abstract class BasePreferences : ThemedInjectingAppCompatActivity(),
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
val binding = ActivityPreferencesBinding.inflate(layoutInflater) val binding = ActivityPreferencesBinding.inflate(layoutInflater)
setContentView(binding.root) 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 toolbar = binding.toolbar.toolbar
if (savedInstanceState == null) { if (savedInstanceState == null) {
val rootPreference = getRootPreference() val rootPreference = getRootPreference()

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

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

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

@ -119,6 +119,8 @@ internal class TasksWidgetViewFactory(
private fun buildFooter(): RemoteViews { private fun buildFooter(): RemoteViews {
return RemoteViews(BuildConfig.APPLICATION_ID, R.layout.widget_footer).apply { 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( setOnClickFillInIntent(
R.id.widget_view_more, R.id.widget_view_more,
Intent(WidgetClickActivity.OPEN_TASK_LIST) Intent(WidgetClickActivity.OPEN_TASK_LIST)

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

@ -743,4 +743,5 @@
<string name="help_me_choose">Pomozte mi s výběrem</string> <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="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="continue_without_sync">Pokračovat bez synchronizace</string>
<string name="widget_view_more_tasks">Zobrazit další úkoly</string>
</resources> </resources>

@ -721,4 +721,5 @@
<string name="continue_without_sync">Fortsæt uden synkronisering</string> <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="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="delete_tasks_warning">%s bliver slettet. Dette kan ikke fortrydes!</string>
<string name="widget_view_more_tasks">Vis flere opgaver</string>
</resources> </resources>

@ -721,4 +721,5 @@
<string name="help_me_choose">Hilf mir bei der Auswahl</string> <string name="help_me_choose">Hilf mir bei der Auswahl</string>
<string name="continue_without_sync">Weiter ohne Synchronisierung</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="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> </resources>

@ -721,4 +721,5 @@
<string name="continue_without_sync">Jätka ilma sünkroniseerimata</string> <string name="continue_without_sync">Jätka ilma sünkroniseerimata</string>
<string name="help_me_choose">Aita mul valida</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="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> </resources>

@ -743,4 +743,5 @@
<string name="help_me_choose">Aidez-moi à choisir</string> <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="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="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> </resources>

@ -721,4 +721,5 @@
<string name="continue_without_sync">Folytatás szinkronizálás nélkül</string> <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="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="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> </resources>

@ -9,7 +9,7 @@
<string name="import_summary_title">Riepilogo del ripristino</string> <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_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="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="discard_confirmation">Vuoi davvero annullare tutte le modifiche\?</string>
<string name="keep_editing">No, continua la modifica</string> <string name="keep_editing">No, continua la modifica</string>
<string name="DLG_delete_this_task_question">Eliminare questa attività\?</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="location_radius_meters">%s m</string>
<string name="subtasks">Attività secondaria</string> <string name="subtasks">Attività secondaria</string>
<string name="TEA_timer_controls">Timer</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="chips">Smart Chips</string>
<string name="custom_filter_not">NON</string> <string name="custom_filter_not">NON</string>
<string name="custom_filter_or">O</string> <string name="custom_filter_or">O</string>
@ -743,4 +743,5 @@
<string name="continue_without_sync">Continua senza sincronizzazione</string> <string name="continue_without_sync">Continua senza sincronizzazione</string>
<string name="help_me_choose">Aiutami a scegliere</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="delete_tasks_warning">%s verrà cancellato. L\'operazione non può essere annullata!</string>
<string name="widget_view_more_tasks">Visualizza ulteriori attività</string>
</resources> </resources>

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

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

@ -7,7 +7,7 @@
<string name="backup_BAc_export">Utwórz kopię zapasową teraz</string> <string name="backup_BAc_export">Utwórz kopię zapasową teraz</string>
<string name="export_toast">Zapisano %1$s do %2$s.</string> <string name="export_toast">Zapisano %1$s do %2$s.</string>
<string name="import_summary_title">Podsumowanie odzyskiwania</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="import_progress_read">Czytanie zadania %d…</string>
<string name="read_permission_label">Uprawnienia Tasks</string> <string name="read_permission_label">Uprawnienia Tasks</string>
<string name="discard_confirmation">Czy jesteś pewien, że chcesz porzucić zmiany?</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_ascending">Rosnąco</string>
<string name="sort_descending">Malejąco</string> <string name="sort_descending">Malejąco</string>
<string name="sort_completed">Wg czasu zakończenia</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> </resources>

@ -38,8 +38,8 @@
<string name="TEA_timer_elap">Decorrido %s</string> <string name="TEA_timer_elap">Decorrido %s</string>
<string name="due_date">Data de vencimento</string> <string name="due_date">Data de vencimento</string>
<string name="due_time">Na hora de vencimento</string> <string name="due_time">Na hora de vencimento</string>
<string name="day_before_due">Dias antes do vencimento</string> <string name="day_before_due">Um dia antes do vencimento</string>
<string name="week_before_due">Semanas 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_repeat">Repetir</string>
<string name="TEA_control_gcal">Calendário</string> <string name="TEA_control_gcal">Calendário</string>
<string name="TEA_control_importance">Prioridade</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_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_list">Lista de tarefas</string>
<string name="auto_dismiss_datetime">Fechar automaticamente o seletor de data e hora</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_time">Sem horário</string>
<string name="no_date">Sem data</string> <string name="no_date">Sem data</string>
<string name="chip_appearance_icon_only">Somente o ícone</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="delete_tasks_warning">%s silinecek. Bu geri alınamaz!</string>
<string name="continue_without_sync">Eşzamanlamadan sürdür</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="help_me_choose">Seçmeme yardım et</string>
<string name="widget_view_more_tasks">Daha çok görev gör</string>
</resources> </resources>

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

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

@ -1,17 +1,18 @@
package org.tasks.caldav.property package org.tasks.caldav.property
import at.bitfire.dav4jvm.PropertyRegistry 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.Before
import org.junit.Test 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.ShareAccess.Companion.SHARED_OWNER
import org.tasks.caldav.property.TestPropertyUtils.toProperty import org.tasks.caldav.property.TestPropertyUtils.toProperty
class InviteTest { class InviteTest {
@Before @Before
fun setUp() { fun setUp() {
PropertyRegistry.register(ShareAccess.Factory(), Invite.Factory()) PropertyRegistry.register(listOf(ShareAccess.Factory(), Invite.Factory()))
} }
@Test @Test

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

@ -49,4 +49,6 @@ data class CaldavCalendar(
@JvmField val NAME = TABLE.column("cdl_name") @JvmField val NAME = TABLE.column("cdl_name")
@JvmField val ORDER = TABLE.column("cdl_order") @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] [versions]
versionCode = "140710" # increment by 2 versionCode = "140802" # increment by 2
versionName = "14.7.3" versionName = "14.8"
agp = "8.11.0" agp = "8.12.0"
android-compileSdk = "36" android-compileSdk = "36"
android-minSdk = "26" android-minSdk = "26"
android-targetSdk = "34" android-targetSdk = "35"
accompanist = "0.37.3" accompanist = "0.37.3"
activity-compose = "1.10.1" activity-compose = "1.10.1"
appauth = "0.11.1" appauth = "0.11.1"
appcompat = "1.7.1" appcompat = "1.7.1"
cert4android = "7814052" cert4android = "7814052"
coil = "2.7.0" coil = "2.7.0"
compose = "2025.06.01" compose = "2025.07.00"
constraintlayout = "2.2.1" constraintlayout = "2.2.1"
dagger-hilt = "2.56.2" dagger-hilt = "2.57"
dashclock-api = "2.0.0" dashclock-api = "2.0.0"
dav4jvm = "2.2.1" dav4jvm = "2.2.1"
desugar_jdk_libs = "2.1.5" desugar_jdk_libs = "2.1.5"
etebase = "2.3.2" etebase = "2.3.2"
firebase = "33.16.0" firebase = "33.16.0"
firebase-crashlytics-gradle = "3.0.4" firebase-crashlytics-gradle = "3.0.6"
google-oauth2 = "1.37.1" 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-api-tasks = "v1-rev20250518-2.0.0"
google-services = "4.4.3" google-services = "4.4.3"
grpc = "1.73.0" grpc = "1.73.0"
hilt = "1.2.0" hilt = "1.2.0"
horologist = "0.6.23" horologist = "0.7.15"
ical4android = "2fe63dd" ical4android = "2fe63dd"
jchronic = "0.2.6" jchronic = "0.2.6"
jems = "1.33" jems = "1.33"
junit-junit = "4.13.2" junit-junit = "4.13.2"
junit = "1.2.1" junit = "1.3.0"
kotlin = "2.1.21" kotlin = "2.1.21"
kotlinx-coroutines = "1.10.2" kotlinx-coroutines = "1.10.2"
ktor = "3.1.3" ktor = "3.1.3"
leakcanary = "2.14" leakcanary = "2.14"
lib-recur = "0.11.4" lib-recur = "0.11.4"
lifecycle = "2.9.1" lifecycle = "2.9.2"
locale = "1.0.4" locale = "1.0.4"
make-it-easy = "4.0.1" make-it-easy = "4.0.1"
markwon = "4.6.2" markwon = "4.6.2"
@ -46,11 +46,11 @@ mockito = "5.18.0"
okhttp = "4.12.0" okhttp = "4.12.0"
opentasks = "562fec5" opentasks = "562fec5"
osmdroid = "6.1.20" osmdroid = "6.1.20"
oss-licenses-plugin = "0.10.6" oss-licenses-plugin = "0.10.7"
persistent-cookiejar = "1.0.1" persistent-cookiejar = "1.0.1"
play-services-maps = "19.2.0" play-services-maps = "19.2.0"
play-services-location = "21.3.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" preference = "1.2.1"
protobuf = "4.31.1" protobuf = "4.31.1"
recyclerview = "1.4.0" recyclerview = "1.4.0"
@ -59,9 +59,9 @@ room = "2.7.2"
shortcut-badger = "1.1.22" shortcut-badger = "1.1.22"
timber = "5.0.1" timber = "5.0.1"
swiperefreshlayout = "1.1.0" swiperefreshlayout = "1.1.0"
work = "2.10.2" work = "2.10.3"
androidx-test = "1.6.1" androidx-test = "1.7.0"
androidx-test-runner = "1.6.2" androidx-test-runner = "1.7.0"
xpp3 = "1.1.6" xpp3 = "1.1.6"
wearCompose = "1.4.1" 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 = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", 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-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-paging-compose = { module = "androidx.paging:paging-compose", version = "3.3.6" }
androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" } androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } 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-tables = { module = "io.noties.markwon:ext-tables", version.ref = "markwon" }
markwon-tasklist = { module = "io.noties.markwon:ext-tasklist", version.ref = "markwon" } markwon-tasklist = { module = "io.noties.markwon:ext-tasklist", version.ref = "markwon" }
material = { module = "com.google.android.material:material", version.ref = "material" } 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-android = { module = "org.mockito:mockito-android", version.ref = "mockito" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } 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-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-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-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-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" } wear-tooling-preview = { group = "androidx.wear", name = "wear-tooling-preview", version = "1.0.0" }

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

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

@ -24,8 +24,8 @@
<string name="tomorrow_lowercase">завтра</string> <string name="tomorrow_lowercase">завтра</string>
<string name="yest">Вчр</string> <string name="yest">Вчр</string>
<string name="yesterday_abbrev_lowercase">вчора</string> <string name="yesterday_abbrev_lowercase">вчора</string>
<string name="yesterday">Вчора</string> <string name="yesterday">Учора</string>
<string name="yesterday_lowercase">вчора</string> <string name="yesterday_lowercase">учора</string>
<string name="today">Сьогодні</string> <string name="today">Сьогодні</string>
<string name="today_lowercase">сьогодні</string> <string name="today_lowercase">сьогодні</string>
<string name="add_task">Додати завдання</string> <string name="add_task">Додати завдання</string>

Loading…
Cancel
Save