[iOS & Android] iOS 1.2.3 and Android 1.7.2
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m32s
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m32s
This commit is contained in:
@@ -16,8 +16,8 @@ android {
|
||||
applicationId = "com.atridad.openclimb"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 29
|
||||
versionName = "1.7.1"
|
||||
versionCode = 30
|
||||
versionName = "1.7.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -1,82 +1,105 @@
|
||||
package com.atridad.openclimb.data.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import android.content.Context
|
||||
import com.atridad.openclimb.data.database.dao.*
|
||||
import com.atridad.openclimb.data.model.*
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
Gym::class,
|
||||
Problem::class,
|
||||
ClimbSession::class,
|
||||
Attempt::class
|
||||
],
|
||||
version = 5,
|
||||
exportSchema = false
|
||||
entities = [Gym::class, Problem::class, ClimbSession::class, Attempt::class],
|
||||
version = 6,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class OpenClimbDatabase : RoomDatabase() {
|
||||
|
||||
|
||||
abstract fun gymDao(): GymDao
|
||||
abstract fun problemDao(): ProblemDao
|
||||
abstract fun climbSessionDao(): ClimbSessionDao
|
||||
abstract fun attemptDao(): AttemptDao
|
||||
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: OpenClimbDatabase? = null
|
||||
|
||||
val MIGRATION_4_5 = object : Migration(4, 5) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
val cursor = database.query("PRAGMA table_info(climb_sessions)")
|
||||
val existingColumns = mutableSetOf<String>()
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val columnName = cursor.getString(cursor.getColumnIndexOrThrow("name"))
|
||||
existingColumns.add(columnName)
|
||||
@Volatile private var INSTANCE: OpenClimbDatabase? = null
|
||||
|
||||
val MIGRATION_4_5 =
|
||||
object : Migration(4, 5) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
val cursor = database.query("PRAGMA table_info(climb_sessions)")
|
||||
val existingColumns = mutableSetOf<String>()
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val columnName = cursor.getString(cursor.getColumnIndexOrThrow("name"))
|
||||
existingColumns.add(columnName)
|
||||
}
|
||||
cursor.close()
|
||||
|
||||
if (!existingColumns.contains("startTime")) {
|
||||
database.execSQL("ALTER TABLE climb_sessions ADD COLUMN startTime TEXT")
|
||||
}
|
||||
if (!existingColumns.contains("endTime")) {
|
||||
database.execSQL("ALTER TABLE climb_sessions ADD COLUMN endTime TEXT")
|
||||
}
|
||||
if (!existingColumns.contains("status")) {
|
||||
database.execSQL(
|
||||
"ALTER TABLE climb_sessions ADD COLUMN status TEXT NOT NULL DEFAULT 'COMPLETED'"
|
||||
)
|
||||
}
|
||||
|
||||
database.execSQL(
|
||||
"UPDATE climb_sessions SET startTime = createdAt WHERE startTime IS NULL"
|
||||
)
|
||||
database.execSQL(
|
||||
"UPDATE climb_sessions SET status = 'COMPLETED' WHERE status IS NULL OR status = ''"
|
||||
)
|
||||
}
|
||||
}
|
||||
cursor.close()
|
||||
|
||||
if (!existingColumns.contains("startTime")) {
|
||||
database.execSQL("ALTER TABLE climb_sessions ADD COLUMN startTime TEXT")
|
||||
|
||||
val MIGRATION_5_6 =
|
||||
object : Migration(5, 6) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// Add updatedAt column to attempts table
|
||||
val cursor = database.query("PRAGMA table_info(attempts)")
|
||||
val existingColumns = mutableSetOf<String>()
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val columnName = cursor.getString(cursor.getColumnIndexOrThrow("name"))
|
||||
existingColumns.add(columnName)
|
||||
}
|
||||
cursor.close()
|
||||
|
||||
if (!existingColumns.contains("updatedAt")) {
|
||||
database.execSQL(
|
||||
"ALTER TABLE attempts ADD COLUMN updatedAt TEXT NOT NULL DEFAULT ''"
|
||||
)
|
||||
// Set updatedAt to createdAt for existing records
|
||||
database.execSQL(
|
||||
"UPDATE attempts SET updatedAt = createdAt WHERE updatedAt = ''"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!existingColumns.contains("endTime")) {
|
||||
database.execSQL("ALTER TABLE climb_sessions ADD COLUMN endTime TEXT")
|
||||
}
|
||||
if (!existingColumns.contains("status")) {
|
||||
database.execSQL("ALTER TABLE climb_sessions ADD COLUMN status TEXT NOT NULL DEFAULT 'COMPLETED'")
|
||||
}
|
||||
|
||||
database.execSQL("UPDATE climb_sessions SET startTime = createdAt WHERE startTime IS NULL")
|
||||
database.execSQL("UPDATE climb_sessions SET status = 'COMPLETED' WHERE status IS NULL OR status = ''")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_5_6 = object : Migration(5, 6) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getDatabase(context: Context): OpenClimbDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
OpenClimbDatabase::class.java,
|
||||
"openclimb_database"
|
||||
)
|
||||
.addMigrations(MIGRATION_4_5, MIGRATION_5_6)
|
||||
.enableMultiInstanceInvalidation()
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
return INSTANCE
|
||||
?: synchronized(this) {
|
||||
val instance =
|
||||
Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
OpenClimbDatabase::class.java,
|
||||
"openclimb_database"
|
||||
)
|
||||
.addMigrations(MIGRATION_4_5, MIGRATION_5_6)
|
||||
.enableMultiInstanceInvalidation()
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ data class BackupAttempt(
|
||||
val restTime: Long? = null,
|
||||
val timestamp: String,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
val updatedAt: String? = null
|
||||
) {
|
||||
companion object {
|
||||
/** Create BackupAttempt from native Android Attempt model */
|
||||
@@ -207,7 +207,8 @@ data class BackupAttempt(
|
||||
duration = attempt.duration,
|
||||
restTime = attempt.restTime,
|
||||
timestamp = attempt.timestamp,
|
||||
createdAt = attempt.createdAt
|
||||
createdAt = attempt.createdAt,
|
||||
updatedAt = attempt.updatedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -224,7 +225,8 @@ data class BackupAttempt(
|
||||
duration = duration,
|
||||
restTime = restTime,
|
||||
timestamp = timestamp,
|
||||
createdAt = createdAt
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt ?: createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ data class Attempt(
|
||||
val duration: Long? = null,
|
||||
val restTime: Long? = null,
|
||||
val timestamp: String,
|
||||
val createdAt: String
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
) {
|
||||
companion object {
|
||||
fun create(
|
||||
@@ -68,8 +69,31 @@ data class Attempt(
|
||||
duration = duration,
|
||||
restTime = restTime,
|
||||
timestamp = timestamp,
|
||||
createdAt = now
|
||||
createdAt = now,
|
||||
updatedAt = now
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updated(
|
||||
result: AttemptResult? = null,
|
||||
highestHold: String? = null,
|
||||
notes: String? = null,
|
||||
duration: Long? = null,
|
||||
restTime: Long? = null
|
||||
): Attempt {
|
||||
return Attempt(
|
||||
id = this.id,
|
||||
sessionId = this.sessionId,
|
||||
problemId = this.problemId,
|
||||
result = result ?: this.result,
|
||||
highestHold = highestHold ?: this.highestHold,
|
||||
notes = notes ?: this.notes,
|
||||
duration = duration ?: this.duration,
|
||||
restTime = restTime ?: this.restTime,
|
||||
timestamp = this.timestamp,
|
||||
createdAt = this.createdAt,
|
||||
updatedAt = DateFormatUtils.nowISO8601()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@ class SyncService(private val context: Context, private val repository: ClimbRep
|
||||
private val _isConnected = MutableStateFlow(false)
|
||||
val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
|
||||
|
||||
private val _isConfigured = MutableStateFlow(false)
|
||||
val isConfiguredFlow: StateFlow<Boolean> = _isConfigured.asStateFlow()
|
||||
|
||||
private val _isTesting = MutableStateFlow(false)
|
||||
val isTesting: StateFlow<Boolean> = _isTesting.asStateFlow()
|
||||
|
||||
@@ -91,17 +94,29 @@ class SyncService(private val context: Context, private val repository: ClimbRep
|
||||
get() = sharedPreferences.getString(Keys.SERVER_URL, "") ?: ""
|
||||
set(value) {
|
||||
sharedPreferences.edit { putString(Keys.SERVER_URL, value) }
|
||||
updateConfiguredState()
|
||||
// Clear connection status when configuration changes
|
||||
_isConnected.value = false
|
||||
sharedPreferences.edit().putBoolean(Keys.IS_CONNECTED, false).apply()
|
||||
}
|
||||
|
||||
var authToken: String
|
||||
get() = sharedPreferences.getString(Keys.AUTH_TOKEN, "") ?: ""
|
||||
set(value) {
|
||||
sharedPreferences.edit { putString(Keys.AUTH_TOKEN, value) }
|
||||
updateConfiguredState()
|
||||
// Clear connection status when configuration changes
|
||||
_isConnected.value = false
|
||||
sharedPreferences.edit().putBoolean(Keys.IS_CONNECTED, false).apply()
|
||||
}
|
||||
|
||||
val isConfigured: Boolean
|
||||
get() = serverURL.isNotEmpty() && authToken.isNotEmpty()
|
||||
|
||||
private fun updateConfiguredState() {
|
||||
_isConfigured.value = serverURL.isNotEmpty() && authToken.isNotEmpty()
|
||||
}
|
||||
|
||||
var isAutoSyncEnabled: Boolean
|
||||
get() = sharedPreferences.getBoolean(Keys.AUTO_SYNC_ENABLED, true)
|
||||
set(value) {
|
||||
@@ -111,6 +126,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep
|
||||
init {
|
||||
_isConnected.value = sharedPreferences.getBoolean(Keys.IS_CONNECTED, false)
|
||||
_lastSyncTime.value = sharedPreferences.getString(Keys.LAST_SYNC_TIME, null)
|
||||
updateConfiguredState()
|
||||
|
||||
repository.setAutoSyncCallback {
|
||||
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { triggerAutoSync() }
|
||||
@@ -783,6 +799,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep
|
||||
_syncError.value = null
|
||||
|
||||
sharedPreferences.edit().clear().apply()
|
||||
updateConfiguredState()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,11 +205,7 @@ fun OpenClimbApp(
|
||||
) {
|
||||
showNotificationPermissionDialog = true
|
||||
} else {
|
||||
if (gyms.size == 1) {
|
||||
viewModel.startSession(context, gyms.first().id)
|
||||
} else {
|
||||
navController.navigate(Screen.AddEditSession())
|
||||
}
|
||||
navController.navigate(Screen.AddEditSession())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -732,6 +732,9 @@ fun AddEditSessionScreen(
|
||||
LaunchedEffect(gymId, gyms) {
|
||||
if (gymId != null && selectedGym == null) {
|
||||
selectedGym = gyms.find { it.id == gymId }
|
||||
} else if (gymId == null && selectedGym == null && gyms.size == 1) {
|
||||
// Auto-select the gym if there's only one available
|
||||
selectedGym = gyms.first()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ fun SettingsScreen(viewModel: ClimbViewModel) {
|
||||
val syncService = viewModel.syncService
|
||||
val isSyncing by syncService.isSyncing.collectAsState()
|
||||
val isConnected by syncService.isConnected.collectAsState()
|
||||
val isConfigured by syncService.isConfiguredFlow.collectAsState()
|
||||
val isTesting by syncService.isTesting.collectAsState()
|
||||
val lastSyncTime by syncService.lastSyncTime.collectAsState()
|
||||
val syncError by syncService.syncError.collectAsState()
|
||||
@@ -49,6 +50,12 @@ fun SettingsScreen(viewModel: ClimbViewModel) {
|
||||
val packageInfo = remember { context.packageManager.getPackageInfo(context.packageName, 0) }
|
||||
val appVersion = packageInfo.versionName
|
||||
|
||||
// Update local state when sync service configuration changes
|
||||
LaunchedEffect(syncService.serverURL, syncService.authToken) {
|
||||
serverUrl = syncService.serverURL
|
||||
authToken = syncService.authToken
|
||||
}
|
||||
|
||||
// File picker launcher for import - only accepts ZIP files
|
||||
val importLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { uri
|
||||
@@ -142,7 +149,7 @@ fun SettingsScreen(viewModel: ClimbViewModel) {
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
if (syncService.isConfigured) {
|
||||
if (isConfigured) {
|
||||
// Connected state
|
||||
Card(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
@@ -730,7 +737,60 @@ fun SettingsScreen(viewModel: ClimbViewModel) {
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (syncService.isConfigured) {
|
||||
// Connection status indicator
|
||||
if (isTesting) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Testing connection...",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else if (isConnected && isConfigured) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Connection successful",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else if (syncError != null && isConfigured) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Connection failed: $syncError",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
} else if (isConfigured) {
|
||||
Text(
|
||||
text = "Test connection before enabling sync features",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -747,18 +807,66 @@ fun SettingsScreen(viewModel: ClimbViewModel) {
|
||||
},
|
||||
confirmButton = {
|
||||
Row {
|
||||
if (syncService.isConfigured) {
|
||||
// Primary action: Save & Test
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
syncService.serverURL = serverUrl.trim()
|
||||
syncService.authToken = authToken.trim()
|
||||
viewModel.testSyncConnection()
|
||||
while (syncService.isTesting.value) {
|
||||
kotlinx.coroutines.delay(100)
|
||||
}
|
||||
showSyncConfigDialog = false
|
||||
} catch (e: Exception) {
|
||||
viewModel.setError(
|
||||
"Failed to save and test: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled =
|
||||
!isTesting &&
|
||||
serverUrl.isNotBlank() &&
|
||||
authToken.isNotBlank()
|
||||
) {
|
||||
if (isTesting) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Save & Test")
|
||||
}
|
||||
} else {
|
||||
Text("Save & Test")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
// Secondary action: Test only (when already configured)
|
||||
if (isConfigured) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
// Save configuration first
|
||||
syncService.serverURL = serverUrl.trim()
|
||||
syncService.authToken = authToken.trim()
|
||||
viewModel.testSyncConnection()
|
||||
showSyncConfigDialog = false
|
||||
} catch (_: Exception) {
|
||||
// Error will be shown via syncError state
|
||||
while (syncService.isTesting.value) {
|
||||
kotlinx.coroutines.delay(100)
|
||||
}
|
||||
if (isConnected) {
|
||||
showSyncConfigDialog = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
viewModel.setError(
|
||||
"Connection test failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -766,34 +874,13 @@ fun SettingsScreen(viewModel: ClimbViewModel) {
|
||||
!isTesting &&
|
||||
serverUrl.isNotBlank() &&
|
||||
authToken.isNotBlank()
|
||||
) {
|
||||
if (isTesting) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text("Test Connection")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
) { Text("Test Only") }
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
syncService.serverURL = serverUrl.trim()
|
||||
syncService.authToken = authToken.trim()
|
||||
showSyncConfigDialog = false
|
||||
},
|
||||
enabled = serverUrl.isNotBlank() && authToken.isNotBlank()
|
||||
) { Text("Save") }
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
// Reset to current values
|
||||
serverUrl = syncService.serverURL
|
||||
authToken = syncService.authToken
|
||||
showSyncConfigDialog = false
|
||||
|
||||
@@ -489,7 +489,8 @@ class BusinessLogicTests {
|
||||
duration = attempt.duration,
|
||||
restTime = attempt.restTime,
|
||||
timestamp = attempt.timestamp,
|
||||
createdAt = attempt.createdAt
|
||||
createdAt = attempt.createdAt,
|
||||
updatedAt = attempt.updatedAt,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -230,7 +230,8 @@ class DataModelTests {
|
||||
duration = 300,
|
||||
restTime = 120,
|
||||
timestamp = "2024-01-01T10:30:00Z",
|
||||
createdAt = "2024-01-01T10:30:00Z"
|
||||
createdAt = "2024-01-01T10:30:00Z",
|
||||
updatedAt = "2024-01-01T10:30:00Z"
|
||||
)
|
||||
|
||||
assertEquals("attempt123", attempt.id)
|
||||
@@ -555,7 +556,8 @@ class DataModelTests {
|
||||
duration = 120,
|
||||
restTime = null,
|
||||
timestamp = "2024-01-01T10:30:00Z",
|
||||
createdAt = "2024-01-01T10:30:00Z"
|
||||
createdAt = "2024-01-01T10:30:00Z",
|
||||
updatedAt = "2024-01-01T10:30:00Z"
|
||||
)
|
||||
|
||||
// Verify referential integrity
|
||||
|
||||
@@ -73,7 +73,8 @@ class SyncMergeLogicTest {
|
||||
duration = 300,
|
||||
restTime = null,
|
||||
timestamp = "2024-01-01T10:30:00",
|
||||
createdAt = "2024-01-01T10:30:00"
|
||||
createdAt = "2024-01-01T10:30:00",
|
||||
updatedAt = "2024-01-01T10:30:00"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -186,7 +187,8 @@ class SyncMergeLogicTest {
|
||||
duration = 180,
|
||||
restTime = 60,
|
||||
timestamp = "2024-01-02T14:30:00",
|
||||
createdAt = "2024-01-02T14:30:00"
|
||||
createdAt = "2024-01-02T14:30:00",
|
||||
updatedAt = "2024-01-02T14:30:00"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -428,7 +430,11 @@ class SyncMergeLogicTest {
|
||||
// Add server attempts, preferring newer updates
|
||||
server.forEach { serverAttempt ->
|
||||
val localAttempt = merged[serverAttempt.id]
|
||||
if (localAttempt == null || isNewerThan(serverAttempt.createdAt, localAttempt.createdAt)
|
||||
if (localAttempt == null ||
|
||||
isNewerThan(
|
||||
serverAttempt.updatedAt ?: serverAttempt.createdAt,
|
||||
localAttempt.updatedAt ?: localAttempt.createdAt
|
||||
)
|
||||
) {
|
||||
merged[serverAttempt.id] = serverAttempt
|
||||
}
|
||||
|
||||
@@ -18,15 +18,16 @@ viewmodel = "2.9.4"
|
||||
kotlinxSerialization = "1.9.0"
|
||||
kotlinxCoroutines = "1.10.2"
|
||||
coil = "2.7.0"
|
||||
ksp = "2.2.10-2.0.2"
|
||||
okhttp = "5.1.0"
|
||||
ksp = "2.2.20-2.0.3"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
#noinspection SimilarGradleDependency
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" }
|
||||
#noinspection SimilarGradleDependency
|
||||
androidx-test-ext = { group = "androidx.test.ext", name = "junit", version.ref = "androidxTestExt" }
|
||||
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" }
|
||||
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" }
|
||||
|
||||
Reference in New Issue
Block a user