From 30d2b3938ee428c9a7d46ed200742860a3ec049e Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Sun, 12 Oct 2025 20:41:39 -0600 Subject: [PATCH] [Android] 1.9.2 --- android/README.md | 22 + android/app/build.gradle.kts | 4 +- android/app/src/main/AndroidManifest.xml | 1 + .../data/migration/ImageMigrationService.kt | 205 --- .../atridad/openclimb/data/model/Attempt.kt | 21 - .../openclimb/data/model/DifficultySystem.kt | 2 +- .../data/repository/ClimbRepository.kt | 4 +- .../openclimb/data/state/DataStateManager.kt | 24 +- .../openclimb/data/sync/SyncService.kt | 1343 ++++------------- .../service/SessionTrackingService.kt | 8 +- .../openclimb/ui/screens/ProblemsScreen.kt | 80 +- .../openclimb/ui/screens/SettingsScreen.kt | 73 +- .../openclimb/ui/viewmodel/ClimbViewModel.kt | 58 - ios/OpenClimb.xcodeproj/project.pbxproj | 8 +- .../UserInterfaceState.xcuserstate | Bin 161931 -> 166689 bytes ios/OpenClimb/Components/AsyncImageView.swift | 39 + ios/OpenClimb/Services/SyncService.swift | 118 +- ios/OpenClimb/Utils/ImageManager.swift | 115 +- .../Utils/OrientationAwareImage.swift | 11 +- ios/OpenClimb/Views/ProblemsView.swift | 99 +- ios/OpenClimb/Views/SettingsView.swift | 46 +- ios/README.md | 23 + sync/README.md | 37 + 23 files changed, 620 insertions(+), 1721 deletions(-) create mode 100644 android/README.md delete mode 100644 android/app/src/main/java/com/atridad/openclimb/data/migration/ImageMigrationService.kt create mode 100644 ios/OpenClimb/Components/AsyncImageView.swift create mode 100644 ios/README.md create mode 100644 sync/README.md diff --git a/android/README.md b/android/README.md new file mode 100644 index 0000000..1944898 --- /dev/null +++ b/android/README.md @@ -0,0 +1,22 @@ +# OpenClimb for Android + +This is the native Android app for OpenClimb, built with Kotlin and Jetpack Compose. + +## Project Structure + +This is a standard Android Gradle project. The main code lives in `app/src/main/java/com/atridad/openclimb/`. + +- `data/`: Handles all the app's data. + - `database/`: Room database setup (DAOs, entities). + - `model/`: Core data models (`Problem`, `Gym`, `ClimbSession`). + - `repository/`: Manages the data, providing a clean API for the rest of the app. + - `sync/`: Handles talking to the sync server. +- `ui/`: All the Jetpack Compose UI code. + - `screens/`: The main screens of the app. + - `components/`: Reusable UI bits used across screens. + - `viewmodel/`: `ClimbViewModel` for managing UI state. +- `navigation/`: Navigation graph and routes using Jetpack Navigation. +- `service/`: Background service for tracking climbing sessions. +- `utils/`: Helpers for things like date formatting and image handling. + +The app is built to be offline-first. All data is stored locally on your device and works without an internet connection. \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 8dd927e..acecb41 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -16,8 +16,8 @@ android { applicationId = "com.atridad.openclimb" minSdk = 31 targetSdk = 36 - versionCode = 38 - versionName = "1.9.1" + versionCode = 39 + versionName = "1.9.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d5e5d51..52b0e91 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + diff --git a/android/app/src/main/java/com/atridad/openclimb/data/migration/ImageMigrationService.kt b/android/app/src/main/java/com/atridad/openclimb/data/migration/ImageMigrationService.kt deleted file mode 100644 index 6b0d619..0000000 --- a/android/app/src/main/java/com/atridad/openclimb/data/migration/ImageMigrationService.kt +++ /dev/null @@ -1,205 +0,0 @@ -package com.atridad.openclimb.data.migration - -import android.content.Context -import android.util.Log -import com.atridad.openclimb.data.repository.ClimbRepository -import com.atridad.openclimb.utils.ImageNamingUtils -import com.atridad.openclimb.utils.ImageUtils -import kotlinx.coroutines.flow.first - -/** - * Service responsible for migrating images to use consistent naming convention across platforms. - * This ensures that iOS and Android use the same image filenames for sync compatibility. - */ -class ImageMigrationService(private val context: Context, private val repository: ClimbRepository) { - companion object { - private const val TAG = "ImageMigrationService" - private const val MIGRATION_PREF_KEY = "image_naming_migration_completed" - } - - /** - * Performs a complete migration of all images in the system to use consistent naming. This - * should be called once during app startup after the naming convention is implemented. - */ - suspend fun performFullMigration(): ImageMigrationResult { - Log.i(TAG, "Starting full image naming migration") - - val prefs = context.getSharedPreferences("openclimb_migration", Context.MODE_PRIVATE) - if (prefs.getBoolean(MIGRATION_PREF_KEY, false)) { - Log.i(TAG, "Image migration already completed, skipping") - return ImageMigrationResult.AlreadyCompleted - } - - try { - val allProblems = repository.getAllProblems().first() - val migrationResults = mutableMapOf() - var migratedCount = 0 - var errorCount = 0 - - Log.i(TAG, "Found ${allProblems.size} problems to check for image migration") - - for (problem in allProblems) { - if (problem.imagePaths.isNotEmpty()) { - Log.d( - TAG, - "Migrating images for problem '${problem.name}': ${problem.imagePaths}" - ) - - try { - val problemMigrations = - ImageUtils.migrateImageNaming( - context = context, - problemId = problem.id, - currentImagePaths = problem.imagePaths - ) - - if (problemMigrations.isNotEmpty()) { - migrationResults.putAll(problemMigrations) - migratedCount += problemMigrations.size - - // Update image paths - val newImagePaths = - problem.imagePaths.map { oldPath -> - problemMigrations[oldPath] ?: oldPath - } - - val updatedProblem = problem.copy(imagePaths = newImagePaths) - repository.insertProblem(updatedProblem) - - Log.d( - TAG, - "Updated problem '${problem.name}' with ${problemMigrations.size} migrated images" - ) - } - } catch (e: Exception) { - Log.e( - TAG, - "Failed to migrate images for problem '${problem.name}': ${e.message}", - e - ) - errorCount++ - } - } - } - - // Mark migration as completed - prefs.edit().putBoolean(MIGRATION_PREF_KEY, true).apply() - - Log.i( - TAG, - "Image migration completed: $migratedCount images migrated, $errorCount errors" - ) - - return ImageMigrationResult.Success( - totalMigrated = migratedCount, - errors = errorCount, - migrations = migrationResults - ) - } catch (e: Exception) { - Log.e(TAG, "Image migration failed: ${e.message}", e) - return ImageMigrationResult.Failed(e.message ?: "Unknown error") - } - } - - /** Validates that all images in the system follow the consistent naming convention. */ - suspend fun validateImageNaming(): ValidationResult { - try { - val allProblems = repository.getAllProblems().first() - val validImages = mutableListOf() - val invalidImages = mutableListOf() - val missingImages = mutableListOf() - - for (problem in allProblems) { - for (imagePath in problem.imagePaths) { - val filename = imagePath.substringAfterLast('/') - - // Check if file exists - val imageFile = ImageUtils.getImageFile(context, imagePath) - if (!imageFile.exists()) { - missingImages.add(imagePath) - continue - } - - // Check if filename follows convention - if (ImageNamingUtils.isValidImageFilename(filename)) { - validImages.add(imagePath) - } else { - invalidImages.add(imagePath) - } - } - } - - return ValidationResult( - totalImages = validImages.size + invalidImages.size + missingImages.size, - validImages = validImages, - invalidImages = invalidImages, - missingImages = missingImages - ) - } catch (e: Exception) { - Log.e(TAG, "Image validation failed: ${e.message}", e) - return ValidationResult( - totalImages = 0, - validImages = emptyList(), - invalidImages = emptyList(), - missingImages = emptyList() - ) - } - } - - /** Migrates images for a specific problem during sync operations. */ - suspend fun migrateProblemImages( - problemId: String, - currentImagePaths: List - ): Map { - return try { - ImageUtils.migrateImageNaming(context, problemId, currentImagePaths) - } catch (e: Exception) { - Log.e(TAG, "Failed to migrate images for problem $problemId: ${e.message}", e) - emptyMap() - } - } - - /** - * Cleans up any orphaned image files that don't follow our naming convention and aren't - * referenced by any problems. - */ - suspend fun cleanupOrphanedImages() { - try { - val allProblems = repository.getAllProblems().first() - val referencedPaths = allProblems.flatMap { it.imagePaths }.toSet() - - ImageUtils.cleanupOrphanedImages(context, referencedPaths) - - Log.i(TAG, "Orphaned image cleanup completed") - } catch (e: Exception) { - Log.e(TAG, "Failed to cleanup orphaned images: ${e.message}", e) - } - } -} - -/** Result of an image migration operation */ -sealed class ImageMigrationResult { - object AlreadyCompleted : ImageMigrationResult() - - data class Success( - val totalMigrated: Int, - val errors: Int, - val migrations: Map - ) : ImageMigrationResult() - - data class Failed(val error: String) : ImageMigrationResult() -} - -/** Result of image naming validation */ -data class ValidationResult( - val totalImages: Int, - val validImages: List, - val invalidImages: List, - val missingImages: List -) { - val isAllValid: Boolean - get() = invalidImages.isEmpty() && missingImages.isEmpty() - - val validPercentage: Double - get() = if (totalImages == 0) 100.0 else (validImages.size.toDouble() / totalImages) * 100 -} diff --git a/android/app/src/main/java/com/atridad/openclimb/data/model/Attempt.kt b/android/app/src/main/java/com/atridad/openclimb/data/model/Attempt.kt index 2ebda6d..92d4e57 100644 --- a/android/app/src/main/java/com/atridad/openclimb/data/model/Attempt.kt +++ b/android/app/src/main/java/com/atridad/openclimb/data/model/Attempt.kt @@ -75,25 +75,4 @@ data class Attempt( } } - 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() - ) - } } diff --git a/android/app/src/main/java/com/atridad/openclimb/data/model/DifficultySystem.kt b/android/app/src/main/java/com/atridad/openclimb/data/model/DifficultySystem.kt index 33c28aa..39da548 100644 --- a/android/app/src/main/java/com/atridad/openclimb/data/model/DifficultySystem.kt +++ b/android/app/src/main/java/com/atridad/openclimb/data/model/DifficultySystem.kt @@ -207,7 +207,7 @@ data class DifficultyGrade(val system: DifficultySystem, val grade: String, val private fun compareVScaleGrades(grade1: String, grade2: String): Int { if (grade1 == "VB" && grade2 != "VB") return -1 if (grade2 == "VB" && grade1 != "VB") return 1 - if (grade1 == "VB" && grade2 == "VB") return 0 + if (grade1 == "VB") return 0 val num1 = grade1.removePrefix("V").toIntOrNull() ?: 0 val num2 = grade2.removePrefix("V").toIntOrNull() ?: 0 diff --git a/android/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt b/android/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt index a83318c..f028bae 100644 --- a/android/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt +++ b/android/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt @@ -17,8 +17,6 @@ import com.atridad.openclimb.utils.ZipExportImportUtils import java.io.File import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json class ClimbRepository(database: OpenClimbDatabase, private val context: Context) { @@ -288,7 +286,7 @@ class ClimbRepository(database: OpenClimbDatabase, private val context: Context) try { val deletion = json.decodeFromString(value) deletions.add(deletion) - } catch (e: Exception) { + } catch (_: Exception) { // Invalid deletion record, ignore } } diff --git a/android/app/src/main/java/com/atridad/openclimb/data/state/DataStateManager.kt b/android/app/src/main/java/com/atridad/openclimb/data/state/DataStateManager.kt index 01ad1db..85064c4 100644 --- a/android/app/src/main/java/com/atridad/openclimb/data/state/DataStateManager.kt +++ b/android/app/src/main/java/com/atridad/openclimb/data/state/DataStateManager.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.SharedPreferences import android.util.Log import com.atridad.openclimb.utils.DateFormatUtils +import androidx.core.content.edit /** * Manages the overall data state timestamp for sync purposes. This tracks when any data in the @@ -35,7 +36,7 @@ class DataStateManager(context: Context) { */ fun updateDataState() { val now = DateFormatUtils.nowISO8601() - prefs.edit().putString(KEY_LAST_MODIFIED, now).apply() + prefs.edit { putString(KEY_LAST_MODIFIED, now) } Log.d(TAG, "Data state updated to: $now") } @@ -48,21 +49,6 @@ class DataStateManager(context: Context) { ?: DateFormatUtils.nowISO8601() } - /** - * Sets the data state timestamp to a specific value. Used when importing data from server to - * sync the state. - */ - fun setLastModified(timestamp: String) { - prefs.edit().putString(KEY_LAST_MODIFIED, timestamp).apply() - Log.d(TAG, "Data state set to: $timestamp") - } - - /** Resets the data state (for testing or complete data wipe). */ - fun reset() { - prefs.edit().clear().apply() - Log.d(TAG, "Data state reset") - } - /** Checks if the data state has been initialized. */ private fun isInitialized(): Boolean { return prefs.getBoolean(KEY_INITIALIZED, false) @@ -70,11 +56,7 @@ class DataStateManager(context: Context) { /** Marks the data state as initialized. */ private fun markAsInitialized() { - prefs.edit().putBoolean(KEY_INITIALIZED, true).apply() + prefs.edit { putBoolean(KEY_INITIALIZED, true) } } - /** Gets debug information about the current state. */ - fun getDebugInfo(): String { - return "DataState(lastModified=${getLastModified()}, initialized=${isInitialized()})" - } } diff --git a/android/app/src/main/java/com/atridad/openclimb/data/sync/SyncService.kt b/android/app/src/main/java/com/atridad/openclimb/data/sync/SyncService.kt index 9176b5f..9aac55b 100644 --- a/android/app/src/main/java/com/atridad/openclimb/data/sync/SyncService.kt +++ b/android/app/src/main/java/com/atridad/openclimb/data/sync/SyncService.kt @@ -2,30 +2,28 @@ package com.atridad.openclimb.data.sync import android.content.Context import android.content.SharedPreferences +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.util.Log +import androidx.annotation.RequiresPermission import androidx.core.content.edit import com.atridad.openclimb.data.format.BackupAttempt import com.atridad.openclimb.data.format.BackupClimbSession import com.atridad.openclimb.data.format.BackupGym import com.atridad.openclimb.data.format.BackupProblem import com.atridad.openclimb.data.format.ClimbDataBackup -import com.atridad.openclimb.data.format.DeletedItem -import com.atridad.openclimb.data.migration.ImageMigrationService -import com.atridad.openclimb.data.model.Attempt -import com.atridad.openclimb.data.model.ClimbSession -import com.atridad.openclimb.data.model.Gym -import com.atridad.openclimb.data.model.Problem -import com.atridad.openclimb.data.model.SessionStatus import com.atridad.openclimb.data.repository.ClimbRepository import com.atridad.openclimb.data.state.DataStateManager import com.atridad.openclimb.utils.DateFormatUtils import com.atridad.openclimb.utils.ImageNamingUtils import com.atridad.openclimb.utils.ImageUtils import java.io.IOException -import java.time.Instant +import java.io.Serializable import java.util.concurrent.TimeUnit +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -43,9 +41,9 @@ import okhttp3.RequestBody.Companion.toRequestBody class SyncService(private val context: Context, private val repository: ClimbRepository) { - private val migrationService = ImageMigrationService(context, repository) private val dataStateManager = DataStateManager(context) private val syncMutex = Mutex() + private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) companion object { private const val TAG = "SyncService" @@ -65,8 +63,6 @@ class SyncService(private val context: Context, private val repository: ClimbRep prettyPrint = true ignoreUnknownKeys = true explicitNulls = false - encodeDefaults = true - coerceInputValues = true } // State @@ -91,6 +87,8 @@ class SyncService(private val context: Context, private val repository: ClimbRep private val _isAutoSyncEnabled = MutableStateFlow(true) val isAutoSyncEnabled: StateFlow = _isAutoSyncEnabled.asStateFlow() + private var isOfflineMode = false + // Debounced sync properties private var syncJob: Job? = null private var pendingChanges = false @@ -98,22 +96,49 @@ class SyncService(private val context: Context, private val repository: ClimbRep // Configuration keys private object Keys { - const val SERVER_URL = "sync_server_url" - const val AUTH_TOKEN = "sync_auth_token" + const val SERVER_URL = "server_url" + const val AUTH_TOKEN = "auth_token" + const val IS_CONNECTED = "is_connected" const val LAST_SYNC_TIME = "last_sync_time" - const val IS_CONNECTED = "sync_is_connected" const val AUTO_SYNC_ENABLED = "auto_sync_enabled" + const val OFFLINE_MODE = "offline_mode" } - // Configuration properties - var serverURL: String + init { + loadInitialState() + updateConfiguredState() + repository.setAutoSyncCallback { serviceScope.launch { triggerAutoSync() } } + } + + private fun loadInitialState() { + _lastSyncTime.value = sharedPreferences.getString(Keys.LAST_SYNC_TIME, null) + _isConnected.value = sharedPreferences.getBoolean(Keys.IS_CONNECTED, false) + _isAutoSyncEnabled.value = sharedPreferences.getBoolean(Keys.AUTO_SYNC_ENABLED, true) + isOfflineMode = sharedPreferences.getBoolean(Keys.OFFLINE_MODE, false) + } + + private fun updateConfiguredState() { + _isConfigured.value = serverUrl.isNotBlank() && authToken.isNotBlank() + } + + var serverUrl: String 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() + sharedPreferences.edit { putBoolean(Keys.IS_CONNECTED, false) } + } + + // Legacy accessor expected by some UI code (kept for compatibility) + @Deprecated( + message = "Use serverUrl (kebab case) instead", + replaceWith = ReplaceWith("serverUrl") + ) + var serverURL: String + get() = serverUrl + set(value) { + serverUrl = value } var authToken: String @@ -121,237 +146,42 @@ class SyncService(private val context: Context, private val repository: ClimbRep 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) } } - val isConfigured: Boolean - get() = serverURL.isNotEmpty() && authToken.isNotEmpty() - - private fun updateConfiguredState() { - _isConfigured.value = serverURL.isNotEmpty() && authToken.isNotEmpty() - } - fun setAutoSyncEnabled(enabled: Boolean) { - sharedPreferences.edit().putBoolean(Keys.AUTO_SYNC_ENABLED, enabled).apply() _isAutoSyncEnabled.value = enabled + sharedPreferences.edit { putBoolean(Keys.AUTO_SYNC_ENABLED, enabled) } } - init { - _isConnected.value = sharedPreferences.getBoolean(Keys.IS_CONNECTED, false) - _lastSyncTime.value = sharedPreferences.getString(Keys.LAST_SYNC_TIME, null) - _isAutoSyncEnabled.value = sharedPreferences.getBoolean(Keys.AUTO_SYNC_ENABLED, true) - updateConfiguredState() - - repository.setAutoSyncCallback { - kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { triggerAutoSync() } + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + private fun isNetworkAvailable(): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val network = connectivityManager.activeNetwork ?: return false + val activeNetwork = connectivityManager.getNetworkCapabilities(network) ?: return false + return when { + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + else -> false } - - // Perform image naming migration on initialization - kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { performImageNamingMigration() } } - suspend fun downloadData(): ClimbDataBackup = - withContext(Dispatchers.IO) { - if (!isConfigured) { - throw SyncException.NotConfigured - } - - val request = - Request.Builder() - .url("$serverURL/sync") - .get() - .addHeader("Authorization", "Bearer $authToken") - .addHeader("Accept", "application/json") - .build() - - try { - val response = httpClient.newCall(request).execute() - - when (response.code) { - 200 -> { - val responseBody = - response.body?.string() - ?: throw SyncException.InvalidResponse( - "Empty response body" - ) - Log.d(TAG, "Downloaded data from server: ${responseBody.take(500)}...") - try { - val backup = json.decodeFromString(responseBody) - Log.d( - TAG, - "Server backup contains: gyms=${backup.gyms.size}, problems=${backup.problems.size}, sessions=${backup.sessions.size}, attempts=${backup.attempts.size}" - ) - - backup.problems.forEach { problem -> - val imageCount = problem.imagePaths?.size ?: 0 - if (imageCount > 0) { - Log.d( - TAG, - "Server problem '${problem.name}' has images: ${problem.imagePaths}" - ) - } - } - - backup - } catch (e: Exception) { - Log.e(TAG, "Failed to decode download response: ${e.message}") - throw SyncException.DecodingError( - e.message ?: "Failed to decode response" - ) - } - } - 401 -> throw SyncException.Unauthorized - else -> throw SyncException.ServerError(response.code) - } - } catch (e: IOException) { - throw SyncException.NetworkError(e.message ?: "Network error") - } - } - - suspend fun uploadData(backup: ClimbDataBackup): ClimbDataBackup = - withContext(Dispatchers.IO) { - if (!isConfigured) { - throw SyncException.NotConfigured - } - - val jsonBody = json.encodeToString(backup) - Log.d(TAG, "Uploading JSON to server: $jsonBody") - val requestBody = jsonBody.toRequestBody("application/json".toMediaType()) - - val request = - Request.Builder() - .url("$serverURL/sync") - .put(requestBody) - .addHeader("Authorization", "Bearer $authToken") - .addHeader("Content-Type", "application/json") - .build() - - try { - val response = httpClient.newCall(request).execute() - Log.d(TAG, "Upload response code: ${response.code}") - - when (response.code) { - 200 -> { - val responseBody = - response.body?.string() - ?: throw SyncException.InvalidResponse( - "Empty response body" - ) - try { - json.decodeFromString(responseBody) - } catch (e: Exception) { - Log.e(TAG, "Failed to decode upload response: ${e.message}") - throw SyncException.DecodingError( - e.message ?: "Failed to decode response" - ) - } - } - 401 -> throw SyncException.Unauthorized - else -> { - val errorBody = response.body?.string() ?: "No error details" - Log.e(TAG, "Server error ${response.code}: $errorBody") - throw SyncException.ServerError(response.code) - } - } - } catch (e: IOException) { - throw SyncException.NetworkError(e.message ?: "Network error") - } - } - - suspend fun uploadImage(filename: String, imageData: ByteArray) = - withContext(Dispatchers.IO) { - if (!isConfigured) { - throw SyncException.NotConfigured - } - - val justFilename = filename.substringAfterLast('/') - val requestBody = imageData.toRequestBody("image/*".toMediaType()) - - val request = - Request.Builder() - .url("$serverURL/images/upload?filename=$justFilename") - .post(requestBody) - .addHeader("Authorization", "Bearer $authToken") - .build() - - try { - val response = httpClient.newCall(request).execute() - - when (response.code) { - 200 -> Unit - 401 -> throw SyncException.Unauthorized - else -> { - val errorBody = response.body?.string() ?: "No error details" - Log.e(TAG, "Image upload error ${response.code}: $errorBody") - throw SyncException.ServerError(response.code) - } - } - } catch (e: IOException) { - throw SyncException.NetworkError(e.message ?: "Network error") - } - } - - suspend fun downloadImage(filename: String): ByteArray = - withContext(Dispatchers.IO) { - if (!isConfigured) { - throw SyncException.NotConfigured - } - - Log.d(TAG, "Downloading image from server: $filename") - val request = - Request.Builder() - .url("$serverURL/images/download?filename=$filename") - .get() - .addHeader("Authorization", "Bearer $authToken") - .build() - - try { - val response = httpClient.newCall(request).execute() - Log.d(TAG, "Image download response for $filename: ${response.code}") - if (response.code != 200) { - Log.w(TAG, "Failed request URL: ${request.url}") - } - - when (response.code) { - 200 -> { - val imageBytes = - response.body?.bytes() - ?: throw SyncException.InvalidResponse( - "Empty image response" - ) - Log.d( - TAG, - "Successfully downloaded image $filename: ${imageBytes.size} bytes" - ) - imageBytes - } - 401 -> throw SyncException.Unauthorized - 404 -> { - Log.w(TAG, "Image not found on server: $filename") - throw SyncException.ImageNotFound(filename) - } - else -> { - val errorBody = response.body?.string() ?: "No error details" - Log.e( - TAG, - "Image download error ${response.code} for $filename: $errorBody" - ) - throw SyncException.ServerError(response.code) - } - } - } catch (e: IOException) { - Log.e(TAG, "Network error downloading image $filename: ${e.message}") - throw SyncException.NetworkError(e.message ?: "Network error") - } - } - + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) suspend fun syncWithServer() { - if (!isConfigured) { + if (isOfflineMode) { + Log.d(TAG, "Sync skipped: Offline mode is enabled.") + return + } + if (!isNetworkAvailable()) { + _syncError.value = "No internet connection." + Log.d(TAG, "Sync skipped: No internet connection.") + return + } + if (!_isConfigured.value) { throw SyncException.NotConfigured } - if (!_isConnected.value) { throw SyncException.NotConnected } @@ -361,20 +191,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep _syncError.value = null try { - Log.d(TAG, "Fixing existing image paths before sync") - val pathFixSuccess = fixImagePaths() - if (!pathFixSuccess) { - Log.w(TAG, "Image path fix failed, but continuing with sync") - } - - Log.d(TAG, "Performing image migration before sync") - val migrationSuccess = migrateImagesForSync() - if (!migrationSuccess) { - Log.w(TAG, "Image migration failed, but continuing with sync") - } - val localBackup = createBackupFromRepository() - val serverBackup = downloadData() val hasLocalData = @@ -403,9 +220,9 @@ class SyncService(private val context: Context, private val repository: ClimbRep Log.d(TAG, "Initial upload completed") } hasLocalData && hasServerData -> { - Log.d(TAG, "Both local and server data exist, merging safely") - mergeDataSafely(localBackup, serverBackup) - Log.d(TAG, "Safe merge completed") + Log.d(TAG, "Both local and server data exist, merging (server wins)") + mergeDataSafely(serverBackup) + Log.d(TAG, "Merge completed") } else -> { Log.d(TAG, "No data to sync") @@ -414,7 +231,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep val now = DateFormatUtils.nowISO8601() _lastSyncTime.value = now - sharedPreferences.edit().putString(Keys.LAST_SYNC_TIME, now).apply() + sharedPreferences.edit { putString(Keys.LAST_SYNC_TIME, now) } } catch (e: Exception) { _syncError.value = e.message throw e @@ -424,871 +241,329 @@ class SyncService(private val context: Context, private val repository: ClimbRep } } + private suspend fun downloadData(): ClimbDataBackup { + val request = + Request.Builder() + .url("$serverUrl/sync") + .header("Authorization", "Bearer $authToken") + .get() + .build() + + return withContext(Dispatchers.IO) { + try { + httpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + val body = response.body?.string() + if (!body.isNullOrEmpty()) { + json.decodeFromString(body) + } else { + ClimbDataBackup( + exportedAt = DateFormatUtils.nowISO8601(), + gyms = emptyList(), + problems = emptyList(), + sessions = emptyList(), + attempts = emptyList() + ) + } + } else { + handleHttpError(response.code) + } + } + } catch (e: IOException) { + throw SyncException.NetworkError(e.message ?: "Network error") + } + } + } + + private suspend fun uploadData(backup: ClimbDataBackup) { + val requestBody = + json.encodeToString(ClimbDataBackup.serializer(), backup) + .toRequestBody("application/json".toMediaType()) + + val request = + Request.Builder() + .url("$serverUrl/sync") + .header("Authorization", "Bearer $authToken") + .post(requestBody) + .build() + + withContext(Dispatchers.IO) { + try { + httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + handleHttpError(response.code) + } + } + } catch (e: IOException) { + throw SyncException.NetworkError(e.message ?: "Network error") + } + } + } + private suspend fun syncImagesFromServer(backup: ClimbDataBackup): Map { val imagePathMapping = mutableMapOf() + val totalImages = backup.problems.sumOf { it.imagePaths?.size ?: 0 } + Log.d(TAG, "Starting image download from server for $totalImages images") - Log.d(TAG, "Starting to download images from server") - var totalImages = 0 - var downloadedImages = 0 - var failedImages = 0 - - for (problem in backup.problems) { - val imageCount = problem.imagePaths?.size ?: 0 - if (imageCount > 0) { - Log.d( - TAG, - "Problem '${problem.name}' has $imageCount images: ${problem.imagePaths}" - ) - totalImages += imageCount - } - - problem.imagePaths?.forEach { imagePath -> - try { - // Use the server's actual filename, not regenerated + withContext(Dispatchers.IO) { + backup.problems.forEach { problem -> + problem.imagePaths?.forEach { imagePath -> val serverFilename = imagePath.substringAfterLast('/') - - Log.d(TAG, "Attempting to download image: $serverFilename") - val imageData = downloadImage(serverFilename) - - val localImagePath = - ImageUtils.saveImageFromBytesWithFilename( - context, - imageData, - serverFilename - ) - - if (localImagePath != null) { - imagePathMapping[imagePath] = localImagePath - downloadedImages++ - Log.d( - TAG, - "Downloaded and mapped image: $serverFilename -> $localImagePath" - ) - } else { - Log.w(TAG, "Failed to save downloaded image locally: $imagePath") - failedImages++ + try { + val localImagePath = downloadImage(serverFilename) + if (localImagePath != null) { + imagePathMapping[imagePath] = localImagePath + } + } catch (_: SyncException.ImageNotFound) { + Log.w(TAG, "Image not found on server: $imagePath") + } catch (e: Exception) { + Log.w(TAG, "Failed to download image $imagePath: ${e.message}") } - } catch (e: SyncException.ImageNotFound) { - Log.w( - TAG, - "Image not found on server: $imagePath - might be missing or use different naming" - ) - failedImages++ - } catch (e: Exception) { - Log.w(TAG, "Failed to download image $imagePath: ${e.message}") - failedImages++ } } } - - Log.d( - TAG, - "Image download completed: $downloadedImages downloaded, $failedImages failed, $totalImages total" - ) return imagePathMapping } + private suspend fun downloadImage(serverFilename: String): String? { + val request = + Request.Builder() + .url("$serverUrl/images/download?filename=$serverFilename") + .header("Authorization", "Bearer $authToken") + .build() + + return withContext(Dispatchers.IO) { + try { + httpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + response.body?.bytes()?.let { + ImageUtils.saveImageFromBytesWithFilename(context, it, serverFilename) + } + } else { + if (response.code == 404) throw SyncException.ImageNotFound + null + } + } + } catch (e: IOException) { + Log.e(TAG, "Network error downloading image $serverFilename", e) + null + } + } + } + private suspend fun syncImagesForBackup(backup: ClimbDataBackup) { Log.d(TAG, "Starting image sync for backup with ${backup.problems.size} problems") - - var totalImages = 0 - var uploadedImages = 0 - var failedImages = 0 - - for (problem in backup.problems) { - val imageCount = problem.imagePaths?.size ?: 0 - totalImages += imageCount - - Log.d(TAG, "Problem '${problem.name}' has $imageCount images: ${problem.imagePaths}") - - problem.imagePaths?.forEachIndexed { index, imagePath -> - try { - val imageFile = ImageUtils.getImageFile(context, imagePath) - Log.d(TAG, "Checking image file: $imagePath -> ${imageFile.absolutePath}") - Log.d( - TAG, - "Image file exists: ${imageFile.exists()}, size: ${if (imageFile.exists()) imageFile.length() else 0} bytes" - ) - - if (imageFile.exists() && imageFile.length() > 0) { - val imageData = imageFile.readBytes() - // Always use consistent problem-based naming for uploads - val consistentFilename = - ImageNamingUtils.generateImageFilename(problem.id, index) - - val filename = imagePath.substringAfterLast('/') - - // Rename local file if needed - if (filename != consistentFilename) { - val newFile = java.io.File(imageFile.parent, consistentFilename) - if (imageFile.renameTo(newFile)) { - Log.d( - TAG, - "Renamed local image file: $filename -> $consistentFilename" - ) - } else { - Log.w(TAG, "Failed to rename local image file, using original") - } - } - - Log.d(TAG, "Uploading image: $consistentFilename (${imageData.size} bytes)") - uploadImage(consistentFilename, imageData) - uploadedImages++ - Log.d(TAG, "Successfully uploaded image: $consistentFilename") - } else { - Log.w( - TAG, - "Image file not found or empty: $imagePath at ${imageFile.absolutePath}" - ) - failedImages++ - } - } catch (e: Exception) { - Log.e(TAG, "Failed to upload image $imagePath: ${e.message}", e) - failedImages++ + withContext(Dispatchers.IO) { + backup.problems.forEach { problem -> + problem.imagePaths?.forEach { localPath -> + val filename = localPath.substringAfterLast('/') + uploadImage(localPath, filename) } } } + } - Log.d( - TAG, - "Image sync completed: $uploadedImages uploaded, $failedImages failed, $totalImages total" - ) + private suspend fun uploadImage(localPath: String, filename: String) { + val file = ImageUtils.getImageFile(context, localPath) + if (!file.exists()) { + Log.w(TAG, "Local image file not found, cannot upload: $localPath") + return + } + + val requestBody = file.readBytes().toRequestBody("application/octet-stream".toMediaType()) + + val request = + Request.Builder() + .url("$serverUrl/images/upload?filename=$filename") + .header("Authorization", "Bearer $authToken") + .post(requestBody) + .build() + + withContext(Dispatchers.IO) { + try { + httpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + Log.d(TAG, "Successfully uploaded image: $filename") + } else { + Log.w( + TAG, + "Failed to upload image $filename. Server responded with ${response.code}" + ) + } + } + } catch (e: IOException) { + Log.e(TAG, "Network error uploading image $filename", e) + } + } } private suspend fun createBackupFromRepository(): ClimbDataBackup { - val allGyms = repository.getAllGyms().first() - val allProblems = repository.getAllProblems().first() - val allSessions = repository.getAllSessions().first() - val allAttempts = repository.getAllAttempts().first() - - // Filter out active sessions and their attempts from sync - val completedSessions = allSessions.filter { it.status != SessionStatus.ACTIVE } - val activeSessionIds = - allSessions.filter { it.status == SessionStatus.ACTIVE }.map { it.id }.toSet() - val completedAttempts = allAttempts.filter { !activeSessionIds.contains(it.sessionId) } - - Log.d( - TAG, - "Sync exclusions: ${allSessions.size - completedSessions.size} active sessions, ${allAttempts.size - completedAttempts.size} active session attempts" - ) - - return ClimbDataBackup( - exportedAt = dataStateManager.getLastModified(), - version = "2.0", - formatVersion = "2.0", - gyms = allGyms.map { BackupGym.fromGym(it) }, - problems = - allProblems.map { problem -> - // Normalize image paths to consistent naming in backup - val normalizedImagePaths = - problem.imagePaths?.mapIndexed { index, _ -> - ImageNamingUtils.generateImageFilename(problem.id, index) - } - - val backupProblem = BackupProblem.fromProblem(problem) - if (!normalizedImagePaths.isNullOrEmpty()) { - backupProblem.copy(imagePaths = normalizedImagePaths) - } else { - backupProblem - } - }, - sessions = completedSessions.map { BackupClimbSession.fromClimbSession(it) }, - attempts = completedAttempts.map { BackupAttempt.fromAttempt(it) }, - deletedItems = repository.getDeletedItems() - ) + return withContext(Dispatchers.Default) { + ClimbDataBackup( + exportedAt = dataStateManager.getLastModified(), + gyms = repository.getAllGyms().first().map { BackupGym.fromGym(it) }, + problems = + repository.getAllProblems().first().map { problem -> + val backupProblem = BackupProblem.fromProblem(problem) + val normalizedImagePaths = + problem.imagePaths.mapIndexed { index, _ -> + ImageNamingUtils.generateImageFilename( + problem.id, + index + ) + } + if (normalizedImagePaths.isNotEmpty()) { + backupProblem.copy(imagePaths = normalizedImagePaths) + } else { + backupProblem + } + }, + sessions = + repository.getAllSessions().first().map { + BackupClimbSession.fromClimbSession(it) + }, + attempts = + repository.getAllAttempts().first().map { + BackupAttempt.fromAttempt(it) + }, + deletedItems = repository.getDeletedItems() + ) + } } private suspend fun importBackupToRepository( backup: ClimbDataBackup, - imagePathMapping: Map = emptyMap() + imagePathMapping: Map ) { - // Store active sessions and their attempts before reset - val activeSessions = - repository.getAllSessions().first().filter { it.status == SessionStatus.ACTIVE } - val activeSessionIds = activeSessions.map { it.id }.toSet() - val activeAttempts = - repository.getAllAttempts().first().filter { - activeSessionIds.contains(it.sessionId) + val gyms = backup.gyms.map { it.toGym() } + val problems = + backup.problems.map { backupProblem -> + val imagePaths = backupProblem.imagePaths + val updatedImagePaths = + imagePaths?.map { oldPath -> + imagePathMapping[oldPath] ?: oldPath + } + backupProblem.copy(imagePaths = updatedImagePaths).toProblem() } - - Log.d( - TAG, - "Preserving ${activeSessions.size} active sessions and ${activeAttempts.size} active attempts during import" - ) + val sessions = backup.sessions.map { it.toClimbSession() } + val attempts = backup.attempts.map { it.toAttempt() } repository.resetAllData() - // Filter out deleted gyms before importing - val deletedGymIds = backup.deletedItems.filter { it.type == "gym" }.map { it.id }.toSet() - backup.gyms.forEach { backupGym -> - try { - if (!deletedGymIds.contains(backupGym.id)) { - val gym = backupGym.toGym() - Log.d(TAG, "Importing gym: ${gym.name} (ID: ${gym.id})") - repository.insertGymWithoutSync(gym) - } else { - Log.d(TAG, "Skipping import of deleted gym: ${backupGym.id}") - } - } catch (e: Exception) { - Log.e(TAG, "Failed to import gym '${backupGym.name}': ${e.message}") - throw e - } - } + gyms.forEach { repository.insertGymWithoutSync(it) } + problems.forEach { repository.insertProblemWithoutSync(it) } + sessions.forEach { repository.insertSessionWithoutSync(it) } + attempts.forEach { repository.insertAttemptWithoutSync(it) } - // Filter out deleted problems before importing - val deletedProblemIds = - backup.deletedItems.filter { it.type == "problem" }.map { it.id }.toSet() - backup.problems.forEach { backupProblem -> - try { - if (!deletedProblemIds.contains(backupProblem.id)) { - val updatedProblem = - if (imagePathMapping.isNotEmpty()) { - val newImagePaths = - backupProblem.imagePaths?.map { oldPath -> - val filename = oldPath.substringAfterLast('/') - - imagePathMapping[filename] - ?: if (ImageNamingUtils.isValidImageFilename( - filename - ) - ) { - "problem_images/$filename" - } else { - val index = - backupProblem.imagePaths.indexOf( - oldPath - ) - val consistentFilename = - ImageNamingUtils - .generateImageFilename( - backupProblem.id, - index - ) - "problem_images/$consistentFilename" - } - } - ?: emptyList() - backupProblem.withUpdatedImagePaths(newImagePaths) - } else { - backupProblem - } - repository.insertProblemWithoutSync(updatedProblem.toProblem()) - } else { - Log.d(TAG, "Skipping import of deleted problem: ${backupProblem.id}") - } - } catch (e: Exception) { - Log.e(TAG, "Failed to import problem '${backupProblem.name}': ${e.message}") - } - } - - // Filter out deleted sessions before importing - val deletedSessionIds = - backup.deletedItems.filter { it.type == "session" }.map { it.id }.toSet() - backup.sessions.forEach { backupSession -> - try { - if (!deletedSessionIds.contains(backupSession.id)) { - repository.insertSessionWithoutSync(backupSession.toClimbSession()) - } else { - Log.d(TAG, "Skipping import of deleted session: ${backupSession.id}") - } - } catch (e: Exception) { - Log.e(TAG, "Failed to import session '${backupSession.id}': ${e.message}") - } - } - - // Filter out deleted attempts before importing - val deletedAttemptIds = - backup.deletedItems.filter { it.type == "attempt" }.map { it.id }.toSet() - backup.attempts.forEach { backupAttempt -> - try { - if (!deletedAttemptIds.contains(backupAttempt.id)) { - repository.insertAttemptWithoutSync(backupAttempt.toAttempt()) - } else { - Log.d(TAG, "Skipping import of deleted attempt: ${backupAttempt.id}") - } - } catch (e: Exception) { - Log.e(TAG, "Failed to import attempt '${backupAttempt.id}': ${e.message}") - } - } - - // Restore active sessions and their attempts after import - activeSessions.forEach { session -> - try { - Log.d(TAG, "Restoring active session: ${session.id}") - repository.insertSessionWithoutSync(session) - } catch (e: Exception) { - Log.e(TAG, "Failed to restore active session '${session.id}': ${e.message}") - } - } - - activeAttempts.forEach { attempt -> - try { - repository.insertAttemptWithoutSync(attempt) - } catch (e: Exception) { - Log.e(TAG, "Failed to restore active attempt '${attempt.id}': ${e.message}") - } - } - - // Import deletion records to prevent future resurrections - backup.deletedItems.forEach { deletion -> - try { - val deletionJson = json.encodeToString(deletion) - val preferences = - context.getSharedPreferences("deleted_items", Context.MODE_PRIVATE) - preferences.edit { putString("deleted_${deletion.id}", deletionJson) } - Log.d(TAG, "Imported deletion record: ${deletion.type} ${deletion.id}") - } catch (e: Exception) { - Log.e(TAG, "Failed to import deletion record: ${e.message}") - } - } - - dataStateManager.setLastModified(backup.exportedAt) - Log.d(TAG, "Data state synchronized to imported timestamp: ${backup.exportedAt}") - } - - private suspend fun mergeDataSafely( - localBackup: ClimbDataBackup, - serverBackup: ClimbDataBackup - ) { - val imagePathMapping = syncImagesFromServer(serverBackup) - - // Get all local data - val localGyms = repository.getAllGyms().first() - val localProblems = repository.getAllProblems().first() - val localSessions = repository.getAllSessions().first() - val localAttempts = repository.getAllAttempts().first() - - // Store active sessions before clearing (but exclude any that were deleted) - val localDeletedItems = repository.getDeletedItems() - val allDeletedSessionIds = - (serverBackup.deletedItems + localDeletedItems) - .filter { it.type == "session" } - .map { it.id } - .toSet() - val activeSessions = - localSessions.filter { - it.status == SessionStatus.ACTIVE && !allDeletedSessionIds.contains(it.id) - } - val activeSessionIds = activeSessions.map { it.id }.toSet() - val allDeletedAttemptIds = - (serverBackup.deletedItems + localDeletedItems) - .filter { it.type == "attempt" } - .map { it.id } - .toSet() - val activeAttempts = - localAttempts.filter { - activeSessionIds.contains(it.sessionId) && !allDeletedAttemptIds.contains(it.id) - } - - // Merge deletion lists - val localDeletions = repository.getDeletedItems() - val allDeletions = (localDeletions + serverBackup.deletedItems).distinctBy { it.id } - - Log.d(TAG, "Merging data...") - val mergedGyms = mergeGyms(localGyms, serverBackup.gyms, allDeletions) - val mergedProblems = - mergeProblems(localProblems, serverBackup.problems, imagePathMapping, allDeletions) - val mergedSessions = mergeSessions(localSessions, serverBackup.sessions, allDeletions) - val mergedAttempts = mergeAttempts(localAttempts, serverBackup.attempts, allDeletions) - - // Clear and repopulate with merged data - repository.resetAllData() - - mergedGyms.forEach { gym -> - try { - repository.insertGymWithoutSync(gym) - } catch (e: Exception) { - Log.e(TAG, "Failed to insert merged gym: ${e.message}") - } - } - - mergedProblems.forEach { problem -> - try { - repository.insertProblemWithoutSync(problem) - } catch (e: Exception) { - Log.e(TAG, "Failed to insert merged problem: ${e.message}") - } - } - - mergedSessions.forEach { session -> - try { - repository.insertSessionWithoutSync(session) - } catch (e: Exception) { - Log.e(TAG, "Failed to insert merged session: ${e.message}") - } - } - - mergedAttempts.forEach { attempt -> - try { - repository.insertAttemptWithoutSync(attempt) - } catch (e: Exception) { - Log.e(TAG, "Failed to insert merged attempt: ${e.message}") - } - } - - // Restore active sessions - activeSessions.forEach { session -> - try { - repository.insertSessionWithoutSync(session) - } catch (e: Exception) { - Log.e(TAG, "Failed to restore active session: ${e.message}") - } - } - - activeAttempts.forEach { attempt -> - try { - repository.insertAttemptWithoutSync(attempt) - } catch (e: Exception) { - Log.e(TAG, "Failed to restore active attempt: ${e.message}") - } - } - - // Update local deletions with merged list repository.clearDeletedItems() - allDeletions.forEach { deletion -> - try { - val deletionJson = json.encodeToString(deletion) - val preferences = - context.getSharedPreferences("deleted_items", Context.MODE_PRIVATE) - preferences.edit { putString("deleted_${deletion.id}", deletionJson) } - Log.d(TAG, "Merged deletion record: ${deletion.type} ${deletion.id}") - } catch (e: Exception) { - Log.e(TAG, "Failed to save merged deletion: ${e.message}") - } - } - - // Upload merged data back to server - val mergedBackup = createBackupFromRepository() - uploadData(mergedBackup) - syncImagesForBackup(mergedBackup) - - dataStateManager.updateDataState() } - private fun mergeGyms( - local: List, - server: List, - deletedItems: List - ): List { - val merged = local.toMutableList() - val localIds = local.map { it.id }.toSet() - val deletedGymIds = deletedItems.filter { it.type == "gym" }.map { it.id }.toSet() - - merged.removeAll { deletedGymIds.contains(it.id) } - - server.forEach { serverGym -> - if (!localIds.contains(serverGym.id) && !deletedGymIds.contains(serverGym.id)) { - try { - merged.add(serverGym.toGym()) - } catch (e: Exception) { - Log.e(TAG, "Failed to convert server gym: ${e.message}") - } - } - } - - return merged + private suspend fun mergeDataSafely(serverBackup: ClimbDataBackup) { + Log.d(TAG, "Server data will overwrite local data. Performing full restore.") + val imagePathMapping = syncImagesFromServer(serverBackup) + importBackupToRepository(serverBackup, imagePathMapping) } - private fun mergeProblems( - local: List, - server: List, - imagePathMapping: Map, - deletedItems: List - ): List { - val merged = local.toMutableList() - val localIds = local.map { it.id }.toSet() - val deletedProblemIds = deletedItems.filter { it.type == "problem" }.map { it.id }.toSet() - - merged.removeAll { deletedProblemIds.contains(it.id) } - - server.forEach { serverProblem -> - if (!localIds.contains(serverProblem.id) && - !deletedProblemIds.contains(serverProblem.id) - ) { - try { - val problemToAdd = - if (imagePathMapping.isNotEmpty() && - !serverProblem.imagePaths.isNullOrEmpty() - ) { - val updatedImagePaths = - serverProblem.imagePaths?.mapNotNull { oldPath -> - imagePathMapping[oldPath] ?: oldPath - } - if (updatedImagePaths != serverProblem.imagePaths) { - serverProblem.copy(imagePaths = updatedImagePaths) - } else { - serverProblem - } - } else { - serverProblem - } - merged.add(problemToAdd.toProblem()) - } catch (e: Exception) { - Log.e(TAG, "Failed to convert server problem: ${e.message}") - } - } - } - - return merged - } - - private fun mergeSessions( - local: List, - server: List, - deletedItems: List - ): List { - val merged = local.toMutableList() - val localIds = local.map { it.id }.toSet() - val deletedSessionIds = deletedItems.filter { it.type == "session" }.map { it.id }.toSet() - - merged.removeAll { deletedSessionIds.contains(it.id) && it.status != SessionStatus.ACTIVE } - - server.forEach { serverSession -> - if (!localIds.contains(serverSession.id) && - !deletedSessionIds.contains(serverSession.id) - ) { - try { - merged.add(serverSession.toClimbSession()) - } catch (e: Exception) { - Log.e(TAG, "Failed to convert server session: ${e.message}") - } - } - } - - return merged - } - - private fun mergeAttempts( - local: List, - server: List, - deletedItems: List - ): List { - val merged = local.toMutableList() - val localIds = local.map { it.id }.toSet() - val deletedAttemptIds = deletedItems.filter { it.type == "attempt" }.map { it.id }.toSet() - - merged.removeAll { deletedAttemptIds.contains(it.id) } - - server.forEach { serverAttempt -> - if (!localIds.contains(serverAttempt.id) && - !deletedAttemptIds.contains(serverAttempt.id) - ) { - try { - merged.add(serverAttempt.toAttempt()) - } catch (e: Exception) { - Log.e(TAG, "Failed to convert server attempt: ${e.message}") - } - } - } - - return merged - } - - /** Parses ISO8601 timestamp to milliseconds for comparison */ - private fun parseISO8601ToMillis(timestamp: String): Long { - return try { - Instant.parse(timestamp).toEpochMilli() - } catch (e: Exception) { - Log.w(TAG, "Failed to parse timestamp: $timestamp, using 0", e) - 0L - } - } - - /** - * Fixes existing image paths in the database to include the proper directory structure. This - * corrects paths like "problem_abc_0.jpg" to "problem_images/problem_abc_0.jpg" - */ - suspend fun fixImagePaths(): Boolean { - return try { - Log.d(TAG, "Fixing existing image paths in database") - - val allProblems = repository.getAllProblems().first() - var fixedCount = 0 - - for (problem in allProblems) { - if (problem.imagePaths.isNotEmpty()) { - val originalPaths = problem.imagePaths - val fixedPaths = - problem.imagePaths.map { path -> - if (!path.startsWith("problem_images/") && !path.contains("/")) { - val fixedPath = "problem_images/$path" - Log.d(TAG, "Fixed path: $path -> $fixedPath") - fixedCount++ - fixedPath - } else { - path - } - } - - if (originalPaths != fixedPaths) { - val updatedProblem = problem.copy(imagePaths = fixedPaths) - repository.insertProblem(updatedProblem) - } - } - } - - Log.i(TAG, "Fixed $fixedCount image paths in database") - true - } catch (e: Exception) { - Log.e(TAG, "Failed to fix image paths: ${e.message}", e) - false - } - } - - /** - * Performs image migration to ensure all images use consistent naming convention before sync - * operations. This should be called before any sync to avoid filename conflicts. - */ - suspend fun migrateImagesForSync(): Boolean { - return try { - Log.d(TAG, "Starting image migration for sync compatibility") - val result = migrationService.performFullMigration() - - when (result) { - is com.atridad.openclimb.data.migration.ImageMigrationResult.AlreadyCompleted -> { - Log.d(TAG, "Image migration already completed") - true - } - is com.atridad.openclimb.data.migration.ImageMigrationResult.Success -> { - Log.i( - TAG, - "Image migration completed: ${result.totalMigrated} images migrated, ${result.errors} errors" - ) - true - } - is com.atridad.openclimb.data.migration.ImageMigrationResult.Failed -> { - Log.e(TAG, "Image migration failed: ${result.error}") - false - } - } - } catch (e: Exception) { - Log.e(TAG, "Image migration error: ${e.message}", e) - false + private fun handleHttpError(code: Int): Nothing { + when (code) { + 401 -> throw SyncException.Unauthorized + in 500..599 -> throw SyncException.ServerError(code) + else -> throw SyncException.InvalidResponse("HTTP error code: $code") } } suspend fun testConnection() { - if (!isConfigured) { - throw SyncException.NotConfigured + if (!_isConfigured.value) { + _isConnected.value = false + _syncError.value = "Server URL or Auth Token is not set." + return } - _isTesting.value = true _syncError.value = null + val request = + Request.Builder() + .url("$serverUrl/sync") + .header("Authorization", "Bearer $authToken") + .head() + .build() try { withContext(Dispatchers.IO) { - val request = - Request.Builder() - .url("$serverURL/sync") - .get() - .addHeader("Authorization", "Bearer $authToken") - .addHeader("Accept", "application/json") - .build() - - val response = httpClient.newCall(request).execute() - - when (response.code) { - 200 -> { - _isConnected.value = true - sharedPreferences.edit().putBoolean(Keys.IS_CONNECTED, true).apply() - } - 401 -> throw SyncException.Unauthorized - else -> throw SyncException.ServerError(response.code) + httpClient.newCall(request).execute().use { response -> + _isConnected.value = response.isSuccessful || response.code == 405 } } + if (!_isConnected.value) { + _syncError.value = "Connection failed. Check URL and token." + } } catch (e: Exception) { _isConnected.value = false - sharedPreferences.edit().putBoolean(Keys.IS_CONNECTED, false).apply() - _syncError.value = e.message - throw e + _syncError.value = "Connection error: ${e.message}" } finally { + sharedPreferences.edit { putBoolean(Keys.IS_CONNECTED, _isConnected.value) } _isTesting.value = false } } - suspend fun triggerAutoSync() { - if (!isConfigured || !_isConnected.value || !_isAutoSyncEnabled.value) { + fun triggerAutoSync() { + if (!_isConfigured.value || !_isConnected.value || !_isAutoSyncEnabled.value) { return } - if (_isSyncing.value) { pendingChanges = true return } - syncJob?.cancel() - syncJob = - kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { + serviceScope.launch { delay(syncDebounceDelay) - - do { + try { + syncWithServer() + } catch (e: Exception) { + Log.e(TAG, "Auto-sync failed", e) + } + if (pendingChanges) { pendingChanges = false - - try { - syncWithServer() - } catch (e: Exception) { - Log.e(TAG, "Auto-sync failed: ${e.message}") - _syncError.value = e.message - return@launch - } - - if (pendingChanges) { - delay(syncDebounceDelay) - } - } while (pendingChanges) + triggerAutoSync() + } } } - suspend fun forceSyncNow() { - if (!isConfigured || !_isConnected.value) return - - syncJob?.cancel() - syncJob = null - pendingChanges = false - - try { - syncWithServer() - } catch (e: Exception) { - Log.e(TAG, "Force sync failed: ${e.message}") - _syncError.value = e.message - } - } - fun clearConfiguration() { syncJob?.cancel() - syncJob = null - pendingChanges = false - - serverURL = "" + serverUrl = "" authToken = "" setAutoSyncEnabled(true) _lastSyncTime.value = null _isConnected.value = false _syncError.value = null - - sharedPreferences.edit().clear().apply() + sharedPreferences.edit { clear() } updateConfiguredState() } - - // MARK: - Image Naming Migration - - private suspend fun performImageNamingMigration() = - withContext(Dispatchers.IO) { - val migrationKey = "image_naming_migration_completed" - if (sharedPreferences.getBoolean(migrationKey, false)) { - Log.d(TAG, "Image naming migration already completed") - return@withContext - } - - Log.d(TAG, "Starting image naming migration...") - var updateCount = 0 - - try { - // Get all problems with images - val problems = repository.getAllProblems().first() - val updatedProblems = mutableListOf() - - for (problem in problems) { - if (problem.imagePaths.isNullOrEmpty()) { - continue - } - - val updatedImagePaths = mutableListOf() - var hasChanges = false - - problem.imagePaths.forEachIndexed { index, imagePath -> - val currentFilename = imagePath.substringAfterLast('/') - val consistentFilename = - ImageNamingUtils.generateImageFilename(problem.id, index) - - if (currentFilename != consistentFilename) { - // Get the image file - val oldFile = ImageUtils.getImageFile(context, imagePath) - - if (oldFile.exists()) { - val newPath = "problem_images/$consistentFilename" - val newFile = ImageUtils.getImageFile(context, newPath) - - try { - // Create parent directory if needed - newFile.parentFile?.mkdirs() - - if (oldFile.renameTo(newFile)) { - updatedImagePaths.add(newPath) - hasChanges = true - updateCount++ - Log.d( - TAG, - "Migrated image: $currentFilename -> $consistentFilename" - ) - } else { - Log.w(TAG, "Failed to migrate image $currentFilename") - updatedImagePaths.add( - imagePath - ) // Keep original on failure - } - } catch (e: Exception) { - Log.w( - TAG, - "Failed to migrate image $currentFilename: ${e.message}" - ) - updatedImagePaths.add(imagePath) // Keep original on failure - } - } else { - updatedImagePaths.add( - imagePath - ) // Keep original if file doesn't exist - } - } else { - updatedImagePaths.add(imagePath) // Already consistent - } - } - - if (hasChanges) { - val updatedProblem = - problem.copy( - imagePaths = updatedImagePaths, - updatedAt = DateFormatUtils.formatISO8601(Instant.now()) - ) - updatedProblems.add(updatedProblem) - } - } - - // Update problems in database - if (updatedProblems.isNotEmpty()) { - updatedProblems.forEach { problem -> repository.updateProblem(problem) } - Log.d( - TAG, - "Updated ${updatedProblems.size} problems with migrated image paths" - ) - } - - // Mark migration as completed - sharedPreferences.edit().putBoolean(migrationKey, true).apply() - Log.d(TAG, "Image naming migration completed, updated $updateCount images") - - // Trigger sync after migration if images were updated - if (updateCount > 0) { - Log.d(TAG, "Triggering sync after image migration") - triggerAutoSync() - } - } catch (e: Exception) { - Log.e(TAG, "Image naming migration failed: ${e.message}", e) - } - } } -sealed class SyncException(message: String) : Exception(message) { +sealed class SyncException(message: String) : IOException(message), Serializable { object NotConfigured : - SyncException("Sync is not configured. Please set server URL and auth token.") - object NotConnected : SyncException("Not connected to server. Please test connection first.") - object Unauthorized : SyncException("Unauthorized. Please check your auth token.") + SyncException("Sync is not configured. Please set server URL and auth token.") { + @JvmStatic private fun readResolve(): Any = NotConfigured + } + + object NotConnected : SyncException("Not connected to server. Please test connection first.") { + @JvmStatic private fun readResolve(): Any = NotConnected + } + + object Unauthorized : SyncException("Unauthorized. Please check your auth token.") { + @JvmStatic private fun readResolve(): Any = Unauthorized + } + + object ImageNotFound : SyncException("Image not found on server") { + @JvmStatic private fun readResolve(): Any = ImageNotFound + } + data class ServerError(val code: Int) : SyncException("Server error: HTTP $code") data class InvalidResponse(val details: String) : SyncException("Invalid server response: $details") data class DecodingError(val details: String) : SyncException("Failed to decode server response: $details") - data class ImageNotFound(val filename: String) : SyncException("Image not found: $filename") data class NetworkError(val details: String) : SyncException("Network error: $details") } diff --git a/android/app/src/main/java/com/atridad/openclimb/service/SessionTrackingService.kt b/android/app/src/main/java/com/atridad/openclimb/service/SessionTrackingService.kt index be28811..091d45c 100644 --- a/android/app/src/main/java/com/atridad/openclimb/service/SessionTrackingService.kt +++ b/android/app/src/main/java/com/atridad/openclimb/service/SessionTrackingService.kt @@ -88,11 +88,7 @@ class SessionTrackingService : Service() { return START_REDELIVER_INTENT } - - override fun onTaskRemoved(rootIntent: Intent?) { - super.onTaskRemoved(rootIntent) - } - + override fun onBind(intent: Intent?): IBinder? = null private fun startSessionTracking(sessionId: String) { @@ -153,7 +149,7 @@ class SessionTrackingService : Service() { return try { val activeNotifications = notificationManager.activeNotifications activeNotifications.any { it.id == NOTIFICATION_ID } - } catch (e: Exception) { + } catch (_: Exception) { false } } diff --git a/android/app/src/main/java/com/atridad/openclimb/ui/screens/ProblemsScreen.kt b/android/app/src/main/java/com/atridad/openclimb/ui/screens/ProblemsScreen.kt index 05c12f3..90d20b0 100644 --- a/android/app/src/main/java/com/atridad/openclimb/ui/screens/ProblemsScreen.kt +++ b/android/app/src/main/java/com/atridad/openclimb/ui/screens/ProblemsScreen.kt @@ -4,6 +4,9 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Image import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -13,11 +16,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.atridad.openclimb.R +import com.atridad.openclimb.data.model.Attempt +import com.atridad.openclimb.data.model.AttemptResult import com.atridad.openclimb.data.model.ClimbType import com.atridad.openclimb.data.model.Gym import com.atridad.openclimb.data.model.Problem -import com.atridad.openclimb.ui.components.FullscreenImageViewer -import com.atridad.openclimb.ui.components.ImageDisplay import com.atridad.openclimb.ui.components.SyncIndicator import com.atridad.openclimb.ui.viewmodel.ClimbViewModel @@ -26,10 +29,8 @@ import com.atridad.openclimb.ui.viewmodel.ClimbViewModel fun ProblemsScreen(viewModel: ClimbViewModel, onNavigateToProblemDetail: (String) -> Unit) { val problems by viewModel.problems.collectAsState() val gyms by viewModel.gyms.collectAsState() + val attempts by viewModel.attempts.collectAsState() val context = LocalContext.current - var showImageViewer by remember { mutableStateOf(false) } - var selectedImagePaths by remember { mutableStateOf>(emptyList()) } - var selectedImageIndex by remember { mutableIntStateOf(0) } // Filter state var selectedClimbType by remember { mutableStateOf(null) } @@ -178,12 +179,8 @@ fun ProblemsScreen(viewModel: ClimbViewModel, onNavigateToProblemDetail: (String ProblemCard( problem = problem, gymName = gyms.find { it.id == problem.gymId }?.name ?: "Unknown Gym", + attempts = attempts, onClick = { onNavigateToProblemDetail(problem.id) }, - onImageClick = { imagePaths, index -> - selectedImagePaths = imagePaths - selectedImageIndex = index - showImageViewer = true - }, onToggleActive = { val updatedProblem = problem.copy(isActive = !problem.isActive) viewModel.updateProblem(updatedProblem, context) @@ -194,15 +191,6 @@ fun ProblemsScreen(viewModel: ClimbViewModel, onNavigateToProblemDetail: (String } } } - - // Fullscreen Image Viewer - if (showImageViewer && selectedImagePaths.isNotEmpty()) { - FullscreenImageViewer( - imagePaths = selectedImagePaths, - initialIndex = selectedImageIndex, - onDismiss = { showImageViewer = false } - ) - } } @OptIn(ExperimentalMaterial3Api::class) @@ -210,10 +198,17 @@ fun ProblemsScreen(viewModel: ClimbViewModel, onNavigateToProblemDetail: (String fun ProblemCard( problem: Problem, gymName: String, + attempts: List, onClick: () -> Unit, - onImageClick: ((List, Int) -> Unit)? = null, onToggleActive: (() -> Unit)? = null ) { + val isCompleted = + attempts.any { attempt -> + attempt.problemId == problem.id && + (attempt.result == AttemptResult.SUCCESS || + attempt.result == AttemptResult.FLASH) + } + Card(onClick = onClick, modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { Row( @@ -242,12 +237,35 @@ fun ProblemCard( } Column(horizontalAlignment = Alignment.End) { - Text( - text = problem.difficulty.grade, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (problem.imagePaths.isNotEmpty()) { + Icon( + imageVector = Icons.Default.Image, + contentDescription = "Has images", + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + + if (isCompleted) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = "Completed", + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.tertiary + ) + } + + Text( + text = problem.difficulty.grade, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + } Text( text = problem.climbType.getDisplayName(), @@ -279,16 +297,6 @@ fun ProblemCard( } } - // Display images if any - if (problem.imagePaths.isNotEmpty()) { - Spacer(modifier = Modifier.height(8.dp)) - ImageDisplay( - imagePaths = problem.imagePaths.take(3), // Show max 3 images in list - imageSize = 60, - onImageClick = { index -> onImageClick?.invoke(problem.imagePaths, index) } - ) - } - if (!problem.isActive) { Spacer(modifier = Modifier.height(8.dp)) Text( diff --git a/android/app/src/main/java/com/atridad/openclimb/ui/screens/SettingsScreen.kt b/android/app/src/main/java/com/atridad/openclimb/ui/screens/SettingsScreen.kt index 10a483e..8d9e0ea 100644 --- a/android/app/src/main/java/com/atridad/openclimb/ui/screens/SettingsScreen.kt +++ b/android/app/src/main/java/com/atridad/openclimb/ui/screens/SettingsScreen.kt @@ -44,9 +44,9 @@ fun SettingsScreen(viewModel: ClimbViewModel) { var showResetDialog by remember { mutableStateOf(false) } var showSyncConfigDialog by remember { mutableStateOf(false) } var showDisconnectDialog by remember { mutableStateOf(false) } - var showFixImagesDialog by remember { mutableStateOf(false) } + var showDeleteImagesDialog by remember { mutableStateOf(false) } - var isFixingImages by remember { mutableStateOf(false) } + var isDeletingImages by remember { mutableStateOf(false) } // Sync configuration state @@ -484,46 +484,6 @@ fun SettingsScreen(viewModel: ClimbViewModel) { Spacer(modifier = Modifier.height(8.dp)) - Card( - shape = RoundedCornerShape(12.dp), - colors = - CardDefaults.cardColors( - containerColor = - MaterialTheme.colorScheme.surfaceVariant.copy( - alpha = 0.3f - ) - ) - ) { - ListItem( - headlineContent = { Text("Fix Image Names") }, - supportingContent = { - Text( - "Rename all images to use consistent naming across devices" - ) - }, - leadingContent = { - Icon(Icons.Default.Build, contentDescription = null) - }, - trailingContent = { - TextButton( - onClick = { showFixImagesDialog = true }, - enabled = !isFixingImages && !uiState.isLoading - ) { - if (isFixingImages) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp - ) - } else { - Text("Fix Names") - } - } - } - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - Card( shape = RoundedCornerShape(12.dp), colors = @@ -1005,35 +965,6 @@ fun SettingsScreen(viewModel: ClimbViewModel) { ) } - // Fix Image Names dialog - if (showFixImagesDialog) { - AlertDialog( - onDismissRequest = { showFixImagesDialog = false }, - title = { Text("Fix Image Names") }, - text = { - Text( - "This will rename all existing image files to use a consistent naming system across devices.\n\nThis improves sync reliability between iOS and Android. Your images will not be lost, only renamed.\n\nThis is safe to run multiple times." - ) - }, - confirmButton = { - TextButton( - onClick = { - isFixingImages = true - showFixImagesDialog = false - coroutineScope.launch { - viewModel.migrateImageNamesToDeterministic(context) - isFixingImages = false - viewModel.setMessage("Image names fixed successfully!") - } - } - ) { Text("Fix Names") } - }, - dismissButton = { - TextButton(onClick = { showFixImagesDialog = false }) { Text("Cancel") } - } - ) - } - // Delete All Images dialog if (showDeleteImagesDialog) { AlertDialog( diff --git a/android/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt b/android/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt index 48ca6f8..cc24c84 100644 --- a/android/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt +++ b/android/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt @@ -171,64 +171,6 @@ class ClimbViewModel( val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet() ImageUtils.cleanupOrphanedImages(context, referencedImagePaths) } - fun migrateImageNamesToDeterministic(context: Context) { - viewModelScope.launch { - val allProblems = repository.getAllProblems().first() - var migrationCount = 0 - val updatedProblems = mutableListOf() - - for (problem in allProblems) { - if (problem.imagePaths.isEmpty()) continue - - var newImagePaths = mutableListOf() - var problemNeedsUpdate = false - - for ((index, imagePath) in problem.imagePaths.withIndex()) { - val currentFilename = File(imagePath).name - - if (ImageNamingUtils.isValidImageFilename(currentFilename)) { - newImagePaths.add(imagePath) - continue - } - - val deterministicName = - ImageNamingUtils.generateImageFilename(problem.id, index) - - val imagesDir = ImageUtils.getImagesDirectory(context) - val oldFile = File(imagesDir, currentFilename) - val newFile = File(imagesDir, deterministicName) - - if (oldFile.exists()) { - if (oldFile.renameTo(newFile)) { - newImagePaths.add(deterministicName) - problemNeedsUpdate = true - migrationCount++ - println("Migrated: $currentFilename → $deterministicName") - } else { - println("Failed to migrate $currentFilename") - newImagePaths.add(imagePath) - } - } else { - println("Warning: Image file not found: $currentFilename") - newImagePaths.add(imagePath) - } - } - - if (problemNeedsUpdate) { - val updatedProblem = problem.copy(imagePaths = newImagePaths) - updatedProblems.add(updatedProblem) - } - } - - for (updatedProblem in updatedProblems) { - repository.insertProblemWithoutSync(updatedProblem) - } - - println( - "Migration completed: $migrationCount images renamed, ${updatedProblems.size} problems updated" - ) - } - } fun deleteAllImages(context: Context) { viewModelScope.launch { diff --git a/ios/OpenClimb.xcodeproj/project.pbxproj b/ios/OpenClimb.xcodeproj/project.pbxproj index 48e7589..c9c880d 100644 --- a/ios/OpenClimb.xcodeproj/project.pbxproj +++ b/ios/OpenClimb.xcodeproj/project.pbxproj @@ -465,7 +465,7 @@ CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 24; + CURRENT_PROJECT_VERSION = 25; DEVELOPMENT_TEAM = 4BC9Y2LL4B; DRIVERKIT_DEPLOYMENT_TARGET = 24.6; ENABLE_PREVIEWS = YES; @@ -513,7 +513,7 @@ CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 24; + CURRENT_PROJECT_VERSION = 25; DEVELOPMENT_TEAM = 4BC9Y2LL4B; DRIVERKIT_DEPLOYMENT_TARGET = 24.6; ENABLE_PREVIEWS = YES; @@ -602,7 +602,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 24; + CURRENT_PROJECT_VERSION = 25; DEVELOPMENT_TEAM = 4BC9Y2LL4B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SessionStatusLive/Info.plist; @@ -632,7 +632,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 24; + CURRENT_PROJECT_VERSION = 25; DEVELOPMENT_TEAM = 4BC9Y2LL4B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SessionStatusLive/Info.plist; diff --git a/ios/OpenClimb.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/OpenClimb.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate index f6ec7800afe8a0befc0023bb89c2707a08d575d8..4b2b45dfe8b31c93f653367e0c98aff6f4f7bb61 100644 GIT binary patch literal 166689 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 Ihr84N4`W66)&Kwi literal 161931 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≥@(hXlWU)P6|vdjrz^ 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 F{{Uw@rGx+g diff --git a/ios/OpenClimb/Components/AsyncImageView.swift b/ios/OpenClimb/Components/AsyncImageView.swift new file mode 100644 index 0000000..6cdfc9b --- /dev/null +++ b/ios/OpenClimb/Components/AsyncImageView.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct AsyncImageView: View { + let imagePath: String + let targetSize: CGSize + + @State private var image: UIImage? + + var body: some View { + ZStack { + Rectangle() + .fill(Color(.systemGray6)) + + if let image = image { + Image(uiImage: image) + .resizable() + .scaledToFill() + .transition(.opacity.animation(.easeInOut(duration: 0.3))) + } else { + Image(systemName: "photo") + .font(.system(size: 24)) + .foregroundColor(Color(.systemGray3)) + } + } + .frame(width: targetSize.width, height: targetSize.height) + .clipped() + .cornerRadius(8) + .task(id: imagePath) { + if self.image != nil { + self.image = nil + } + + self.image = await ImageManager.shared.loadThumbnail( + fromPath: imagePath, + targetSize: targetSize + ) + } + } +} diff --git a/ios/OpenClimb/Services/SyncService.swift b/ios/OpenClimb/Services/SyncService.swift index e84535d..3460f46 100644 --- a/ios/OpenClimb/Services/SyncService.swift +++ b/ios/OpenClimb/Services/SyncService.swift @@ -9,6 +9,7 @@ class SyncService: ObservableObject { @Published var syncError: String? @Published var isConnected = false @Published var isTesting = false + @Published var isOfflineMode = false private let userDefaults = UserDefaults.standard private var syncTask: Task? @@ -19,8 +20,9 @@ class SyncService: ObservableObject { static let serverURL = "sync_server_url" static let authToken = "sync_auth_token" static let lastSyncTime = "last_sync_time" - static let isConnected = "sync_is_connected" + static let isConnected = "is_connected" static let autoSyncEnabled = "auto_sync_enabled" + static let offlineMode = "offline_mode" } var serverURL: String { @@ -46,12 +48,9 @@ class SyncService: ObservableObject { if let lastSync = userDefaults.object(forKey: Keys.lastSyncTime) as? Date { self.lastSyncTime = lastSync } - self.isConnected = userDefaults.bool(forKey: Keys.isConnected) - - // Perform image naming migration on initialization - Task { - await performImageNamingMigration() - } + isConnected = userDefaults.bool(forKey: Keys.isConnected) + isAutoSyncEnabled = userDefaults.object(forKey: Keys.autoSyncEnabled) as? Bool ?? true + isOfflineMode = userDefaults.bool(forKey: Keys.offlineMode) } func downloadData() async throws -> ClimbDataBackup { @@ -211,6 +210,11 @@ class SyncService: ObservableObject { } func syncWithServer(dataManager: ClimbingDataManager) async throws { + if isOfflineMode { + print("Sync skipped: Offline mode is enabled.") + return + } + guard isConfigured else { throw SyncError.notConfigured } @@ -1025,105 +1029,7 @@ class SyncService: ObservableObject { syncTask?.cancel() } - // MARK: - Image Naming Migration - - private func performImageNamingMigration() async { - let migrationKey = "image_naming_migration_completed_v2" - guard !userDefaults.bool(forKey: migrationKey) else { - print("Image naming migration already completed") - return - } - - print("Starting image naming migration...") - var updateCount = 0 - let imageManager = ImageManager.shared - - // Get all problems from UserDefaults - if let problemsData = userDefaults.data(forKey: "problems"), - var problems = try? JSONDecoder().decode([Problem].self, from: problemsData) - { - - for problemIndex in 0.. \(consistentFilename)") - } catch { - print("Failed to migrate image \(currentFilename): \(error)") - updatedImagePaths.append(imagePath) - } - } else { - updatedImagePaths.append(imagePath) - } - } else { - updatedImagePaths.append(imagePath) - } - } - - if hasChanges { - // Decode problem to dictionary, update imagePaths, re-encode - if let problemData = try? JSONEncoder().encode(problem), - var problemDict = try? JSONSerialization.jsonObject(with: problemData) - as? [String: Any] - { - problemDict["imagePaths"] = updatedImagePaths - problemDict["updatedAt"] = ISO8601DateFormatter().string(from: Date()) - if let updatedData = try? JSONSerialization.data( - withJSONObject: problemDict), - let updatedProblem = try? JSONDecoder().decode( - Problem.self, from: updatedData) - { - problems[problemIndex] = updatedProblem - } - } - } - } - - if updateCount > 0 { - if let updatedData = try? JSONEncoder().encode(problems) { - userDefaults.set(updatedData, forKey: "problems") - print("Updated \(updateCount) image paths in UserDefaults") - } - } - } - - userDefaults.set(true, forKey: migrationKey) - print("Image naming migration completed, updated \(updateCount) images") - - // Notify ClimbingDataManager to reload data if images were updated - if updateCount > 0 { - DispatchQueue.main.async { - NotificationCenter.default.post( - name: NSNotification.Name("ImageMigrationCompleted"), - object: nil, - userInfo: ["updateCount": updateCount] - ) - } - } - } - + // MARK: - Merging // MARK: - Safe Merge Functions private func mergeGyms(local: [Gym], server: [BackupGym], deletedItems: [DeletedItem]) -> [Gym] diff --git a/ios/OpenClimb/Utils/ImageManager.swift b/ios/OpenClimb/Utils/ImageManager.swift index e10e8e3..80bdcbb 100644 --- a/ios/OpenClimb/Utils/ImageManager.swift +++ b/ios/OpenClimb/Utils/ImageManager.swift @@ -1,10 +1,12 @@ import Foundation +import ImageIO import SwiftUI import UIKit class ImageManager { static let shared = ImageManager() + private let thumbnailCache = NSCache() private let fileManager = FileManager.default private let appSupportDirectoryName = "OpenClimb" private let imagesDirectoryName = "Images" @@ -479,6 +481,51 @@ class ImageManager { return nil } + func loadThumbnail(fromPath path: String, targetSize: CGSize) async -> UIImage? { + let cacheKey = "\(path)-\(targetSize.width)x\(targetSize.height)" as NSString + + if let cachedImage = thumbnailCache.object(forKey: cacheKey) { + return cachedImage + } + + guard let imageData = loadImageData(fromPath: path) else { + return nil + } + + let options: [CFString: Any] = [ + kCGImageSourceCreateThumbnailFromImageIfAbsent: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceThumbnailMaxPixelSize: max(targetSize.width, targetSize.height) + * UIScreen.main.scale, + ] + + guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil) else { + return UIImage(data: imageData) + } + + let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any] + let orientation = properties?[kCGImagePropertyOrientation] as? UInt32 ?? 1 + + if let cgImage = CGImageSourceCreateThumbnailAtIndex( + imageSource, 0, options as CFDictionary) + { + let imageOrientation = UIImage.Orientation(rawValue: Int(orientation - 1)) ?? .up + let thumbnail = UIImage( + cgImage: cgImage, scale: UIScreen.main.scale, orientation: imageOrientation) + + thumbnailCache.setObject(thumbnail, forKey: cacheKey) + return thumbnail + } else { + if let fallbackImage = UIImage(data: imageData) { + thumbnailCache.setObject(fallbackImage, forKey: cacheKey) + return fallbackImage + } + } + + return nil + } + func imageExists(atPath path: String) -> Bool { let primaryPath = getFullPath(from: path) let backupPath = backupDirectory.appendingPathComponent(getRelativePath(from: path)) @@ -854,72 +901,4 @@ class ImageManager { } } - func migrateImageNamesToDeterministic(dataManager: ClimbingDataManager) { - print("Starting migration of image names to deterministic format...") - - var migrationCount = 0 - var updatedProblems: [Problem] = [] - - for problem in dataManager.problems { - guard !problem.imagePaths.isEmpty else { continue } - - var newImagePaths: [String] = [] - var problemNeedsUpdate = false - - for (index, imagePath) in problem.imagePaths.enumerated() { - let currentFilename = URL(fileURLWithPath: imagePath).lastPathComponent - - if ImageNamingUtils.isValidImageFilename(currentFilename) { - newImagePaths.append(imagePath) - continue - } - - let deterministicName = ImageNamingUtils.generateImageFilename( - problemId: problem.id.uuidString, imageIndex: index) - - let oldPath = imagesDirectory.appendingPathComponent(currentFilename) - let newPath = imagesDirectory.appendingPathComponent(deterministicName) - - if fileManager.fileExists(atPath: oldPath.path) { - do { - try fileManager.moveItem(at: oldPath, to: newPath) - - let oldBackupPath = backupDirectory.appendingPathComponent(currentFilename) - let newBackupPath = backupDirectory.appendingPathComponent( - deterministicName) - - if fileManager.fileExists(atPath: oldBackupPath.path) { - try? fileManager.moveItem(at: oldBackupPath, to: newBackupPath) - } - - newImagePaths.append(deterministicName) - problemNeedsUpdate = true - migrationCount += 1 - - print("Migrated: \(currentFilename) → \(deterministicName)") - - } catch { - print("Failed to migrate \(currentFilename): \(error)") - newImagePaths.append(imagePath) - } - } else { - print("Warning: Image file not found: \(currentFilename)") - newImagePaths.append(imagePath) - } - } - - if problemNeedsUpdate { - let updatedProblem = problem.updated(imagePaths: newImagePaths) - updatedProblems.append(updatedProblem) - } - } - - for updatedProblem in updatedProblems { - dataManager.updateProblem(updatedProblem) - } - - print( - "Migration completed: \(migrationCount) images renamed, \(updatedProblems.count) problems updated" - ) - } } diff --git a/ios/OpenClimb/Utils/OrientationAwareImage.swift b/ios/OpenClimb/Utils/OrientationAwareImage.swift index a12c228..3e1057c 100644 --- a/ios/OpenClimb/Utils/OrientationAwareImage.swift +++ b/ios/OpenClimb/Utils/OrientationAwareImage.swift @@ -37,7 +37,7 @@ struct OrientationAwareImage: View { } private func loadImageWithCorrectOrientation() { - Task { + Task.detached(priority: .userInitiated) { let correctedImage = await loadAndCorrectImage() await MainActor.run { self.uiImage = correctedImage @@ -48,17 +48,10 @@ struct OrientationAwareImage: View { } private func loadAndCorrectImage() async -> UIImage? { - // Load image data from ImageManager - guard - let data = await MainActor.run(body: { - ImageManager.shared.loadImageData(fromPath: imagePath) - }) - else { return nil } + guard let data = ImageManager.shared.loadImageData(fromPath: imagePath) else { return nil } - // Create UIImage from data guard let originalImage = UIImage(data: data) else { return nil } - // Apply orientation correction return correctImageOrientation(originalImage) } diff --git a/ios/OpenClimb/Views/ProblemsView.swift b/ios/OpenClimb/Views/ProblemsView.swift index 3c113b6..5d59bde 100644 --- a/ios/OpenClimb/Views/ProblemsView.swift +++ b/ios/OpenClimb/Views/ProblemsView.swift @@ -9,8 +9,26 @@ struct ProblemsView: View { @State private var showingSearch = false @FocusState private var isSearchFocused: Bool - private var filteredProblems: [Problem] { - var filtered = dataManager.problems + @State private var cachedFilteredProblems: [Problem] = [] + + private func updateFilteredProblems() { + Task(priority: .userInitiated) { + let result = await computeFilteredProblems() + // Switch back to the main thread to update the UI + await MainActor.run { + cachedFilteredProblems = result + } + } + } + + private func computeFilteredProblems() async -> [Problem] { + // Capture dependencies for safe background processing + let problems = dataManager.problems + let searchText = self.searchText + let selectedClimbType = self.selectedClimbType + let selectedGym = self.selectedGym + + var filtered = problems // Apply search filter if !searchText.isEmpty { @@ -93,19 +111,19 @@ struct ProblemsView: View { FilterSection( selectedClimbType: $selectedClimbType, selectedGym: $selectedGym, - filteredProblems: filteredProblems + filteredProblems: cachedFilteredProblems ) .padding() .background(.regularMaterial) } - if filteredProblems.isEmpty { + if cachedFilteredProblems.isEmpty { EmptyProblemsView( isEmpty: dataManager.problems.isEmpty, isFiltered: !dataManager.problems.isEmpty ) } else { - ProblemsList(problems: filteredProblems) + ProblemsList(problems: cachedFilteredProblems) } } } @@ -158,6 +176,21 @@ struct ProblemsView: View { AddEditProblemView() } } + .onAppear { + updateFilteredProblems() + } + .onChange(of: dataManager.problems) { + updateFilteredProblems() + } + .onChange(of: searchText) { + updateFilteredProblems() + } + .onChange(of: selectedClimbType) { + updateFilteredProblems() + } + .onChange(of: selectedGym) { + updateFilteredProblems() + } } } @@ -269,6 +302,7 @@ struct ProblemsList: View { @EnvironmentObject var dataManager: ClimbingDataManager @State private var problemToDelete: Problem? @State private var problemToEdit: Problem? + @State private var animationKey = 0 var body: some View { List(problems, id: \.id) { problem in @@ -309,8 +343,11 @@ struct ProblemsList: View { } .animation( .spring(response: 0.5, dampingFraction: 0.8, blendDuration: 0.1), - value: problems.map { "\($0.id):\($0.isActive)" }.joined() + value: animationKey ) + .onChange(of: problems) { + animationKey += 1 + } .listStyle(.plain) .scrollContentBackground(.hidden) .scrollIndicators(.hidden) @@ -344,6 +381,12 @@ struct ProblemRow: View { dataManager.gym(withId: problem.gymId) } + private var isCompleted: Bool { + dataManager.attempts.contains { attempt in + attempt.problemId == problem.id && attempt.result.isSuccessful + } + } + var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { @@ -361,10 +404,24 @@ struct ProblemRow: View { Spacer() VStack(alignment: .trailing, spacing: 4) { - Text(problem.difficulty.grade) - .font(.title2) - .fontWeight(.bold) - .foregroundColor(.blue) + HStack(spacing: 8) { + if !problem.imagePaths.isEmpty { + Image(systemName: "photo") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.blue) + } + + if isCompleted { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.green) + } + + Text(problem.difficulty.grade) + .font(.title2) + .fontWeight(.bold) + .foregroundColor(.blue) + } Text(problem.climbType.displayName) .font(.caption) @@ -396,17 +453,6 @@ struct ProblemRow: View { } } - if !problem.imagePaths.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(spacing: 8) { - ForEach(problem.imagePaths.prefix(3), id: \.self) { imagePath in - ProblemImageView(imagePath: imagePath) - } - } - .padding(.horizontal, 4) - } - } - if !problem.isActive { Text("Reset / No Longer Set") .font(.caption) @@ -478,17 +524,6 @@ struct EmptyProblemsView: View { } } -struct ProblemImageView: View { - let imagePath: String - - var body: some View { - OrientationAwareImage.fill(imagePath: imagePath) - .frame(width: 60, height: 60) - .clipped() - .cornerRadius(8) - } -} - #Preview { ProblemsView() .environmentObject(ClimbingDataManager.preview) diff --git a/ios/OpenClimb/Views/SettingsView.swift b/ios/OpenClimb/Views/SettingsView.swift index 73c5205..e11efb9 100644 --- a/ios/OpenClimb/Views/SettingsView.swift +++ b/ios/OpenClimb/Views/SettingsView.swift @@ -80,8 +80,7 @@ struct DataManagementSection: View { @Binding var activeSheet: SheetType? @State private var showingResetAlert = false @State private var isExporting = false - @State private var isMigrating = false - @State private var showingMigrationAlert = false + @State private var isDeletingImages = false @State private var showingDeleteImagesAlert = false @@ -121,27 +120,6 @@ struct DataManagementSection: View { } .foregroundColor(.primary) - // Migrate Image Names - Button(action: { - showingMigrationAlert = true - }) { - HStack { - if isMigrating { - ProgressView() - .scaleEffect(0.8) - Text("Migrating Images...") - .foregroundColor(.secondary) - } else { - Image(systemName: "photo.badge.arrow.down") - .foregroundColor(.orange) - Text("Fix Image Names") - } - Spacer() - } - } - .disabled(isMigrating) - .foregroundColor(.primary) - // Delete All Images Button(action: { showingDeleteImagesAlert = true @@ -186,16 +164,7 @@ struct DataManagementSection: View { "Are you sure you want to reset all data? This will permanently delete:\n\n• All gyms and their information\n• All problems and their images\n• All climbing sessions\n• All attempts and progress data\n\nThis action cannot be undone. Consider exporting your data first." ) } - .alert("Fix Image Names", isPresented: $showingMigrationAlert) { - Button("Cancel", role: .cancel) {} - Button("Fix Names") { - migrateImageNames() - } - } message: { - Text( - "This will rename all existing image files to use a consistent naming system across devices.\n\nThis improves sync reliability between iOS and Android. Your images will not be lost, only renamed.\n\nThis is safe to run multiple times." - ) - } + .alert("Delete All Images", isPresented: $showingDeleteImagesAlert) { Button("Cancel", role: .cancel) {} Button("Delete", role: .destructive) { @@ -219,17 +188,6 @@ struct DataManagementSection: View { } } - private func migrateImageNames() { - isMigrating = true - Task { - await MainActor.run { - ImageManager.shared.migrateImageNamesToDeterministic(dataManager: dataManager) - isMigrating = false - dataManager.successMessage = "Image names fixed successfully!" - } - } - } - private func deleteAllImages() { isDeletingImages = true Task { diff --git a/ios/README.md b/ios/README.md new file mode 100644 index 0000000..53b59c6 --- /dev/null +++ b/ios/README.md @@ -0,0 +1,23 @@ +# OpenClimb for iOS + +The native iOS, watchOS, and widget client for OpenClimb, built with Swift and SwiftUI. + +## Project Structure + +This is a standard Xcode project. The main app code is in the `OpenClimb/` directory. + +- `Models/`: Swift `Codable` models (`Problem`, `Gym`, `ClimbSession`) that match the Android app. +- `ViewModels/`: App state and logic. `ClimbingDataManager` is the core here, handling data with SwiftData. +- `Views/`: All the SwiftUI views. + - `AddEdit/`: Views for adding/editing gyms, problems, etc. + - `Detail/`: Detail views for items. +- `Services/`: Handles HealthKit and sync server communication. +- `Utils/`: Helper functions and utilities. + +## Other Targets + +- `OpenClimbWatch/`: The watchOS app for tracking sessions. +- `ClimbingActivityWidget/`: A home screen widget. +- `SessionStatusLive/`: A Live Activity for the lock screen. + +The app is built to be offline-first. All data is stored locally on your device and works without an internet connection. \ No newline at end of file diff --git a/sync/README.md b/sync/README.md new file mode 100644 index 0000000..53e3a33 --- /dev/null +++ b/sync/README.md @@ -0,0 +1,37 @@ +# Sync Server + +A simple Go server for self-hosting your OpenClimb sync data. + +## How It Works + +This server is dead simple. It uses a single `openclimb.json` file for your data and a directory for images. The last client to upload wins, overwriting the old data. Authentication is just a static bearer token. + +## Getting Started + +1. Create a `.env` file in this directory: + ``` + IMAGE=git.atri.dad/atridad/openclimb-sync:latest + APP_PORT=8080 + AUTH_TOKEN=your-super-secret-token + DATA_FILE=/data/openclimb.json + IMAGES_DIR=/data/images + ROOT_DIR=./openclimb-data + ``` + Set `AUTH_TOKEN` to a long, random string. `ROOT_DIR` is where the server will store its data on your machine. + +2. Run with Docker: + ```bash + docker-compose up -d + ``` + The server will be running on `http://localhost:8080`. + +## API + +The API is minimal, just enough for the app to work. All endpoints require an `Authorization: Bearer ` header. + +- `GET /sync`: Download `openclimb.json`. +- `POST /sync`: Upload `openclimb.json`. +- `GET /images/{imageName}`: Download an image. +- `POST /images/{imageName}`: Upload an image. + +Check out `main.go` for the full details. \ No newline at end of file