diff --git a/app/proguard.pro b/app/proguard.pro index 6ccbb322c..cee1470bd 100644 --- a/app/proguard.pro +++ b/app/proguard.pro @@ -59,5 +59,6 @@ -dontwarn com.google.android.libraries.identity.** -dontwarn edu.umd.cs.findbugs.annotations.** -dontwarn com.google.crypto.tink.subtle.** +-dontwarn net.jcip.annotations.** -keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { ; } diff --git a/app/src/googleplay/java/org/tasks/wear/WearRefresherImpl.kt b/app/src/googleplay/java/org/tasks/wear/WearRefresherImpl.kt index 6f7313e37..e3af5a1b4 100644 --- a/app/src/googleplay/java/org/tasks/wear/WearRefresherImpl.kt +++ b/app/src/googleplay/java/org/tasks/wear/WearRefresherImpl.kt @@ -25,7 +25,7 @@ class WearRefresherImpl( init { phoneDataLayerAppHelper .connectedAndInstalledNodes - .catch { Timber.e(it) } + .catch { Timber.e("${it.message}") } .onEach { nodes -> Timber.d("Connected nodes: ${nodes.joinToString()}") watchConnected = nodes.isNotEmpty() diff --git a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt index f10c7047a..1cec40f89 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt @@ -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, diff --git a/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.kt b/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.kt index 907c56c0b..dec40ec19 100644 --- a/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.kt +++ b/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.kt @@ -7,6 +7,7 @@ import com.google.api.services.tasks.model.Task import com.google.api.services.tasks.model.TaskList import com.google.api.services.tasks.model.TaskLists import org.tasks.googleapis.BaseInvoker +import timber.log.Timber import java.io.IOException /** @@ -43,21 +44,30 @@ class GtasksInvoker( @Throws(IOException::class) suspend fun getAllPositions( - listId: String?, pageToken: String?): com.google.api.services.tasks.model.Tasks? = - execute( - service!! - .tasks() - .list(listId) - .setMaxResults(100) - .setShowDeleted(false) - .setShowHidden(false) - .setPageToken(pageToken) - .setFields("items(id,parent,position),nextPageToken")) + listId: String?, + pageToken: String?, + ): com.google.api.services.tasks.model.Tasks? = + execute( + service!! + .tasks() + .list(listId) + .setMaxResults(100) + .setShowDeleted(false) + .setShowHidden(false) + .setPageToken(pageToken) + .setFields("items(id,parent,position),nextPageToken") + ) @Throws(IOException::class) suspend fun createGtask( - listId: String?, task: Task?, parent: String?, previous: String?): Task? = - execute(service!!.tasks().insert(listId, task).setParent(parent).setPrevious(previous)) + listId: String?, + task: Task?, + parent: String?, + previous: String?, + ): Task? { + Timber.d("createGtask(listId=$listId, task=, parent=$parent, previous=$previous)") + return execute(service!!.tasks().insert(listId, task).setParent(parent).setPrevious(previous)) + } @Throws(IOException::class) suspend fun updateGtask(listId: String?, task: Task) = @@ -65,19 +75,26 @@ class GtasksInvoker( @Throws(IOException::class) suspend fun moveGtask( - listId: String?, taskId: String?, parentId: String?, previousId: String?): Task? = - execute( - service!! - .tasks() - .move(listId, taskId) - .setParent(parentId) - .setPrevious(previousId)) + listId: String?, + taskId: String?, + parentId: String?, + previousId: String?, + ): Task? { + Timber.d("moveGtask(listId=$listId, taskId=$taskId, parentId=$parentId, previousId=$previousId)") + return execute( + service!! + .tasks() + .move(listId, taskId) + .setParent(parentId) + .setPrevious(previousId) + ) + } @Throws(IOException::class) suspend fun deleteGtaskList(listId: String?) { try { execute(service!!.tasklists().delete(listId)) - } catch (ignored: HttpNotFoundException) { + } catch (_: HttpNotFoundException) { } } @@ -91,9 +108,10 @@ class GtasksInvoker( @Throws(IOException::class) suspend fun deleteGtask(listId: String?, taskId: String?) { + Timber.d("deleteGtask(listId=$listId, taskId=$taskId)") try { execute(service!!.tasks().delete(listId, taskId)) - } catch (ignored: HttpNotFoundException) { + } catch (_: HttpNotFoundException) { } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.kt b/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.kt index c345f3133..c18711850 100644 --- a/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.kt +++ b/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.kt @@ -15,11 +15,12 @@ import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator.Companion.getDefaultAlarms import com.todoroo.astrid.service.TaskDeleter import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.delay import org.tasks.LocalBroadcastManager import org.tasks.R import org.tasks.Strings.isNullOrEmpty import org.tasks.analytics.Firebase -import org.tasks.data.* +import org.tasks.data.createDueDate import org.tasks.data.dao.AlarmDao import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.GoogleTaskDao @@ -38,7 +39,7 @@ import java.net.HttpRetryException import java.net.SocketException import java.net.SocketTimeoutException import java.net.UnknownHostException -import java.util.* +import java.util.Collections import javax.inject.Inject import javax.net.ssl.SSLException import kotlin.math.max @@ -125,37 +126,50 @@ class GoogleTaskSynchronizer @Inject constructor( preferences.setString(R.string.p_default_list, null) } } - pushLocalChanges(account, gtasksInvoker) + val failedTasks = mutableSetOf() + var retryTaskId = pushLocalChanges(account, gtasksInvoker) + + while (retryTaskId != null) { + if (failedTasks.contains(retryTaskId)) { + throw IOException("Invalid Task ID: $retryTaskId") + } + failedTasks.add(retryTaskId) + + Timber.d("Retrying push local changes due to stale task ID $retryTaskId (${failedTasks.size} total failed tasks)") + + delay(1000) + + retryTaskId = pushLocalChanges(account, gtasksInvoker) + } for (list in caldavDao.getCalendarsByAccount(account.uuid!!)) { if (isNullOrEmpty(list.uuid)) { firebase.reportException(RuntimeException("Empty remote id")) continue } fetchAndApplyRemoteChanges(gtasksInvoker, list) - if (!preferences.isPositionHackEnabled) { - googleTaskDao.reposition(caldavDao, list.uuid!!) - } - } - if (preferences.isPositionHackEnabled) { - for (list in gtaskLists) { - val tasks = fetchPositions(gtasksInvoker, list.id) - for (task in tasks) { - googleTaskDao.updatePosition(task.id, task.parent, task.position) - } - googleTaskDao.reposition(caldavDao, list.id) - } + gtasksInvoker.updatePositions(list.uuid!!) } // account.etag = eTag account.error = "" } @Throws(IOException::class) - private suspend fun fetchPositions( - gtasksInvoker: GtasksInvoker, listId: String): List { + private suspend fun GtasksInvoker.updatePositions(list: String) { + // Unfortunately this is necessary because Google broke the API + // https://issuetracker.google.com/issues/132432317 + Timber.d("updatePositions(list=${list})") + fetchPositions(list).forEach { task -> + googleTaskDao.updatePosition(task.id, task.parent, task.position) + } + googleTaskDao.reposition(caldavDao, list) + } + + @Throws(IOException::class) + private suspend fun GtasksInvoker.fetchPositions(listId: String): List { val tasks: MutableList = ArrayList() var nextPageToken: String? = null do { - val taskList = gtasksInvoker.getAllPositions(listId, nextPageToken) + val taskList = getAllPositions(listId, nextPageToken) taskList?.items?.let { tasks.addAll(it) } @@ -165,15 +179,19 @@ class GoogleTaskSynchronizer @Inject constructor( } @Throws(IOException::class) - private suspend fun pushLocalChanges(account: CaldavAccount, gtasksInvoker: GtasksInvoker) { + private suspend fun pushLocalChanges(account: CaldavAccount, gtasksInvoker: GtasksInvoker): Long? { val tasks = taskDao.getGoogleTasksToPush(account.uuid!!) for (task in tasks) { - pushTask(task, gtasksInvoker) + val staleTaskId = pushTask(task, gtasksInvoker) + if (staleTaskId != null) { + return staleTaskId + } } + return null } @Throws(IOException::class) - private suspend fun pushTask(task: org.tasks.data.entity.Task, gtasksInvoker: GtasksInvoker) { + private suspend fun pushTask(task: org.tasks.data.entity.Task, gtasksInvoker: GtasksInvoker): Long? { for (deleted in googleTaskDao.getDeletedByTaskId(task.id)) { deleted.remoteId?.let { try { @@ -187,7 +205,7 @@ class GoogleTaskSynchronizer @Inject constructor( } googleTaskDao.delete(deleted) } - val gtasksMetadata = googleTaskDao.getByTaskId(task.id) ?: return + val gtasksMetadata = googleTaskDao.getByTaskId(task.id) ?: return null val remoteModel = Task() var newlyCreated = false val remoteId: String? @@ -208,7 +226,7 @@ class GoogleTaskSynchronizer @Inject constructor( // creating a task which may end up being cancelled. Also don't sync new but already // deleted tasks if (newlyCreated && (isNullOrEmpty(task.title) || task.deletionDate > 0)) { - return + return null } // Update the remote model's changed properties @@ -231,10 +249,11 @@ class GoogleTaskSynchronizer @Inject constructor( val parent = task.parent val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent) else null val previous = googleTaskDao.getPrevious( - listId!!, if (isNullOrEmpty(localParent)) 0 else parent, task.order ?: 0) + listId, if (isNullOrEmpty(localParent)) 0 else parent, task.order ?: 0) val created: Task? = try { gtasksInvoker.createGtask(listId, remoteModel, localParent, previous) } catch (e: HttpNotFoundException) { + Timber.e(e, "Failed to create task, retry without parent or order") gtasksInvoker.createGtask(listId, remoteModel, null, null) } if (created != null) { @@ -242,8 +261,10 @@ class GoogleTaskSynchronizer @Inject constructor( gtasksMetadata.remoteId = created.id gtasksMetadata.calendar = listId setOrderAndParent(gtasksMetadata, created, task) + Timber.d("Created new task: $gtasksMetadata") } else { - return + Timber.e("Empty response when creating task") + return null } } else { try { @@ -252,29 +273,64 @@ class GoogleTaskSynchronizer @Inject constructor( val parent = task.parent val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent) else null val previous = googleTaskDao.getPrevious( - listId!!, + listId, if (localParent.isNullOrBlank()) 0 else parent, - task.order ?: 0) + task.order ?: 0, + ) gtasksInvoker - .moveGtask(listId, remoteModel.id, localParent, previous) - ?.let { setOrderAndParent(gtasksMetadata, it, task) } + .moveGtask( + listId = listId, + taskId = remoteModel.id, + parentId = localParent, + previousId = previous, + ) + ?.let { + setOrderAndParent( + googleTask = gtasksMetadata, + task = it, + local = task, + ) + } } catch (e: GoogleJsonResponseException) { if (e.statusCode == 400) { - Timber.e(e) + Timber.w("HTTP 400: clearing parent and order") + firebase.reportException(e) + taskDao.setParent(0L, listOf(task.id)) + taskDao.setOrder(task.id, 0L) + googleTaskDao.update(gtasksMetadata.copy(isMoved = false)) + return task.id } else { throw e } } } // TODO: don't updateGtask if it was only moved - gtasksInvoker.updateGtask(listId, remoteModel) - } catch (e: HttpNotFoundException) { + try { + gtasksInvoker.updateGtask(listId, remoteModel) + } catch (e: GoogleJsonResponseException) { + if (e.statusCode == 400 && e.details?.message == "Invalid task ID") { + Timber.w("HTTP 400: Invalid task ID for ${remoteModel.id}, clearing to recreate on next sync") + firebase.reportException(e) + googleTaskDao.update( + gtasksMetadata.copy( + remoteId = "", + isMoved = false, + ) + ) + return task.id + } else { + throw e + } + } + } catch (_: HttpNotFoundException) { + Timber.w("HTTP 404, deleting $gtasksMetadata") googleTaskDao.delete(gtasksMetadata) - return + return null } } gtasksMetadata.isMoved = false write(task, gtasksMetadata) + return null } @Throws(IOException::class) diff --git a/app/src/main/java/org/tasks/jobs/SyncWork.kt b/app/src/main/java/org/tasks/jobs/SyncWork.kt index 4a74e9220..a9029ca64 100644 --- a/app/src/main/java/org/tasks/jobs/SyncWork.kt +++ b/app/src/main/java/org/tasks/jobs/SyncWork.kt @@ -96,9 +96,6 @@ class SyncWork @AssistedInject constructor( private val syncStatus = R.string.p_sync_ongoing private suspend fun doSync() { - if (preferences.isManualSort) { - preferences.isPositionHackEnabled = true - } val hasNetworkConnectivity = context.hasNetworkConnectivity() if (hasNetworkConnectivity) { googleTaskJobs().plus(caldavJobs()).awaitAll() diff --git a/app/src/main/java/org/tasks/preferences/Device.kt b/app/src/main/java/org/tasks/preferences/Device.kt index a8bf8d33f..070c35e69 100644 --- a/app/src/main/java/org/tasks/preferences/Device.kt +++ b/app/src/main/java/org/tasks/preferences/Device.kt @@ -26,15 +26,15 @@ class Device @Inject constructor( val pm = context.packageManager val activities = pm.queryIntentActivities(Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0) - return (activities.size != 0) + return activities.isNotEmpty() } - private fun isDontKeepActivitiesEnabled(): Boolean { + private fun isDontKeepActivitiesEnabled(): Boolean? { return try { Settings.Global.getInt(context.contentResolver, Settings.Global.ALWAYS_FINISH_ACTIVITIES) == 1 } catch (e: Exception) { - Timber.e(e) - false + Timber.e("failed to fetch ${Settings.Global.ALWAYS_FINISH_ACTIVITIES}: ${e.message}") + null } } diff --git a/app/src/main/java/org/tasks/preferences/Preferences.kt b/app/src/main/java/org/tasks/preferences/Preferences.kt index 019cead4c..cd0896218 100644 --- a/app/src/main/java/org/tasks/preferences/Preferences.kt +++ b/app/src/main/java/org/tasks/preferences/Preferences.kt @@ -467,10 +467,6 @@ class Preferences @JvmOverloads constructor( fun getPrefs(c: Class): Map = prefs.all.filter { (_, value) -> c.isInstance(value) } as Map - var isPositionHackEnabled: Boolean - get() = getLong(R.string.p_google_tasks_position_hack, 0) > currentTimeMillis() - ONE_WEEK - set(value) { setLong(R.string.p_google_tasks_position_hack, if (value) currentTimeMillis() else 0) } - override var isManualSort: Boolean get() = getBoolean(R.string.p_manual_sort, false) set(value) { setBoolean(R.string.p_manual_sort, value) } diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 6132ab089..137ddd6e1 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -721,4 +721,5 @@ Folytatás szinkronizálás nélkül Segítséget kérek a választásban Kattints a Kész gombra a feladat elmentéséhez + További feladatok megtekintése diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a79707e83..d5982126d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -7,7 +7,7 @@ Utwórz kopię zapasową teraz Zapisano %1$s do %2$s. Podsumowanie odzyskiwania - Plik %1$s zawiera %2$s.\n\n %3$s zaimportowanych,\n %4$s już istnieje\n %5$s zawiera błędy\n + Plik %1$s zawiera %2$s.\n\n %3$s zaimportowanych,\n %4$s już istnieje\n %5$s zawiera błędy Czytanie zadania %d… Uprawnienia Tasks Czy jesteś pewien, że chcesz porzucić zmiany? @@ -703,4 +703,20 @@ Rosnąco Malejąco Wg czasu zakończenia + Ustawienia aplikacji + Przesuń aby uśpić + natychmiast + po 15 minutach + po 30 minutach + po 1 godzinie + po 24 godzinach + Czas drzemki + komentarz + Komentarz + Wczoraj + Wyślij logi aplikacji + Zmień priorytet + Dynamiczny + Kontynuuj bez synchronizacji + Pomóż mi wybrać diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e8063579d..89b3ec499 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -750,4 +750,5 @@ Продолжить без синхронизации Помогите мне выбрать %s будет удалена. Это нельзя отменить! + Просмотреть больше задач diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 326d2f050..408179b13 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -373,7 +373,6 @@ linkify_task_edit preference_screen google_tasks_add_to_top - google_tasks_position_hack wearable_notifications notified_oauth_error_%1$s_%2$s chip_appearance diff --git a/deps_googleplay.txt b/deps_googleplay.txt index 45db69f1b..b4d93b180 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -765,16 +765,16 @@ +| \--- com.google.android.gms:play-services-wearable:19.0.0 (*) ++--- com.google.android.horologist:horologist-datalayer:0.7.15 (*) ++--- com.google.android.gms:play-services-wearable:19.0.0 (*) -++--- com.microsoft.identity.client:msal:6.2.0 -+| +--- com.microsoft.identity:common:21.4.0 -+| | +--- com.microsoft.identity:common4j:21.4.0 -+| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.21 -> 2.2.0 (*) +++--- com.microsoft.identity.client:msal:7.0.0 ++| +--- com.microsoft.identity:common:22.0.0 ++| | +--- com.microsoft.identity:common4j:22.0.0 ++| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.0 -> 2.2.0 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4 -> 1.10.2 (*) +| | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.5.1 -> 2.9.2 (*) +| | +--- androidx.datastore:datastore-preferences:1.0.0 -> 1.1.7 (*) +| | +--- org.apache.httpcomponents.core5:httpcore5:5.3 -+| | +--- com.nimbusds:nimbus-jose-jwt:9.37.3 -+| | | \--- com.github.stephenc.jcip:jcip-annotations:1.0-1 ++| | +--- com.nimbusds:nimbus-jose-jwt:10.0.2 ++| | +--- androidx.activity:activity:1.8.2 -> 1.10.1 (*) +| | +--- androidx.appcompat:appcompat:1.1.0 -> 1.7.1 (*) +| | +--- com.google.code.gson:gson:2.8.9 -> 2.12.1 +| | +--- com.squareup.moshi:moshi:1.14.0 @@ -838,11 +838,11 @@ +| | +--- io.opentelemetry:opentelemetry-api:1.18.0 +| | | \--- io.opentelemetry:opentelemetry-context:1.18.0 +| | \--- androidx.fragment:fragment:1.3.2 -> 1.8.8 (*) -+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.21 -> 2.2.0 (*) ++| +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.0 -> 2.2.0 (*) +| +--- androidx.appcompat:appcompat:1.1.0 -> 1.7.1 (*) +| +--- androidx.browser:browser:1.0.0 -> 1.3.0 (*) +| +--- com.google.code.gson:gson:2.8.9 -> 2.12.1 -+| +--- com.nimbusds:nimbus-jose-jwt:9.37.3 (*) ++| +--- com.nimbusds:nimbus-jose-jwt:10.0.2 +| +--- org.apache.httpcomponents.core5:httpcore5:5.3 +| +--- androidx.constraintlayout:constraintlayout:1.1.3 -> 2.2.1 (*) +| +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.10.2 (*) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2bb6f4e01..f50adb352 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ dav4jvm = "2.2.1" desugar_jdk_libs = "2.1.5" etebase = "2.3.2" firebase = "33.16.0" -firebase-crashlytics-gradle = "3.0.5" +firebase-crashlytics-gradle = "3.0.6" google-oauth2 = "1.37.1" google-api-drive = "v3-rev20250723-2.0.0" google-api-tasks = "v1-rev20250518-2.0.0" @@ -46,7 +46,7 @@ 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" @@ -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.2.0" } +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" } diff --git a/kmp/src/commonMain/composeResources/values-pl/strings.xml b/kmp/src/commonMain/composeResources/values-pl/strings.xml index e5406a1de..3b5680084 100644 --- a/kmp/src/commonMain/composeResources/values-pl/strings.xml +++ b/kmp/src/commonMain/composeResources/values-pl/strings.xml @@ -27,4 +27,5 @@ dzisiaj Pokaż ukończone Pokaż nierozpoczęte - \ No newline at end of file + Funkcja Pro + diff --git a/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/TaskListDrawer.kt b/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/TaskListDrawer.kt index d6f7a4f92..4749ca38b 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/TaskListDrawer.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/TaskListDrawer.kt @@ -94,7 +94,11 @@ fun TaskListDrawer( BottomAppBar( modifier = Modifier .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) + val safeConstraints = constraints.copy( + minHeight = constraints.minHeight.coerceAtLeast(0), + maxHeight = constraints.maxHeight.coerceAtLeast(0) + ) + val placeable = measurable.measure(safeConstraints) bottomAppBarScrollBehavior.state.heightOffsetLimit = -placeable.height.toFloat() val height =