From 800f5ccaf16a123f4d57eb7f9b258a47c2532c3a Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 29 Jun 2026 00:30:41 +0200 Subject: [PATCH 1/2] Refactor DomainError logic, remove unused LiveData from Kv store, and update testing dependencies. ### Core & Data - **DomainError**: Simplified the `plus` operator implementation using safe casts and the elvis operator. - **Kv (Dao, Repository, Proxy)**: - Removed `getLive` and `observeKv` methods to streamline the API towards coroutines. - Updated `KvProxy` to explicitly enable `ignoreUnknownKeys` in JSON decoding. - **FolderSyncService**: Improved readability of update logic with additional parentheses. ### IO & Utils - **XoppFile**: Refactored `fastParseFloat` to use a more idiomatic `when (val c = ...)` expression and grouped character branches. - **Formatting**: Added trailing commas and improved line wrapping in `SyncSettings`, `checkPermissions`, and `DomainError` for better maintainability. ### Testing - **EditorUnsupportedConcurrentChangeTests**: - Migrated to `androidx.compose.ui.test.junit4.v2.createComposeRule`. - Removed unused coroutine imports (`delay`, `filter`, `first`, `withTimeout`). - Standardized line breaks for long log messages and assignments. --- .../EditorUnsupportedConcurrentChangeTests.kt | 14 +++++++------- .../main/java/com/ethran/notable/data/db/Kv.kt | 18 +----------------- .../java/com/ethran/notable/io/XoppFile.kt | 13 ++++++------- .../ethran/notable/sync/FolderSyncService.kt | 2 +- .../com/ethran/notable/sync/SyncSettings.kt | 2 +- .../java/com/ethran/notable/utils/AppResult.kt | 7 ++++--- .../ethran/notable/utils/checkPermissions.kt | 2 +- 7 files changed, 21 insertions(+), 37 deletions(-) diff --git a/app/src/androidTest/java/com/ethran/notable/editor/EditorUnsupportedConcurrentChangeTests.kt b/app/src/androidTest/java/com/ethran/notable/editor/EditorUnsupportedConcurrentChangeTests.kt index c44b252b..0d7c224a 100644 --- a/app/src/androidTest/java/com/ethran/notable/editor/EditorUnsupportedConcurrentChangeTests.kt +++ b/app/src/androidTest/java/com/ethran/notable/editor/EditorUnsupportedConcurrentChangeTests.kt @@ -6,7 +6,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.snapshots.Snapshot -import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.ethran.notable.data.AppRepository @@ -38,11 +38,7 @@ import io.mockk.mockk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout import org.junit.After import org.junit.Assert.assertTrue import org.junit.Before @@ -84,7 +80,10 @@ class EditorUnsupportedConcurrentChangeTests { */ @Test(timeout = 60_000) fun selectionState_isNotWrittenOffMainThread_duringPageSwitch_compose() { - android.util.Log.i("EditorTest", "Starting test: selectionState_isNotWrittenOffMainThread_duringPageSwitch_compose") + android.util.Log.i( + "EditorTest", + "Starting test: selectionState_isNotWrittenOffMainThread_duringPageSwitch_compose" + ) val seeded = runBlocking { android.util.Log.i("EditorTest", "Seeding notebook...") TestNotebookSeeder.seedNotebook(db, pageCount = 3, strokesPerPage = 10) @@ -114,7 +113,8 @@ class EditorUnsupportedConcurrentChangeTests { android.util.Log.i("EditorTest", "Seeding selection on UI thread...") composeRule.runOnUiThread { try { - viewModel.selectionState.selectedStrokes = listOf(dummyStroke(pageId = seeded.pageIds.first())) + viewModel.selectionState.selectedStrokes = + listOf(dummyStroke(pageId = seeded.pageIds.first())) viewModel.selectionState.placementMode = PlacementMode.Move Snapshot.sendApplyNotifications() } catch (e: Throwable) { diff --git a/app/src/main/java/com/ethran/notable/data/db/Kv.kt b/app/src/main/java/com/ethran/notable/data/db/Kv.kt index bfae3077..ba9dc961 100644 --- a/app/src/main/java/com/ethran/notable/data/db/Kv.kt +++ b/app/src/main/java/com/ethran/notable/data/db/Kv.kt @@ -2,7 +2,6 @@ package com.ethran.notable.data.db import android.content.Context import androidx.lifecycle.LiveData -import androidx.lifecycle.map import androidx.room.Dao import androidx.room.Entity import androidx.room.Insert @@ -39,9 +38,6 @@ interface KvDao { @Query("SELECT * FROM kv WHERE `key`=:key") suspend fun get(key: String): Kv? - @Query("SELECT * FROM kv WHERE `key`=:key") - fun getLive(key: String): LiveData - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun set(kv: Kv) @@ -67,10 +63,6 @@ class KvRepository @Inject constructor( db.get(key) } - fun getLive(key: String): LiveData { - return db.getLive(key) - } - suspend fun set(kv: Kv) = withContext(Dispatchers.IO) { checkPermission() db.set(kv) @@ -101,17 +93,9 @@ class KvProxy @Inject constructor( private val cryptoHelper: CryptoHelper ) { private val log = ShipBook.getLogger("KvProxy") - private val json = Json //{ ignoreUnknownKeys = true } + private val json = Json { ignoreUnknownKeys = true } - fun observeKv(key: String, serializer: KSerializer, default: T): LiveData { - return kvRepository.getLive(key).map { - if (it == null) return@map default - val jsonValue = it.value - json.decodeFromString(serializer, jsonValue) - } - } - suspend fun get(key: String, serializer: KSerializer): T? = withContext(Dispatchers.IO) { val kv = kvRepository.get(key) ?: return@withContext null //returns null when there is no database diff --git a/app/src/main/java/com/ethran/notable/io/XoppFile.kt b/app/src/main/java/com/ethran/notable/io/XoppFile.kt index a2ebf999..f24606cd 100644 --- a/app/src/main/java/com/ethran/notable/io/XoppFile.kt +++ b/app/src/main/java/com/ethran/notable/io/XoppFile.kt @@ -59,7 +59,7 @@ class XoppFile @Inject constructor( @param:ApplicationContext private val context: Context, private val pageRepo: PageRepository, private val bookRepo: BookRepository, - private val appEventBus: AppEventBus + private val appEventBus: AppEventBus, ) { private val log = ShipBook.getLogger("XoppFile") private val scaleFactor = A4_WIDTH.toFloat() / SCREEN_WIDTH @@ -165,7 +165,7 @@ class XoppFile @Inject constructor( writer.write("\" width=\"") writer.write((stroke.size * scaleFactor).toString()) - if (stroke.pen == Pen.FOUNTAIN || stroke.pen == Pen.BRUSH || stroke.pen == Pen.PENCIL) { + if ((stroke.pen == Pen.FOUNTAIN) || (stroke.pen == Pen.BRUSH) || (stroke.pen == Pen.PENCIL)) { stroke.points.forEach { point -> writer.write(" ") writer.write( @@ -486,10 +486,9 @@ class XoppFile @Inject constructor( var isFraction = false while (i < end) { - val c = input[i] - when { - c == '.' -> isFraction = true - c in '0'..'9' -> { + when (val c = input[i]) { + '.' -> isFraction = true + in '0'..'9' -> { val digit = c - '0' if (isFraction) { divisor *= 10.0 @@ -499,7 +498,7 @@ class XoppFile @Inject constructor( } } // Scientific notation is rare; only then pay the allocation cost - c == 'e' || c == 'E' -> return input.subSequence(start, end).toString().toFloat() + 'e', 'E' -> return input.subSequence(start, end).toString().toFloat() else -> return input.subSequence(start, end).toString().toFloat() } i++ diff --git a/app/src/main/java/com/ethran/notable/sync/FolderSyncService.kt b/app/src/main/java/com/ethran/notable/sync/FolderSyncService.kt index 51a9c148..cf51cb2c 100644 --- a/app/src/main/java/com/ethran/notable/sync/FolderSyncService.kt +++ b/app/src/main/java/com/ethran/notable/sync/FolderSyncService.kt @@ -37,7 +37,7 @@ class FolderSyncService @Inject constructor( localFolders.forEach { local -> val remote = folderMap[local.id] - if (remote == null || local.updatedAt.after(remote.updatedAt)) { + if (remote == null || (local.updatedAt.after(remote.updatedAt))) { folderMap[local.id] = local } } diff --git a/app/src/main/java/com/ethran/notable/sync/SyncSettings.kt b/app/src/main/java/com/ethran/notable/sync/SyncSettings.kt index 67c194fc..cc369c1e 100644 --- a/app/src/main/java/com/ethran/notable/sync/SyncSettings.kt +++ b/app/src/main/java/com/ethran/notable/sync/SyncSettings.kt @@ -16,5 +16,5 @@ data class SyncSettings( val syncOnNoteClose: Boolean = true, val wifiOnly: Boolean = false, val uploadOnly: Boolean = false, - val syncedNotebookIds: Set = emptySet() + val syncedNotebookIds: Set = emptySet(), ) diff --git a/app/src/main/java/com/ethran/notable/utils/AppResult.kt b/app/src/main/java/com/ethran/notable/utils/AppResult.kt index 9b6e48a3..bb68f179 100644 --- a/app/src/main/java/com/ethran/notable/utils/AppResult.kt +++ b/app/src/main/java/com/ethran/notable/utils/AppResult.kt @@ -30,7 +30,8 @@ interface DomainErrorInterface { sealed class DomainError( - override val userMessage: String, override val recoverable: Boolean = true + override val userMessage: String, + override val recoverable: Boolean = true, ) : DomainErrorInterface { data class NotFound(val resource: String) : DomainError("$resource was not found.") @@ -82,8 +83,8 @@ sealed class DomainError( * Usage: val combined = error1 + error2 */ operator fun DomainError.plus(other: DomainError): DomainError.MultipleErrors { - val leftList = if (this is DomainError.MultipleErrors) this.errors else listOf(this) - val rightList = if (other is DomainError.MultipleErrors) other.errors else listOf(other) + val leftList = (this as? DomainError.MultipleErrors)?.errors ?: listOf(this) + val rightList = (other as? DomainError.MultipleErrors)?.errors ?: listOf(other) return DomainError.MultipleErrors(leftList + rightList) } diff --git a/app/src/main/java/com/ethran/notable/utils/checkPermissions.kt b/app/src/main/java/com/ethran/notable/utils/checkPermissions.kt index ba2097a1..b3698e19 100644 --- a/app/src/main/java/com/ethran/notable/utils/checkPermissions.kt +++ b/app/src/main/java/com/ethran/notable/utils/checkPermissions.kt @@ -17,7 +17,7 @@ fun hasFilePermission(context: Context): Boolean { Environment.isExternalStorageManager() } else { ContextCompat.checkSelfPermission( - context, Manifest.permission.WRITE_EXTERNAL_STORAGE + context, Manifest.permission.WRITE_EXTERNAL_STORAGE, ) == PackageManager.PERMISSION_GRANTED } From 2056562d10d606fb0d873c128730db83de7fa251 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 29 Jun 2026 00:58:43 +0200 Subject: [PATCH 2/2] Update build configuration, improve localization with plurals, and refactor UI components for better performance. ### Build - **Gradle**: - Migrated `compose_version` from the root `build.gradle` to the Version Catalog (`libs.versions.toml`). - Fixed property access logic for `IS_NEXT` and updated `projectDir` references to use `project.projectDir`. - Moved Robolectric `jvmArgs` configuration to a global `tasks.withType(Test)` block. - Corrected dependency scopes for `androidx.junit.ktx` and `room.testing.android` from `implementation` to `androidTestImplementation`. ### UI & UX - **Switch**: Refactored thumb offset to use the lambda-based `Modifier.offset` with `IntOffset` to improve performance by avoiding recomposition during animations. - **SyncSettingsTab**: Updated the auto-sync label to support pluralized string resources. ### Localization - **Strings (English & Polish)**: Converted `sync_auto_sync_label` and `sync_clock_skew_warning` into `` to correctly handle singular and plural time units (minute/minutes, second/seconds). ### Core & Testing - **XoppFile**: Refactored bounding box initialization for clarity and updated KDoc references. - **EditorSelectionThreadSafetyTests**: - Updated timeouts and delays to use Kotlin `Duration` APIs (`5.seconds`, `16.milliseconds`). - Simplified flow collection logic using `first { ... }` predicates. - Improved reflection-based delegate access and added sequence-based filtering. - **FolderSyncService**: Minor code style and formatting improvements. --- app/build.gradle | 37 ++++++++++--------- .../EditorSelectionThreadSafetyTests.kt | 36 +++++++++--------- .../java/com/ethran/notable/io/XoppFile.kt | 11 +++++- .../ethran/notable/sync/FolderSyncService.kt | 4 +- .../ethran/notable/ui/components/Switch.kt | 3 +- .../notable/ui/views/SyncSettingsTab.kt | 6 ++- app/src/main/res/values-pl/strings.xml | 14 ++++++- app/src/main/res/values/strings.xml | 10 ++++- build.gradle | 3 -- gradle/libs.versions.toml | 1 + 10 files changed, 75 insertions(+), 50 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d0fbbd0b..d58024c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,7 +18,7 @@ android { versionCode 35 versionName '0.2.1' - if (project.hasProperty('IS_NEXT') && project.IS_NEXT.toBoolean()) { + if (project.hasProperty('IS_NEXT') && project.property('IS_NEXT').toBoolean()) { def timestamp = new Date().format('dd.MM.YYYY-HH:mm') versionName = "${versionName}-next-${timestamp}" } @@ -28,7 +28,7 @@ android { useSupportLibrary = true } ksp { - arg('room.schemaLocation', "$projectDir/schemas") + arg('room.schemaLocation', "${project.projectDir}/schemas") } } @@ -95,7 +95,7 @@ android { buildConfig = true } composeOptions { - kotlinCompilerExtensionVersion = rootProject.compose_version + kotlinCompilerExtensionVersion = libs.versions.compose.get() } packaging { @@ -108,7 +108,7 @@ android { } namespace = 'com.ethran.notable' sourceSets { - androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + androidTest.assets.srcDirs += files("${project.projectDir}/schemas") } testOptions { unitTests { @@ -117,22 +117,23 @@ android { // HiddenApiBypass needs deep reflection into java.lang on JDK 17+, which // requires explicit --add-opens flags. Without these, Robolectric tests // fail at class-init with ExceptionInInitializerError / NoSuchMethodException. - all { - jvmArgs += [ - '--add-opens=java.base/java.lang=ALL-UNNAMED', - '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED', - '--add-opens=java.base/java.io=ALL-UNNAMED', - '--add-opens=java.base/java.net=ALL-UNNAMED', - '--add-opens=java.base/java.nio=ALL-UNNAMED', - '--add-opens=java.base/java.util=ALL-UNNAMED', - '--add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED', - '--add-opens=java.base/sun.nio.ch=ALL-UNNAMED', - ] - } } } } +tasks.withType(Test).configureEach { + jvmArgs += [ + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + '--add-opens=java.base/java.net=ALL-UNNAMED', + '--add-opens=java.base/java.nio=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', + '--add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED', + '--add-opens=java.base/sun.nio.ch=ALL-UNNAMED', + ] +} + dependencies { implementation libs.androidx.core.ktx implementation libs.androidx.ui @@ -144,7 +145,7 @@ dependencies { implementation libs.androidx.fragment.ktx implementation libs.androidx.graphics.core implementation libs.androidx.input.motionprediction - implementation libs.androidx.junit.ktx + androidTestImplementation libs.androidx.junit.ktx //implementation fileTree(dir: 'libs', include: ['*.aar']) implementation(libs.onyxsdk.device) { @@ -191,7 +192,7 @@ dependencies { ksp libs.androidx.room.compiler // For testing: - implementation libs.androidx.room.testing.android + androidTestImplementation libs.androidx.room.testing.android testImplementation libs.androidx.room.testing testImplementation libs.androidx.core.testing diff --git a/app/src/androidTest/java/com/ethran/notable/editor/EditorSelectionThreadSafetyTests.kt b/app/src/androidTest/java/com/ethran/notable/editor/EditorSelectionThreadSafetyTests.kt index df290c25..b202072a 100644 --- a/app/src/androidTest/java/com/ethran/notable/editor/EditorSelectionThreadSafetyTests.kt +++ b/app/src/androidTest/java/com/ethran/notable/editor/EditorSelectionThreadSafetyTests.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout @@ -47,6 +46,8 @@ import org.junit.Test import org.junit.runner.RunWith import java.util.UUID import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds /** * Thread-safety tests for SelectionState. @@ -136,7 +137,7 @@ class EditorSelectionThreadSafetyTests { val watchedStates = selectionState.snapshotDelegateStatesForTest() val violations = ConcurrentLinkedQueue() val handle = Snapshot.registerGlobalWriteObserver { stateObject -> - if (stateObject in watchedStates && Thread.currentThread() != mainThread) { + if ((stateObject in watchedStates) && (Thread.currentThread() != mainThread)) { violations.add("write on ${Thread.currentThread().name}") } } @@ -217,19 +218,18 @@ class EditorSelectionThreadSafetyTests { } private suspend fun awaitToolbarPage(viewModel: EditorViewModel, expectedPageId: String) { - withTimeout(5_000) { + withTimeout(5.seconds) { viewModel.toolbarState - .filter { it.pageId == expectedPageId } - .first() + .first { it.pageId == expectedPageId } } } private suspend fun awaitSelectionReset(viewModel: EditorViewModel) { - withTimeout(5_000) { + withTimeout(5.seconds) { while (viewModel.selectionState.selectedStrokes != null || viewModel.selectionState.placementMode != null ) { - delay(16) + delay(16.milliseconds) } } } @@ -237,22 +237,22 @@ class EditorSelectionThreadSafetyTests { private fun SelectionState.snapshotDelegateStatesForTest(): Set { return setOf( - delegate("firstPageCut\$delegate"), - delegate("secondPageCut\$delegate"), - delegate("selectedStrokes\$delegate"), - delegate("selectedImages\$delegate"), - delegate("selectedBitmap\$delegate"), - delegate("selectionStartOffset\$delegate"), - delegate("selectionDisplaceOffset\$delegate"), - delegate("selectionRect\$delegate"), - delegate("placementMode\$delegate"), - ).filterNotNull().toSet() + delegate($$"firstPageCut$delegate"), + delegate($$"secondPageCut$delegate"), + delegate($$"selectedStrokes$delegate"), + delegate($$"selectedImages$delegate"), + delegate($$"selectedBitmap$delegate"), + delegate($$"selectionStartOffset$delegate"), + delegate($$"selectionDisplaceOffset$delegate"), + delegate($$"selectionRect$delegate"), + delegate($$"placementMode$delegate"), + ).asSequence().filterNotNull().toSet() } private fun SelectionState.delegate(fieldName: String): Any? { return try { val field = javaClass.getDeclaredField(fieldName).apply { isAccessible = true } - field.get(this) + field[this] } catch (e: Exception) { android.util.Log.e("EditorTest", "Could not find delegate field: $fieldName", e) null diff --git a/app/src/main/java/com/ethran/notable/io/XoppFile.kt b/app/src/main/java/com/ethran/notable/io/XoppFile.kt index f24606cd..a5c2d97d 100644 --- a/app/src/main/java/com/ethran/notable/io/XoppFile.kt +++ b/app/src/main/java/com/ethran/notable/io/XoppFile.kt @@ -49,7 +49,7 @@ import javax.inject.Inject private const val PRESSURE_FACTOR = 0.5f /** - * How many strokes are handed off to [importBook]'s onStrokeBatch callback before the next + * How many strokes are handed off to [XoppFile.importBook]'s onStrokeBatch callback before the next * batch starts. Keeping this bounded means peak memory during import is proportional to one * batch, not one full page. */ @@ -565,7 +565,14 @@ class XoppFile @Inject constructor( } points.add(StrokePoint(x, y, pressure, 0, 0)) - if (i == 0) boundingBox.set(x, y, x, y) else boundingBox.union(x, y) + if (i == 0) { + boundingBox.left = x + boundingBox.top = y + boundingBox.right = x + boundingBox.bottom = y + } else { + boundingBox.union(x, y) + } } boundingBox.inset(-strokeSize, -strokeSize) diff --git a/app/src/main/java/com/ethran/notable/sync/FolderSyncService.kt b/app/src/main/java/com/ethran/notable/sync/FolderSyncService.kt index cf51cb2c..50441bde 100644 --- a/app/src/main/java/com/ethran/notable/sync/FolderSyncService.kt +++ b/app/src/main/java/com/ethran/notable/sync/FolderSyncService.kt @@ -12,7 +12,7 @@ import javax.inject.Singleton @Singleton class FolderSyncService @Inject constructor( - private val appRepository: AppRepository + private val appRepository: AppRepository, ) { private val folderSerializer = FolderSerializer @@ -37,7 +37,7 @@ class FolderSyncService @Inject constructor( localFolders.forEach { local -> val remote = folderMap[local.id] - if (remote == null || (local.updatedAt.after(remote.updatedAt))) { + if ((remote == null) || (local.updatedAt.after(remote.updatedAt))) { folderMap[local.id] = local } } diff --git a/app/src/main/java/com/ethran/notable/ui/components/Switch.kt b/app/src/main/java/com/ethran/notable/ui/components/Switch.kt index 5e0e12f6..d17cebb6 100644 --- a/app/src/main/java/com/ethran/notable/ui/components/Switch.kt +++ b/app/src/main/java/com/ethran/notable/ui/components/Switch.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -133,7 +134,7 @@ fun OnOffSwitch( Surface( color = thumbColor, modifier = Modifier - .offset(x = thumbOffset) + .offset { IntOffset(x = thumbOffset.roundToPx(), y = 0) } .size(thumbSize) .padding(2.dp) ) {} diff --git a/app/src/main/java/com/ethran/notable/ui/views/SyncSettingsTab.kt b/app/src/main/java/com/ethran/notable/ui/views/SyncSettingsTab.kt index c1af6be7..812a0c8b 100644 --- a/app/src/main/java/com/ethran/notable/ui/views/SyncSettingsTab.kt +++ b/app/src/main/java/com/ethran/notable/ui/views/SyncSettingsTab.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -257,8 +258,9 @@ private fun SyncBehaviorSection( if (state.syncSettings.syncEnabled) { SettingToggleRow( - label = stringResource( - R.string.sync_auto_sync_label, + label = pluralStringResource( + R.plurals.sync_auto_sync_label, + state.syncSettings.syncInterval, state.syncSettings.syncInterval ), value = state.syncSettings.autoSync, diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 445883a6..2300fcf6 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -139,7 +139,12 @@ Włącz synchronizację WebDAV - Automatyczna synchronizacja co %1$d minut + + Automatyczna synchronizacja co minutę + Automatyczna synchronizacja co %1$d minuty + Automatyczna synchronizacja co %1$d minut + Automatyczna synchronizacja co %1$d minut + Synchronizuj przy zamykaniu notatek Synchronizuj tylko przez WiFi (bez danych mobilnych) Tylko wysyłaj (bez zmian z serwera) @@ -213,7 +218,12 @@ Potwierdź - Połączono pomyślnie, ale zegar urządzenia różni się od serwera o %1$d sekund. Synchronizacja może dawać nieprawidłowe wyniki do momentu poprawienia czasu. + + Połączono pomyślnie, ale zegar urządzenia różni się od serwera o %1$d sekundę. Synchronizacja może dawać nieprawidłowe wyniki do momentu poprawienia czasu. + Połączono pomyślnie, ale zegar urządzenia różni się od serwera o %1$d sekundy. Synchronizacja może dawać nieprawidłowe wyniki do momentu poprawienia czasu. + Połączono pomyślnie, ale zegar urządzenia różni się od serwera o %1$d sekund. Synchronizacja może dawać nieprawidłowe wyniki do momentu poprawienia czasu. + Połączono pomyślnie, ale zegar urządzenia różni się od serwera o %1$d sekund. Synchronizacja może dawać nieprawidłowe wyniki do momentu poprawienia czasu. + Synchronizacja wstrzymana: włączono synchronizację tylko przez WiFi, a obecnie korzystasz z danych mobilnych. Połącz się z WiFi lub wyłącz to ustawienie. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 062a6d38..6ad0d295 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -122,7 +122,10 @@ Sync WebDAV - Auto sync every %1$d minutes + + Auto sync every minute + Auto sync every %1$d minutes + Sync when closing notes Sync on WiFi only (no mobile data) Upload only (skip remote changes) @@ -196,7 +199,10 @@ Confirm - Connected successfully, but your device clock differs from the server by %1$d seconds. Sync may produce incorrect results until this is corrected. + + Connected successfully, but your device clock differs from the server by %1$d second. Sync may produce incorrect results until this is corrected. + Connected successfully, but your device clock differs from the server by %1$d seconds. Sync may produce incorrect results until this is corrected. + Not syncing: WiFi-only sync is enabled and you\'re on mobile data. Connect to WiFi or disable the WiFi-only setting. diff --git a/build.gradle b/build.gradle index b46045eb..ddd8c202 100644 --- a/build.gradle +++ b/build.gradle @@ -3,9 +3,6 @@ buildscript { google() mavenCentral() } - ext { - compose_version = '1.11.3' - } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias libs.plugins.android.application apply false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 650b9559..f17c0c18 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] activityCompose = "1.13.0" agp = "9.2.1" +compose = "1.11.3" coilCompose = "2.7.0" commonsCompress = "1.28.0" coreKtx = "1.19.0"