From 4f9ffe37e4dde130c0bb5e6a95c55415c223caba Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 23 Sep 2025 00:16:55 +0600 Subject: [PATCH 01/33] Extract send files usecase --- app/build.gradle.kts | 2 + .../arkbuilders/drop/app/TransferManager.kt | 69 +++++------------- .../drop/app/data/ResourcesHelperImpl.kt | 30 ++++++++ .../dev/arkbuilders/drop/app/di/AppModule.kt | 8 ++ .../arkbuilders/drop/app/di/TmpEntryPoint.kt | 12 +++ .../drop/app/domain/ResourcesHelper.kt | 7 ++ .../app/domain/usecase/SendFilesUseCase.kt | 73 +++++++++++++++++++ gradle/libs.versions.toml | 2 + 8 files changed, 151 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dbc414b..29b7ec8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -141,6 +141,8 @@ dependencies { implementation(libs.mlkit.barcode.scanning) implementation(libs.accompanist.permissions) + implementation(libs.timber) + // DAGGER SETUP implementation("com.google.dagger:hilt-android:2.56.2") kapt("com.google.dagger:hilt-compiler:2.56.2") diff --git a/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt b/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt index 28756c9..74f26c1 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt @@ -7,13 +7,14 @@ import android.os.Environment import android.provider.MediaStore import android.provider.OpenableColumns import android.util.Log +import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.* import dev.arkbuilders.drop.app.data.HistoryRepository import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl -import dev.arkbuilders.drop.app.data.SenderFileDataImpl import dev.arkbuilders.drop.app.data.TransferStatus +import dev.arkbuilders.drop.app.di.TmpEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext @@ -44,59 +45,23 @@ class TransferManager @Inject constructor( get() = receiveSubscriber?.progress suspend fun sendFiles(fileUris: List): SendFilesBubble? = withContext(Dispatchers.IO) { - try { - Log.d(TAG, "Starting file send for ${fileUris.size} files") - - val profile = profileManager.getCurrentProfile() - val senderProfile = SenderProfile( - name = profile.name.ifEmpty { "Anonymous" }, - avatarB64 = profile.avatarB64.takeIf { it.isNotEmpty() } - ) - - val senderFiles = fileUris.mapNotNull { uri -> - val fileName = getFileName(uri) - if (fileName != null) { - val fileData = SenderFileDataImpl(context, uri) - SenderFile( - name = fileName, - data = fileData - ) - } else { - Log.w(TAG, "Could not get filename for URI: $uri") - null + val entryPoint = EntryPointAccessors.fromApplication( + context.applicationContext, + TmpEntryPoint::class.java + ) + val sendUseCase = entryPoint.sendFilesUseCase() + sendUseCase.invoke(fileUris).fold( + onSuccess = { bubble -> + currentSendBubble = bubble + sendSubscriber = SendFilesSubscriberImpl().also { subscriber -> + bubble.subscribe(subscriber) } - } - - if (senderFiles.isEmpty()) { - Log.e(TAG, "No valid files to send") + return@withContext bubble + }, + onFailure = { return@withContext null - } - - val request = SendFilesRequest( - profile = senderProfile, - files = senderFiles, - config = SenderConfig( - chunkSize = 1024u * 512u, - parallelStreams = 4u, - ), - ) - - // Create and subscribe to bubble - val bubble = sendFiles(request) - currentSendBubble = bubble - - // Set up subscriber - sendSubscriber = SendFilesSubscriberImpl().also { subscriber -> - bubble.subscribe(subscriber) - } - - Log.d(TAG, "Send bubble created with ticket and confirmation: ${bubble.getTicket()} ${bubble.getConfirmation()}") - bubble - - } catch (e: Exception) { - Log.e(TAG, "Error starting file send", e) - null - } + }, + ) } suspend fun receiveFiles(ticket: String, confirmation: UByte): ReceiveFilesBubble? = diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt new file mode 100644 index 0000000..d43f4fa --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt @@ -0,0 +1,30 @@ +package dev.arkbuilders.drop.app.data + +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.arkbuilders.drop.app.domain.ResourcesHelper +import timber.log.Timber +import javax.inject.Inject + +class ResourcesHelperImpl @Inject constructor( + @ApplicationContext private val context: Context, +) : ResourcesHelper { + override fun getFileName(uri: Uri): String? { + return try { + context + .contentResolver + .query(uri, null, null, null, null) + ?.use { cursor -> + if (cursor.moveToFirst()) { + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (nameIndex >= 0) cursor.getString(nameIndex) else null + } else null + } + } catch (e: Exception) { + Timber.e(e, "Error getting filename for URI: $uri") + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index 9315d85..12b81fb 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -9,6 +9,8 @@ import dagger.hilt.components.SingletonComponent import dev.arkbuilders.drop.app.TransferManager import dev.arkbuilders.drop.app.ProfileManager import dev.arkbuilders.drop.app.data.HistoryRepository +import dev.arkbuilders.drop.app.data.ResourcesHelperImpl +import dev.arkbuilders.drop.app.domain.ResourcesHelper import javax.inject.Singleton @Module @@ -30,4 +32,10 @@ object AppModule { ): TransferManager { return TransferManager(context, profileManager, historyRepository) } + + @Provides + @Singleton + fun provideResourcesHelper( + impl: ResourcesHelperImpl + ): ResourcesHelper = impl } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt new file mode 100644 index 0000000..64f2474 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt @@ -0,0 +1,12 @@ +package dev.arkbuilders.drop.app.di + +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface TmpEntryPoint { + fun sendFilesUseCase(): SendFilesUseCase +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt new file mode 100644 index 0000000..8e1da6d --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt @@ -0,0 +1,7 @@ +package dev.arkbuilders.drop.app.domain + +import android.net.Uri + +interface ResourcesHelper { + fun getFileName(uri: Uri): String? +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt new file mode 100644 index 0000000..9bbf327 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt @@ -0,0 +1,73 @@ +package dev.arkbuilders.drop.app.domain.usecase + +import android.content.Context +import android.net.Uri +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.arkbuilders.drop.SendFilesBubble +import dev.arkbuilders.drop.SendFilesRequest +import dev.arkbuilders.drop.SenderConfig +import dev.arkbuilders.drop.SenderFile +import dev.arkbuilders.drop.SenderProfile +import dev.arkbuilders.drop.app.ProfileManager +import dev.arkbuilders.drop.app.data.SenderFileDataImpl +import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.sendFiles +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +class SendFilesUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val profileManager: ProfileManager, + private val resourcesHelper: ResourcesHelper, +) { + suspend operator fun invoke( + fileUris: List, + ): Result = withContext(Dispatchers.IO) { + runCatching { + Timber.d("Starting file send for ${fileUris.size} files") + + val profile = profileManager.getCurrentProfile() + val senderProfile = SenderProfile( + name = profile.name.ifEmpty { "Anonymous" }, + avatarB64 = profile.avatarB64.takeIf { it.isNotEmpty() } + ) + + val senderFiles = fileUris.mapNotNull { uri -> + val fileName = resourcesHelper.getFileName(uri) + if (fileName != null) { + val fileData = SenderFileDataImpl(context, uri) + SenderFile( + name = fileName, + data = fileData + ) + } else { + Timber.w("Could not get filename for URI: $uri") + null + } + } + + if (senderFiles.isEmpty()) { + Timber.e("No valid files to send") + error("No valid files to send") + } + + val request = SendFilesRequest( + profile = senderProfile, + files = senderFiles, + config = SenderConfig( + chunkSize = 1024u * 512u, + parallelStreams = 4u, + ), + ) + + val bubble = sendFiles(request) + + Timber.d("Send bubble created with ticket and confirmation: ${bubble.getTicket()} ${bubble.getConfirmation()}") + bubble + }.onFailure { + Timber.e("Error starting file send ${it.message}") + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a8a602a..562a19d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ cameraLifecycle = "1.4.0" cameraView = "1.4.0" mlkitBarcodeScanning = "17.3.0" accompanistPermissions = "0.36.0" +timber = "5.0.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -35,6 +36,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } google-zxing-core = { group = "com.google.zxing", name = "core", version.ref = "googleZxingCore" } github-yuriy-budiyev-code-scanner = { group = "com.github.yuriy-budiyev", name = "code-scanner", version.ref = "githubYuriyBudiyevCodeScanner" } From efad2cdb7d33e10fca36139d1604f2d6b9325e5e Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 23 Sep 2025 01:08:02 +0600 Subject: [PATCH 02/33] Extract receive files usecase --- .../arkbuilders/drop/app/TransferManager.kt | 57 +++++++------------ .../arkbuilders/drop/app/di/TmpEntryPoint.kt | 2 + .../app/domain/usecase/ReceiveFilesUseCase.kt | 53 +++++++++++++++++ 3 files changed, 74 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt diff --git a/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt b/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt index 74f26c1..d38455b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt @@ -66,44 +66,25 @@ class TransferManager @Inject constructor( suspend fun receiveFiles(ticket: String, confirmation: UByte): ReceiveFilesBubble? = withContext(Dispatchers.IO) { - try { - Log.d(TAG, "Starting file receive with ticket: $ticket") - - val profile = profileManager.getCurrentProfile() - val receiverProfile = ReceiverProfile( - name = profile.name.ifEmpty { "Anonymous" }, - avatarB64 = profile.avatarB64.takeIf { it.isNotEmpty() } - ) - - val request = ReceiveFilesRequest( - ticket = ticket, - confirmation = confirmation, - profile = receiverProfile, - config = ReceiverConfig( - chunkSize = 1024u * 512u, - parallelStreams = 4u, - ) - ) - - // Create and subscribe to bubble - val bubble = receiveFiles(request) - currentReceiveBubble = bubble - - // Set up subscriber - receiveSubscriber = ReceiveFilesSubscriberImpl().also { subscriber -> - bubble.subscribe(subscriber) - } - - // Start receiving - bubble.start() - - Log.d(TAG, "Receive bubble created and started") - bubble - - } catch (e: Exception) { - Log.e(TAG, "Error starting file receive", e) - null - } + val entryPoint = EntryPointAccessors.fromApplication( + context.applicationContext, + TmpEntryPoint::class.java + ) + val receiveFilesUseCase = entryPoint.receiveFilesUseCase() + + receiveFilesUseCase.invoke(ticket, confirmation).fold( + onSuccess = { bubble -> + currentReceiveBubble = bubble + receiveSubscriber = ReceiveFilesSubscriberImpl().also { subscriber -> + bubble.subscribe(subscriber) + } + bubble.start() + return@withContext bubble + }, + onFailure = { + return@withContext null + }, + ) } suspend fun saveReceivedFiles(): List = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt index 64f2474..8f94c21 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt @@ -3,10 +3,12 @@ package dev.arkbuilders.drop.app.di import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase @EntryPoint @InstallIn(SingletonComponent::class) interface TmpEntryPoint { fun sendFilesUseCase(): SendFilesUseCase + fun receiveFilesUseCase(): ReceiveFilesUseCase } \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt new file mode 100644 index 0000000..0676f45 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt @@ -0,0 +1,53 @@ +package dev.arkbuilders.drop.app.domain.usecase + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.arkbuilders.drop.ReceiveFilesBubble +import dev.arkbuilders.drop.ReceiveFilesRequest +import dev.arkbuilders.drop.ReceiverConfig +import dev.arkbuilders.drop.ReceiverProfile +import dev.arkbuilders.drop.app.ProfileManager +import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.receiveFiles +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +class ReceiveFilesUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val profileManager: ProfileManager, + private val resourcesHelper: ResourcesHelper, +) { + suspend operator fun invoke( + ticket: String, + confirmation: UByte + ): Result = withContext(Dispatchers.IO) { + runCatching { + Timber.d("Starting file receive with ticket: $ticket") + + val profile = profileManager.getCurrentProfile() + val receiverProfile = ReceiverProfile( + name = profile.name.ifEmpty { "Anonymous" }, + avatarB64 = profile.avatarB64.takeIf { it.isNotEmpty() } + ) + + val request = ReceiveFilesRequest( + ticket = ticket, + confirmation = confirmation, + profile = receiverProfile, + config = ReceiverConfig( + chunkSize = 1024u * 512u, + parallelStreams = 4u, + ) + ) + + val bubble = receiveFiles(request) + + Timber.d("Receive bubble created and started") + bubble + }.onFailure { + Timber.e("Error starting file receive ${it.message}") + } + } +} \ No newline at end of file From b2ad1ab74a256d2b3bf9eded00a98c732923dfac Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Fri, 26 Sep 2025 17:15:02 +0600 Subject: [PATCH 03/33] Refactor ProfileRepository --- .../dev/arkbuilders/drop/app/MainActivity.kt | 11 ++- .../arkbuilders/drop/app/ProfileManager.kt | 98 ------------------- .../arkbuilders/drop/app/TransferManager.kt | 3 +- .../data/datasource/ProfileLocalDataSource.kt | 70 +++++++++++++ .../drop/app/data/model/UserProfileDto.kt | 10 ++ .../app/data/repository/ProfileRepoImpl.kt | 41 ++++++++ .../dev/arkbuilders/drop/app/di/AppModule.kt | 11 +-- .../drop/app/domain/model/UserProfile.kt | 7 ++ .../drop/app/domain/repository/ProfileRepo.kt | 13 +++ .../app/domain/usecase/ReceiveFilesUseCase.kt | 6 +- .../app/domain/usecase/SendFilesUseCase.kt | 6 +- .../dev/arkbuilders/drop/app/ui/Home/Home.kt | 9 +- .../drop/app/ui/profile/EditProfile.kt | 18 ++-- .../app/ui/profile/EditProfileEnhanced.kt | 17 ++-- 14 files changed, 178 insertions(+), 142 deletions(-) delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ProfileManager.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt diff --git a/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt index 37c4d36..e2cb89c 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt @@ -16,6 +16,7 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink import dagger.hilt.android.AndroidEntryPoint import dev.arkbuilders.drop.app.data.HistoryRepository +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.navigation.DropDestination import dev.arkbuilders.drop.app.ui.history.History import dev.arkbuilders.drop.app.ui.Home.Home @@ -32,7 +33,7 @@ class MainActivity : ComponentActivity() { lateinit var transferManager: TransferManager @Inject - lateinit var profileManager: ProfileManager + lateinit var profileRepo: ProfileRepo @Inject lateinit var historyRepository: HistoryRepository @@ -50,7 +51,7 @@ class MainActivity : ComponentActivity() { modifier = Modifier .padding(innerPadding), transferManager = transferManager, - profileManager = profileManager, + profileRepo = profileRepo, historyRepository = historyRepository ) } @@ -64,7 +65,7 @@ fun DropNavigation( modifier: Modifier = Modifier, navController: NavHostController = rememberNavController(), transferManager: TransferManager, - profileManager: ProfileManager, + profileRepo: ProfileRepo, historyRepository: HistoryRepository ) { NavHost( @@ -75,7 +76,7 @@ fun DropNavigation( composable(DropDestination.Home.route) { Home( navController = navController, - profileManager = profileManager, + profileRepo = profileRepo, historyRepository = historyRepository ) } @@ -107,7 +108,7 @@ fun DropNavigation( composable(DropDestination.EditProfile.route) { EditProfileEnhanced( navController = navController, - profileManager = profileManager + profileRepo = profileRepo, ) } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ProfileManager.kt b/app/src/main/java/dev/arkbuilders/drop/app/ProfileManager.kt deleted file mode 100644 index b8bdea9..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/ProfileManager.kt +++ /dev/null @@ -1,98 +0,0 @@ -package dev.arkbuilders.drop.app - -import android.content.Context -import android.content.SharedPreferences -import dagger.hilt.android.qualifiers.ApplicationContext -import dev.arkbuilders.drop.app.ui.profile.AvatarUtils -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import javax.inject.Inject -import javax.inject.Singleton - -@Serializable -data class UserProfile( - val name: String = "", - val avatarB64: String = "", - val avatarId: String = "avatar_00" -) - -@Singleton -class ProfileManager @Inject constructor( - @ApplicationContext private val context: Context -) { - companion object { - private const val PREFS_NAME = "drop_profile" - private const val KEY_PROFILE = "user_profile" - } - - private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - private val json = Json { ignoreUnknownKeys = true } - - private val _profile = MutableStateFlow(loadProfile()) - val profile: StateFlow = _profile.asStateFlow() - - private fun loadProfile(): UserProfile { - val profileJson = prefs.getString(KEY_PROFILE, null) - return if (profileJson != null) { - try { - json.decodeFromString(profileJson) - } catch (e: Exception) { - createDefaultProfile() - } - } else { - createDefaultProfile() - } - } - - private fun createDefaultProfile(): UserProfile { - val defaultProfile = UserProfile( - name = "Anonymous", - avatarB64 = AvatarUtils.getDefaultAvatarBase64(context, "avatar_00"), - avatarId = "avatar_00" - ) - saveProfile(defaultProfile) - return defaultProfile - } - - fun updateProfile(profile: UserProfile) { - _profile.value = profile - saveProfile(profile) - } - - fun updateName(name: String) { - val updatedProfile = _profile.value.copy(name = name) - updateProfile(updatedProfile) - } - - fun updateAvatar(avatarId: String) { - val avatarB64 = AvatarUtils.getDefaultAvatarBase64(context, avatarId) - val updatedProfile = _profile.value.copy( - avatarId = avatarId, - avatarB64 = avatarB64 - ) - updateProfile(updatedProfile) - } - - fun updateCustomAvatar(base64: String) { - val updatedProfile = _profile.value.copy( - avatarB64 = base64, - avatarId = "custom" - ) - updateProfile(updatedProfile) - } - - private fun saveProfile(profile: UserProfile) { - try { - val profileJson = json.encodeToString(profile) - prefs.edit().putString(KEY_PROFILE, profileJson).apply() - } catch (e: Exception) { - // Handle serialization error - } - } - - fun getCurrentProfile(): UserProfile = _profile.value -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt b/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt index d38455b..898f94a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt @@ -15,6 +15,7 @@ import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl import dev.arkbuilders.drop.app.data.TransferStatus import dev.arkbuilders.drop.app.di.TmpEntryPoint +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext @@ -26,7 +27,7 @@ import javax.inject.Singleton @Singleton class TransferManager @Inject constructor( @ApplicationContext private val context: Context, - private val profileManager: ProfileManager, + private val profileRepo: ProfileRepo, private val historyRepository: HistoryRepository ) { companion object { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt new file mode 100644 index 0000000..73a4684 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt @@ -0,0 +1,70 @@ +package dev.arkbuilders.drop.app.data.datasource + +import android.content.Context +import android.content.SharedPreferences +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.arkbuilders.drop.app.ui.profile.AvatarUtils +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import javax.inject.Inject +import androidx.core.content.edit +import dev.arkbuilders.drop.app.data.model.UserProfileDto +import dev.arkbuilders.drop.app.domain.model.UserProfile + +class ProfileLocalDataSource @Inject constructor( + @ApplicationContext private val context: Context +) { + private val prefs: SharedPreferences = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + private val json = Json { ignoreUnknownKeys = true } + + fun loadProfile(): UserProfile { + val profileJson = prefs.getString(KEY_PROFILE, null) + if (profileJson == null) { + return createDefaultProfile() + } + + return runCatching { + json + .decodeFromString(profileJson) + .toDomain() + }.getOrElse { + createDefaultProfile() + } + } + + private fun createDefaultProfile(): UserProfile { + val default = UserProfile( + name = "Anonymous", + avatarB64 = AvatarUtils.getDefaultAvatarBase64(context, "avatar_00"), + avatarId = "avatar_00" + ) + saveProfile(default) + return default + } + + fun saveProfile(profile: UserProfile) { + runCatching { + val profileJson = json.encodeToString(profile.toDto()) + prefs.edit { putString(KEY_PROFILE, profileJson) } + } + } + + companion object { + private const val PREFS_NAME = "drop_profile" + private const val KEY_PROFILE = "user_profile" + } +} + +private fun UserProfileDto.toDomain() = UserProfile( + name = name, + avatarB64 = avatarB64, + avatarId = avatarId, +) + +private fun UserProfile.toDto() = UserProfileDto( + name = name, + avatarB64 = avatarB64, + avatarId = avatarId, +) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt new file mode 100644 index 0000000..cb6079c --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt @@ -0,0 +1,10 @@ +package dev.arkbuilders.drop.app.data.model + +import kotlinx.serialization.Serializable + +@Serializable +data class UserProfileDto( + val name: String, + val avatarB64: String, + val avatarId: String +) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt new file mode 100644 index 0000000..fd0ed69 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt @@ -0,0 +1,41 @@ +package dev.arkbuilders.drop.app.data.repository + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.arkbuilders.drop.app.data.datasource.ProfileLocalDataSource +import dev.arkbuilders.drop.app.domain.model.UserProfile +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo +import dev.arkbuilders.drop.app.ui.profile.AvatarUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ProfileRepoImpl @Inject constructor( + private val localDataSource: ProfileLocalDataSource, + @ApplicationContext private val context: Context +) : ProfileRepo { + + private val _profile = MutableStateFlow(localDataSource.loadProfile()) + override val profile: StateFlow = _profile.asStateFlow() + + override fun updateProfile(profile: UserProfile) { + _profile.value = profile + localDataSource.saveProfile(profile) + } + + override fun updateName(name: String) { + updateProfile(_profile.value.copy(name = name)) + } + + override fun updateAvatar(avatarId: String) { + val avatarB64 = AvatarUtils.getDefaultAvatarBase64(context, avatarId) + updateProfile(_profile.value.copy(avatarId = avatarId, avatarB64 = avatarB64)) + } + + override fun updateCustomAvatar(base64: String) { + updateProfile(_profile.value.copy(avatarId = "custom", avatarB64 = base64)) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index 12b81fb..a190c83 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -7,10 +7,11 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import dev.arkbuilders.drop.app.TransferManager -import dev.arkbuilders.drop.app.ProfileManager import dev.arkbuilders.drop.app.data.HistoryRepository import dev.arkbuilders.drop.app.data.ResourcesHelperImpl +import dev.arkbuilders.drop.app.data.repository.ProfileRepoImpl import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import javax.inject.Singleton @Module @@ -19,18 +20,16 @@ object AppModule { @Provides @Singleton - fun provideProfileManager(@ApplicationContext context: Context): ProfileManager { - return ProfileManager(context) - } + fun provideProfileRepo(impl: ProfileRepoImpl): ProfileRepo = impl @Provides @Singleton fun provideTransferManager( @ApplicationContext context: Context, - profileManager: ProfileManager, + profileRepo: ProfileRepo, historyRepository: HistoryRepository ): TransferManager { - return TransferManager(context, profileManager, historyRepository) + return TransferManager(context, profileRepo, historyRepository) } @Provides diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt new file mode 100644 index 0000000..55ebb8b --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt @@ -0,0 +1,7 @@ +package dev.arkbuilders.drop.app.domain.model + +data class UserProfile( + val name: String, + val avatarB64: String, + val avatarId: String, +) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt new file mode 100644 index 0000000..c933a8f --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt @@ -0,0 +1,13 @@ +package dev.arkbuilders.drop.app.domain.repository + +import dev.arkbuilders.drop.app.domain.model.UserProfile +import kotlinx.coroutines.flow.StateFlow + +interface ProfileRepo { + val profile: StateFlow + fun getCurrentProfile() = profile.value + fun updateProfile(profile: UserProfile) + fun updateName(name: String) + fun updateAvatar(avatarId: String) + fun updateCustomAvatar(base64: String) +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt index 0676f45..24b250a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt @@ -6,8 +6,8 @@ import dev.arkbuilders.drop.ReceiveFilesBubble import dev.arkbuilders.drop.ReceiveFilesRequest import dev.arkbuilders.drop.ReceiverConfig import dev.arkbuilders.drop.ReceiverProfile -import dev.arkbuilders.drop.app.ProfileManager import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.receiveFiles import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -16,7 +16,7 @@ import javax.inject.Inject class ReceiveFilesUseCase @Inject constructor( @ApplicationContext private val context: Context, - private val profileManager: ProfileManager, + private val profileRepo: ProfileRepo, private val resourcesHelper: ResourcesHelper, ) { suspend operator fun invoke( @@ -26,7 +26,7 @@ class ReceiveFilesUseCase @Inject constructor( runCatching { Timber.d("Starting file receive with ticket: $ticket") - val profile = profileManager.getCurrentProfile() + val profile = profileRepo.getCurrentProfile() val receiverProfile = ReceiverProfile( name = profile.name.ifEmpty { "Anonymous" }, avatarB64 = profile.avatarB64.takeIf { it.isNotEmpty() } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt index 9bbf327..a17e660 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt @@ -8,9 +8,9 @@ import dev.arkbuilders.drop.SendFilesRequest import dev.arkbuilders.drop.SenderConfig import dev.arkbuilders.drop.SenderFile import dev.arkbuilders.drop.SenderProfile -import dev.arkbuilders.drop.app.ProfileManager import dev.arkbuilders.drop.app.data.SenderFileDataImpl import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.sendFiles import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -19,7 +19,7 @@ import javax.inject.Inject class SendFilesUseCase @Inject constructor( @ApplicationContext private val context: Context, - private val profileManager: ProfileManager, + private val profileRepo: ProfileRepo, private val resourcesHelper: ResourcesHelper, ) { suspend operator fun invoke( @@ -28,7 +28,7 @@ class SendFilesUseCase @Inject constructor( runCatching { Timber.d("Starting file send for ${fileUris.size} files") - val profile = profileManager.getCurrentProfile() + val profile = profileRepo.getCurrentProfile() val senderProfile = SenderProfile( name = profile.name.ifEmpty { "Anonymous" }, avatarB64 = profile.avatarB64.takeIf { it.isNotEmpty() } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt index 75df214..a0cf937 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt @@ -43,12 +43,11 @@ import compose.icons.tablericons.ArrowUpCircle import compose.icons.tablericons.CloudDownload import compose.icons.tablericons.CloudUpload import compose.icons.tablericons.History -import dev.arkbuilders.drop.app.ProfileManager -import dev.arkbuilders.drop.app.R -import dev.arkbuilders.drop.app.UserProfile import dev.arkbuilders.drop.app.data.HistoryRepository import dev.arkbuilders.drop.app.data.TransferHistoryItem import dev.arkbuilders.drop.app.data.TransferType +import dev.arkbuilders.drop.app.domain.model.UserProfile +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.navigation.DropDestination import dev.arkbuilders.drop.app.ui.components.DropButton import dev.arkbuilders.drop.app.ui.components.DropButtonSize @@ -69,10 +68,10 @@ import java.util.Locale @Composable fun Home( navController: NavController, - profileManager: ProfileManager, + profileRepo: ProfileRepo, historyRepository: HistoryRepository, ) { - val profile = remember { profileManager.getCurrentProfile() } + val profile = remember { profileRepo.getCurrentProfile() } val historyItems by historyRepository.historyItems.collectAsState() var logoScale by remember { mutableStateOf(0f) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfile.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfile.kt index 6542d27..991b424 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfile.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfile.kt @@ -1,8 +1,6 @@ package dev.arkbuilders.drop.app.ui.profile import android.net.Uri -import android.widget.ScrollView -import android.widget.Scroller import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement @@ -17,15 +15,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check @@ -55,16 +49,16 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.Camera -import dev.arkbuilders.drop.app.ProfileManager +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo @OptIn(ExperimentalMaterial3Api::class) @Composable fun EditProfile( navController: NavController, - profileManager: ProfileManager + profileRepo: ProfileRepo ) { val context = LocalContext.current - val profile by profileManager.profile.collectAsState() + val profile by profileRepo.profile.collectAsState() var name by remember { mutableStateOf(profile.name) } var selectedAvatarId by remember { mutableStateOf(profile.avatarId) } var customAvatarBase64 by remember { mutableStateOf(null) } @@ -110,11 +104,11 @@ fun EditProfile( // Save button FilledTonalButton( onClick = { - profileManager.updateName(name) + profileRepo.updateName(name) if (selectedAvatarId == "custom" && customAvatarBase64 != null) { - profileManager.updateCustomAvatar(customAvatarBase64!!) + profileRepo.updateCustomAvatar(customAvatarBase64!!) } else { - profileManager.updateAvatar(selectedAvatarId) + profileRepo.updateAvatar(selectedAvatarId) } navController.navigateUp() } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileEnhanced.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileEnhanced.kt index baf5fc0..1d5f9d9 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileEnhanced.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileEnhanced.kt @@ -1,7 +1,6 @@ package dev.arkbuilders.drop.app.ui.profile import android.net.Uri -import android.text.Layout import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility @@ -81,13 +80,13 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.unit.dp import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.Camera import compose.icons.tablericons.Check -import dev.arkbuilders.drop.app.ProfileManager +import dev.arkbuilders.drop.app.domain.model.UserProfile +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.ui.components.DropButton import dev.arkbuilders.drop.app.ui.components.DropButtonSize import dev.arkbuilders.drop.app.ui.components.DropButtonVariant @@ -115,7 +114,7 @@ data class EditProfileUiState( @Composable fun EditProfileEnhanced( navController: NavController, - profileManager: ProfileManager + profileRepo: ProfileRepo ) { val context = LocalContext.current val haptic = LocalHapticFeedback.current @@ -123,7 +122,7 @@ fun EditProfileEnhanced( val keyboardController = LocalSoftwareKeyboardController.current // State management - val profile by profileManager.profile.collectAsState() + val profile by profileRepo.profile.collectAsState() var uiState by remember { mutableStateOf(EditProfileUiState()) } var name by rememberSaveable { mutableStateOf(profile.name) } var selectedAvatarId by rememberSaveable { mutableStateOf(profile.avatarId) } @@ -194,11 +193,11 @@ fun EditProfileEnhanced( val saveProfile = { if (canSave) { uiState = uiState.copy(isSaving = true, error = null) - profileManager.updateName(name.trim()) + profileRepo.updateName(name.trim()) if (selectedAvatarId == "custom" && customAvatarBase64 != null) { - profileManager.updateCustomAvatar(customAvatarBase64!!) + profileRepo.updateCustomAvatar(customAvatarBase64!!) } else { - profileManager.updateAvatar(selectedAvatarId) + profileRepo.updateAvatar(selectedAvatarId) } haptic.performHapticFeedback(HapticFeedbackType.LongPress) @@ -409,7 +408,7 @@ private fun ProfilePreviewSection( name: String, selectedAvatarId: String, customAvatarBase64: String?, - profile: dev.arkbuilders.drop.app.UserProfile, + profile: UserProfile, onNameChange: (String) -> Unit, nameError: String?, nameFocusRequester: FocusRequester, From 1801ad2cf5fb72ca81c1de3bbe2d0c35740fd296 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Mon, 29 Sep 2025 02:06:25 +0600 Subject: [PATCH 04/33] Add Room database, refactor HistoryRepository --- app/build.gradle.kts | 23 ++- .../1.json | 78 +++++++++ .../dev/arkbuilders/drop/app/MainActivity.kt | 12 +- .../arkbuilders/drop/app/TransferManager.kt | 14 +- .../drop/app/data/HistoryRepository.kt | 148 ------------------ .../TransferHistoryItemLocalDataSource.kt | 50 ++++++ .../arkbuilders/drop/app/data/db/Database.kt | 32 ++++ .../app/data/db/dao/TransferHistoryItemDao.kt | 27 ++++ .../db/entity/TransferHistoryItemEntity.kt | 21 +++ .../OffsetDateTimeTypeConverter.kt | 16 ++ .../TransferHistoryItemRepositoryImpl.kt | 68 ++++++++ .../dev/arkbuilders/drop/app/di/AppModule.kt | 25 ++- .../app/domain/model/TransferHistoryItem.kt | 15 ++ .../drop/app/domain/model/TransferStatus.kt | 9 ++ .../TransferHistoryItemRepository.kt | 31 ++++ .../dev/arkbuilders/drop/app/ui/Home/Home.kt | 33 ++-- .../drop/app/ui/history/History.kt | 37 +++-- build.gradle.kts | 3 +- gradle/libs.versions.toml | 7 + 19 files changed, 447 insertions(+), 202 deletions(-) create mode 100644 app/schemas/dev.arkbuilders.drop.app.data.db.Database/1.json delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/HistoryRepository.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferStatus.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 29b7ec8..2be228c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,8 +4,9 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) - id("com.google.dagger.hilt.android") version "2.56.2" + id("com.google.dagger.hilt.android") version "2.57.1" id("com.github.triplet.play") version "3.10.1" + alias(libs.plugins.ksp) } android { @@ -30,8 +31,10 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - setProperty("archivesBaseName", "ark-drop") + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } } buildTypes { @@ -143,17 +146,13 @@ dependencies { implementation(libs.timber) - // DAGGER SETUP - implementation("com.google.dagger:hilt-android:2.56.2") - kapt("com.google.dagger:hilt-compiler:2.56.2") + implementation(libs.room.runtime) + implementation(libs.room.ktx) + ksp(libs.room.compiler) - // For instrumentation tests - androidTestImplementation("com.google.dagger:hilt-android-testing:2.56.2") - kaptAndroidTest("com.google.dagger:hilt-compiler:2.56.2") - - // For local unit tests - testImplementation("com.google.dagger:hilt-android-testing:2.56.2") - kaptTest("com.google.dagger:hilt-compiler:2.56.2") + // DAGGER SETUP + implementation("com.google.dagger:hilt-android:2.57.1") + ksp("com.google.dagger:hilt-compiler:2.57.1") // EXTRA ICONS implementation("br.com.devsrsouza.compose.icons:simple-icons:1.1.0") diff --git a/app/schemas/dev.arkbuilders.drop.app.data.db.Database/1.json b/app/schemas/dev.arkbuilders.drop.app.data.db.Database/1.json new file mode 100644 index 0000000..ebd7541 --- /dev/null +++ b/app/schemas/dev.arkbuilders.drop.app.data.db.Database/1.json @@ -0,0 +1,78 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "5fa9ecea78d66afdf6f2502843c1f445", + "entities": [ + { + "tableName": "TransferHistoryItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fileName` TEXT NOT NULL, `fileSize` INTEGER NOT NULL, `type` TEXT NOT NULL, `timestamp` TEXT NOT NULL, `status` TEXT NOT NULL, `peerName` TEXT NOT NULL, `peerAvatar` TEXT, `fileCount` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "peerName", + "columnName": "peerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "peerAvatar", + "columnName": "peerAvatar", + "affinity": "TEXT" + }, + { + "fieldPath": "fileCount", + "columnName": "fileCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5fa9ecea78d66afdf6f2502843c1f445')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt index e2cb89c..fdc58d0 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt @@ -15,8 +15,8 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink import dagger.hilt.android.AndroidEntryPoint -import dev.arkbuilders.drop.app.data.HistoryRepository import dev.arkbuilders.drop.app.domain.repository.ProfileRepo +import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import dev.arkbuilders.drop.app.navigation.DropDestination import dev.arkbuilders.drop.app.ui.history.History import dev.arkbuilders.drop.app.ui.Home.Home @@ -36,7 +36,7 @@ class MainActivity : ComponentActivity() { lateinit var profileRepo: ProfileRepo @Inject - lateinit var historyRepository: HistoryRepository + lateinit var transferHistoryItemRepository: TransferHistoryItemRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -52,7 +52,7 @@ class MainActivity : ComponentActivity() { .padding(innerPadding), transferManager = transferManager, profileRepo = profileRepo, - historyRepository = historyRepository + transferHistoryItemRepository = transferHistoryItemRepository ) } } @@ -66,7 +66,7 @@ fun DropNavigation( navController: NavHostController = rememberNavController(), transferManager: TransferManager, profileRepo: ProfileRepo, - historyRepository: HistoryRepository + transferHistoryItemRepository: TransferHistoryItemRepository, ) { NavHost( navController = navController, @@ -77,7 +77,7 @@ fun DropNavigation( Home( navController = navController, profileRepo = profileRepo, - historyRepository = historyRepository + transferHistoryItemRepository = transferHistoryItemRepository ) } composable(DropDestination.Send.route) { @@ -102,7 +102,7 @@ fun DropNavigation( composable(DropDestination.History.route) { History( navController = navController, - historyRepository = historyRepository + transferHistoryItemRepository = transferHistoryItemRepository ) } composable(DropDestination.EditProfile.route) { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt b/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt index 898f94a..e35ad36 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt @@ -10,12 +10,12 @@ import android.util.Log import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.* -import dev.arkbuilders.drop.app.data.HistoryRepository import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl -import dev.arkbuilders.drop.app.data.TransferStatus import dev.arkbuilders.drop.app.di.TmpEntryPoint +import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.repository.ProfileRepo +import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext @@ -28,7 +28,7 @@ import javax.inject.Singleton class TransferManager @Inject constructor( @ApplicationContext private val context: Context, private val profileRepo: ProfileRepo, - private val historyRepository: HistoryRepository + private val transferHistoryRepository: TransferHistoryItemRepository, ) { companion object { private const val TAG = "TransferManager" @@ -113,7 +113,7 @@ class TransferManager @Inject constructor( val totalSize = completeFiles.sumOf { it.second.size.toLong() } val firstFileName = savedFiles.firstOrNull()?.name ?: "Unknown" - historyRepository.addReceivedTransfer( + transferHistoryRepository.addReceivedTransfer( fileName = firstFileName, fileSize = totalSize, peerName = senderName, @@ -131,7 +131,7 @@ class TransferManager @Inject constructor( val senderName = progress?.senderName ?: "Unknown" val senderAvatar = progress?.senderAvatar - historyRepository.addReceivedTransfer( + transferHistoryRepository.addReceivedTransfer( fileName = "Transfer failed", fileSize = 0L, peerName = senderName, @@ -144,7 +144,7 @@ class TransferManager @Inject constructor( savedFiles } - fun recordSendCompletion(fileUris: List) { + suspend fun recordSendCompletion(fileUris: List) { try { val progress = sendSubscriber?.progress?.value val receiverName = progress?.receiverName ?: "Unknown" @@ -165,7 +165,7 @@ class TransferManager @Inject constructor( val firstFileName = getFileName(fileUris.firstOrNull()) ?: "Unknown" - historyRepository.addSentTransfer( + transferHistoryRepository.addSentTransfer( fileName = firstFileName, fileSize = totalSize, peerName = receiverName, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/HistoryRepository.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/HistoryRepository.kt deleted file mode 100644 index 9ce02e6..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/HistoryRepository.kt +++ /dev/null @@ -1,148 +0,0 @@ -package dev.arkbuilders.drop.app.data - -import android.content.Context -import android.content.SharedPreferences -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import javax.inject.Inject -import javax.inject.Singleton - -@Serializable -data class TransferHistoryItem( - val id: String, - val fileName: String, - val fileSize: Long, - val type: TransferType, - val timestamp: Long, - val status: TransferStatus, - val peerName: String = "Unknown", - val peerAvatar: String? = null, - val fileCount: Int = 1 -) - -@Serializable -enum class TransferType { - SENT, RECEIVED -} - -@Serializable -enum class TransferStatus { - COMPLETED, FAILED, CANCELLED -} - -@Singleton -class HistoryRepository @Inject constructor( - @ApplicationContext private val context: Context -) { - companion object { - private const val PREFS_NAME = "drop_history" - private const val KEY_HISTORY = "transfer_history" - private const val MAX_HISTORY_ITEMS = 100 - } - - private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - private val json = Json { ignoreUnknownKeys = true } - - private val _historyItems = MutableStateFlow(loadHistory()) - val historyItems: StateFlow> = _historyItems.asStateFlow() - - private fun loadHistory(): List { - return try { - val historyJson = prefs.getString(KEY_HISTORY, null) - if (historyJson != null) { - json.decodeFromString>(historyJson) - } else { - emptyList() - } - } catch (e: Exception) { - emptyList() - } - } - - private fun saveHistory(items: List) { - try { - val historyJson = json.encodeToString(items) - prefs.edit().putString(KEY_HISTORY, historyJson).apply() - _historyItems.value = items - } catch (e: Exception) { - // Handle serialization error - } - } - - fun addSentTransfer( - fileName: String, - fileSize: Long, - peerName: String, - peerAvatar: String?, - fileCount: Int = 1, - status: TransferStatus = TransferStatus.COMPLETED - ) { - val newItem = TransferHistoryItem( - id = generateId(), - fileName = if (fileCount > 1) "$fileName and ${fileCount - 1} more" else fileName, - fileSize = fileSize, - type = TransferType.SENT, - timestamp = System.currentTimeMillis(), - status = status, - peerName = peerName, - peerAvatar = peerAvatar, - fileCount = fileCount - ) - - addHistoryItem(newItem) - } - - fun addReceivedTransfer( - fileName: String, - fileSize: Long, - peerName: String, - peerAvatar: String?, - fileCount: Int = 1, - status: TransferStatus = TransferStatus.COMPLETED - ) { - val newItem = TransferHistoryItem( - id = generateId(), - fileName = if (fileCount > 1) "$fileName and ${fileCount - 1} more" else fileName, - fileSize = fileSize, - type = TransferType.RECEIVED, - timestamp = System.currentTimeMillis(), - status = status, - peerName = peerName, - peerAvatar = peerAvatar, - fileCount = fileCount - ) - - addHistoryItem(newItem) - } - - private fun addHistoryItem(item: TransferHistoryItem) { - val currentItems = _historyItems.value.toMutableList() - currentItems.add(0, item) // Add to beginning (most recent first) - - // Keep only the most recent items - if (currentItems.size > MAX_HISTORY_ITEMS) { - currentItems.removeAt(currentItems.size - 1) - } - - saveHistory(currentItems) - } - - fun deleteHistoryItem(itemId: String) { - val currentItems = _historyItems.value.toMutableList() - currentItems.removeAll { it.id == itemId } - saveHistory(currentItems) - } - - fun clearHistory() { - saveHistory(emptyList()) - } - - private fun generateId(): String { - return "${System.currentTimeMillis()}_${(0..999).random()}" - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt new file mode 100644 index 0000000..bd27f23 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt @@ -0,0 +1,50 @@ +package dev.arkbuilders.drop.app.data.datasource + +import dev.arkbuilders.drop.app.data.db.dao.TransferHistoryItemDao +import dev.arkbuilders.drop.app.data.db.entity.TransferHistoryItemEntity +import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class TransferHistoryItemLocalDataSource @Inject constructor( + private val dao: TransferHistoryItemDao +) { + fun flow(): Flow> = + dao.getAll().map { list -> list.map { it.toDomain() } } + + suspend fun add(item: TransferHistoryItem): Long = + dao.insert(item.toEntity()) + + suspend fun addAll(items: List) = + dao.insertAll(items.map { it.toEntity() }) + + suspend fun delete(itemId: Long) = + dao.deleteById(itemId) + + suspend fun clear() = dao.clear() +} + +fun TransferHistoryItemEntity.toDomain() = TransferHistoryItem( + id = id, + fileName = fileName, + fileSize = fileSize, + type = type, + timestamp = timestamp, + status = status, + peerName = peerName, + peerAvatar = peerAvatar, + fileCount = fileCount +) + +fun TransferHistoryItem.toEntity() = TransferHistoryItemEntity( + id = id, + fileName = fileName, + fileSize = fileSize, + type = type, + timestamp = timestamp, + status = status, + peerName = peerName, + peerAvatar = peerAvatar, + fileCount = fileCount +) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt new file mode 100644 index 0000000..4e69e8c --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt @@ -0,0 +1,32 @@ +package dev.arkbuilders.drop.app.data.db + +import android.content.Context +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import dev.arkbuilders.drop.app.data.db.dao.TransferHistoryItemDao +import dev.arkbuilders.drop.app.data.db.entity.TransferHistoryItemEntity +import dev.arkbuilders.drop.app.data.db.typeconverters.OffsetDateTimeTypeConverter + +@androidx.room.Database( + entities = [ + TransferHistoryItemEntity::class, + ], + version = 1, + exportSchema = true +) +@TypeConverters( + OffsetDateTimeTypeConverter::class, +) +abstract class Database : RoomDatabase() { + + abstract fun transferHistoryDao(): TransferHistoryItemDao + + companion object { + const val DB_NAME = "arkdrop.db" + + fun build(ctx: Context) = + Room.databaseBuilder(ctx, Database::class.java, DB_NAME) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt new file mode 100644 index 0000000..3cc14f2 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt @@ -0,0 +1,27 @@ +package dev.arkbuilders.drop.app.data.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Upsert +import dev.arkbuilders.drop.app.data.db.entity.TransferHistoryItemEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface TransferHistoryItemDao { + @Query("SELECT * FROM TransferHistoryItemEntity ORDER BY timestamp DESC") + fun getAll(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(item: TransferHistoryItemEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(items: List) + + @Query("DELETE FROM TransferHistoryItemEntity WHERE id = :itemId") + suspend fun deleteById(itemId: Long) + + @Query("DELETE FROM TransferHistoryItemEntity") + suspend fun clear() +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt new file mode 100644 index 0000000..b84283a --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt @@ -0,0 +1,21 @@ +package dev.arkbuilders.drop.app.data.db.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +import dev.arkbuilders.drop.app.domain.model.TransferStatus +import dev.arkbuilders.drop.app.domain.model.TransferType +import java.time.OffsetDateTime + +@Entity +data class TransferHistoryItemEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val fileName: String, + val fileSize: Long, + val type: TransferType, + val timestamp: OffsetDateTime, + val status: TransferStatus, + val peerName: String, + val peerAvatar: String?, + val fileCount: Int, +) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt new file mode 100644 index 0000000..f333bdd --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt @@ -0,0 +1,16 @@ +package dev.arkbuilders.drop.app.data.db.typeconverters + +import androidx.room.TypeConverter +import java.time.OffsetDateTime + +object OffsetDateTimeTypeConverter { + @TypeConverter + fun fromOffsetDateTime(date: OffsetDateTime): String = date.toString() + + @TypeConverter + fun toOffsetDateTime(dateString: String): OffsetDateTime { + val date = OffsetDateTime.parse(dateString) + val offset = OffsetDateTime.now().offset + return date.withOffsetSameInstant(offset) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt new file mode 100644 index 0000000..abc4d35 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt @@ -0,0 +1,68 @@ +package dev.arkbuilders.drop.app.data.repository + +import dev.arkbuilders.drop.app.data.datasource.TransferHistoryItemLocalDataSource +import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.domain.model.TransferStatus +import dev.arkbuilders.drop.app.domain.model.TransferType +import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import kotlinx.coroutines.flow.Flow +import java.time.OffsetDateTime +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TransferHistoryItemRepositoryImpl @Inject constructor( + private val localSource: TransferHistoryItemLocalDataSource +): TransferHistoryItemRepository { + override val historyItems: Flow> = localSource.flow() + + override suspend fun addSentTransfer( + fileName: String, + fileSize: Long, + peerName: String, + peerAvatar: String?, + fileCount: Int, + status: TransferStatus, + ) { + val newItem = TransferHistoryItem( + fileName = if (fileCount > 1) "$fileName and ${fileCount - 1} more" else fileName, + fileSize = fileSize, + type = TransferType.SENT, + timestamp = OffsetDateTime.now(), + status = status, + peerName = peerName, + peerAvatar = peerAvatar, + fileCount = fileCount + ) + localSource.add(newItem) + } + + override suspend fun addReceivedTransfer( + fileName: String, + fileSize: Long, + peerName: String, + peerAvatar: String?, + fileCount: Int, + status: TransferStatus, + ) { + val newItem = TransferHistoryItem( + fileName = if (fileCount > 1) "$fileName and ${fileCount - 1} more" else fileName, + fileSize = fileSize, + type = TransferType.RECEIVED, + timestamp = OffsetDateTime.now(), + status = status, + peerName = peerName, + peerAvatar = peerAvatar, + fileCount = fileCount + ) + localSource.add(newItem) + } + + override suspend fun deleteHistoryItem(itemId: Long) { + localSource.delete(itemId) + } + + override suspend fun clearHistory() { + localSource.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index a190c83..cab9ae2 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -7,11 +7,13 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import dev.arkbuilders.drop.app.TransferManager -import dev.arkbuilders.drop.app.data.HistoryRepository import dev.arkbuilders.drop.app.data.ResourcesHelperImpl +import dev.arkbuilders.drop.app.data.db.Database import dev.arkbuilders.drop.app.data.repository.ProfileRepoImpl +import dev.arkbuilders.drop.app.data.repository.TransferHistoryItemRepositoryImpl import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.domain.repository.ProfileRepo +import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import javax.inject.Singleton @Module @@ -27,9 +29,9 @@ object AppModule { fun provideTransferManager( @ApplicationContext context: Context, profileRepo: ProfileRepo, - historyRepository: HistoryRepository + transferHistoryItemRepository: TransferHistoryItemRepository ): TransferManager { - return TransferManager(context, profileRepo, historyRepository) + return TransferManager(context, profileRepo, transferHistoryItemRepository) } @Provides @@ -37,4 +39,21 @@ object AppModule { fun provideResourcesHelper( impl: ResourcesHelperImpl ): ResourcesHelper = impl + + @Provides + @Singleton + fun provideDatabase( + @ApplicationContext context: Context, + ) = Database.build(context) + + @Provides + fun provideTransferHistoryItemDao( + db: Database, + ) = db.transferHistoryDao() + + @Provides + @Singleton + fun provideTransferHistoryItemRepository( + impl: TransferHistoryItemRepositoryImpl + ): TransferHistoryItemRepository = impl } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt new file mode 100644 index 0000000..9bf0fd4 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt @@ -0,0 +1,15 @@ +package dev.arkbuilders.drop.app.domain.model + +import java.time.OffsetDateTime + +data class TransferHistoryItem( + val id: Long = 0, + val fileName: String, + val fileSize: Long, + val type: TransferType, + val timestamp: OffsetDateTime, + val status: TransferStatus, + val peerName: String, + val peerAvatar: String?, + val fileCount: Int, +) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferStatus.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferStatus.kt new file mode 100644 index 0000000..01b8fcc --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferStatus.kt @@ -0,0 +1,9 @@ +package dev.arkbuilders.drop.app.domain.model + +enum class TransferStatus { + COMPLETED, FAILED, CANCELLED +} + +enum class TransferType { + SENT, RECEIVED +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt new file mode 100644 index 0000000..fb4af6b --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt @@ -0,0 +1,31 @@ +package dev.arkbuilders.drop.app.domain.repository + +import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.domain.model.TransferStatus +import kotlinx.coroutines.flow.Flow + +interface TransferHistoryItemRepository { + val historyItems: Flow> + + suspend fun addSentTransfer( + fileName: String, + fileSize: Long, + peerName: String, + peerAvatar: String?, + fileCount: Int = 1, + status: TransferStatus = TransferStatus.COMPLETED + ) + + suspend fun addReceivedTransfer( + fileName: String, + fileSize: Long, + peerName: String, + peerAvatar: String?, + fileCount: Int = 1, + status: TransferStatus = TransferStatus.COMPLETED + ) + + suspend fun deleteHistoryItem(itemId: Long) + + suspend fun clearHistory() +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt index a0cf937..33ab306 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.ArrowDownCircle @@ -43,11 +44,11 @@ import compose.icons.tablericons.ArrowUpCircle import compose.icons.tablericons.CloudDownload import compose.icons.tablericons.CloudUpload import compose.icons.tablericons.History -import dev.arkbuilders.drop.app.data.HistoryRepository -import dev.arkbuilders.drop.app.data.TransferHistoryItem -import dev.arkbuilders.drop.app.data.TransferType +import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.model.UserProfile import dev.arkbuilders.drop.app.domain.repository.ProfileRepo +import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import dev.arkbuilders.drop.app.navigation.DropDestination import dev.arkbuilders.drop.app.ui.components.DropButton import dev.arkbuilders.drop.app.ui.components.DropButtonSize @@ -62,6 +63,9 @@ import dev.arkbuilders.drop.app.ui.profile.AvatarUtils import dev.arkbuilders.drop.app.ui.theme.DesignTokens import kotlinx.coroutines.delay import java.text.SimpleDateFormat +import java.time.Duration +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter import java.util.Date import java.util.Locale @@ -69,10 +73,11 @@ import java.util.Locale fun Home( navController: NavController, profileRepo: ProfileRepo, - historyRepository: HistoryRepository, + transferHistoryItemRepository: TransferHistoryItemRepository, ) { val profile = remember { profileRepo.getCurrentProfile() } - val historyItems by historyRepository.historyItems.collectAsState() + val historyItems by transferHistoryItemRepository + .historyItems.collectAsStateWithLifecycle(emptyList()) var logoScale by remember { mutableStateOf(0f) } @@ -369,15 +374,17 @@ private fun EnhancedTransferHistoryCard(item: TransferHistoryItem) { } } -private fun formatTimestamp(timestamp: Long): String { - val now = System.currentTimeMillis() - val diff = now - timestamp +private fun formatTimestamp(timestamp: OffsetDateTime): String { + val now = OffsetDateTime.now() + val diff = Duration.between(timestamp, now) return when { - diff < 60000 -> "Just now" - diff < 3600000 -> "${diff / 60000}m ago" - diff < 86400000 -> "${diff / 3600000}h ago" - diff < 604800000 -> "${diff / 86400000}d ago" - else -> SimpleDateFormat("MMM dd", Locale.getDefault()).format(Date(timestamp)) + diff.toMinutes() < 1 -> "Just now" + diff.toHours() < 1 -> "${diff.toMinutes()}m ago" + diff.toDays() < 1 -> "${diff.toHours()}h ago" + diff.toDays() < 7 -> "${diff.toDays()}d ago" + else -> timestamp.format( + DateTimeFormatter.ofPattern("MMM dd", Locale.getDefault()) + ) } } \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt index 614e40f..e8ae5de 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -45,12 +46,16 @@ import compose.icons.tablericons.ClearAll import compose.icons.tablericons.FileDownload import compose.icons.tablericons.FileUpload import compose.icons.tablericons.History -import dev.arkbuilders.drop.app.data.HistoryRepository -import dev.arkbuilders.drop.app.data.TransferHistoryItem -import dev.arkbuilders.drop.app.data.TransferStatus -import dev.arkbuilders.drop.app.data.TransferType +import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.domain.model.TransferStatus +import dev.arkbuilders.drop.app.domain.model.TransferType +import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import dev.arkbuilders.drop.app.ui.profile.AvatarUtils +import kotlinx.coroutines.launch import java.text.SimpleDateFormat +import java.time.Duration +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter import java.util.Date import java.util.Locale @@ -58,10 +63,12 @@ import java.util.Locale @Composable fun History( navController: NavController, - historyRepository: HistoryRepository + transferHistoryItemRepository: TransferHistoryItemRepository ) { - val historyItems by historyRepository.historyItems.collectAsState() + val historyItems by transferHistoryItemRepository + .historyItems.collectAsState(emptyList()) var showClearDialog by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() Column( modifier = Modifier @@ -140,7 +147,9 @@ fun History( HistoryItemCard( item = item, onDelete = { - historyRepository.deleteHistoryItem(item.id) + scope.launch { + transferHistoryItemRepository.deleteHistoryItem(item.id) + } } ) } @@ -168,7 +177,9 @@ fun History( confirmButton = { Button( onClick = { - historyRepository.clearHistory() + scope.launch { + transferHistoryItemRepository.clearHistory() + } showClearDialog = false }, colors = ButtonDefaults.buttonColors( @@ -381,15 +392,17 @@ private fun formatFileSize(bytes: Long): String { return "%.1f GB".format(gb) } -private fun formatTimestamp(timestamp: Long): String { - val now = System.currentTimeMillis() - val diff = now - timestamp +private fun formatTimestamp(timestamp: OffsetDateTime): String { + val now = OffsetDateTime.now() + val diff = Duration.between(timestamp, now).toMillis() return when { diff < 60000 -> "Just now" diff < 3600000 -> "${diff / 60000}m ago" diff < 86400000 -> "${diff / 3600000}h ago" diff < 604800000 -> "${diff / 86400000}d ago" - else -> SimpleDateFormat("MMM dd", Locale.getDefault()).format(Date(timestamp)) + else -> timestamp.format( + DateTimeFormatter.ofPattern("MMM dd", Locale.getDefault()) + ) } } diff --git a/build.gradle.kts b/build.gradle.kts index e2bd9f7..fc1978c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false - id("com.google.dagger.hilt.android") version "2.56.2" apply false + id("com.google.dagger.hilt.android") version "2.57.1" apply false id("com.github.triplet.play") version "3.10.1" apply false + alias(libs.plugins.ksp) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 562a19d..0261bec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] agp = "8.12.1" kotlin = "2.2.0" +ksp = "2.2.0-2.0.2" coreKtx = "1.15.0" junit = "4.13.2" junitVersion = "1.2.1" @@ -19,6 +20,7 @@ cameraView = "1.4.0" mlkitBarcodeScanning = "17.3.0" accompanistPermissions = "0.36.0" timber = "5.0.1" +room = "2.8.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -46,8 +48,13 @@ androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycl androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraView" } mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkitBarcodeScanning" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistPermissions" } +room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +room-testing = { module = "androidx.room:room-testing", version.ref = "room" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"} From 4dd846f2f07d29761d92ddab3e39c7ab68921d80 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Fri, 3 Oct 2025 03:04:48 +0600 Subject: [PATCH 05/33] Add viewmodels --- app/build.gradle.kts | 4 + .../app/data/helper/PermissionsHelperImpl.kt | 20 + .../dev/arkbuilders/drop/app/di/AppModule.kt | 8 + .../drop/app/domain/PermissionsHelper.kt | 5 + .../drop/app/domain/model/UserProfile.kt | 6 +- .../dev/arkbuilders/drop/app/ui/Home/Home.kt | 19 +- .../drop/app/ui/Home/HomeViewModel.kt | 54 + .../drop/app/ui/history/History.kt | 55 +- .../drop/app/ui/history/HistoryViewModel.kt | 82 + .../drop/app/ui/receive/Receive.kt | 1417 +---------------- .../drop/app/ui/receive/ReceiveViewModel.kt | 391 +++++ .../receive/components/ReceiveCompleteCard.kt | 166 ++ .../ui/receive/components/ReceiveErrorCard.kt | 124 ++ .../receive/components/ReceiveLoadingCard.kt | 56 + .../components/ReceiveManualInputCard.kt | 164 ++ .../ReceivePermissionRequestCard.kt | 137 ++ .../receive/components/ReceiveProgressCard.kt | 247 +++ .../components/ReceiveQRCodeScannedCard.kt | 124 ++ .../components/ReceiveReadyToScanCard.kt | 149 ++ .../receive/components/ReceiveScanningCard.kt | 250 +++ gradle/libs.versions.toml | 3 + 21 files changed, 2086 insertions(+), 1395 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/Home/HomeViewModel.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/history/HistoryViewModel.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveCompleteCard.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveErrorCard.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveLoadingCard.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveManualInputCard.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceivePermissionRequestCard.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveQRCodeScannedCard.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveReadyToScanCard.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveScanningCard.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2be228c..540cdf3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -150,8 +150,12 @@ dependencies { implementation(libs.room.ktx) ksp(libs.room.compiler) + implementation(libs.orbit.compose) + implementation(libs.orbit.viewmodel) + // DAGGER SETUP implementation("com.google.dagger:hilt-android:2.57.1") + implementation("androidx.hilt:hilt-navigation-compose:1.3.0") ksp("com.google.dagger:hilt-compiler:2.57.1") // EXTRA ICONS diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt new file mode 100644 index 0000000..ccb739d --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt @@ -0,0 +1,20 @@ +package dev.arkbuilders.drop.app.data.helper + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.arkbuilders.drop.app.domain.PermissionsHelper +import javax.inject.Inject + +class PermissionsHelperImpl @Inject constructor( + @ApplicationContext private val ctx: Context, +): PermissionsHelper { + override fun isCameraGranted(): Boolean { + return ContextCompat.checkSelfPermission( + ctx, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index cab9ae2..a427cb6 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -9,8 +9,10 @@ import dagger.hilt.components.SingletonComponent import dev.arkbuilders.drop.app.TransferManager import dev.arkbuilders.drop.app.data.ResourcesHelperImpl import dev.arkbuilders.drop.app.data.db.Database +import dev.arkbuilders.drop.app.data.helper.PermissionsHelperImpl import dev.arkbuilders.drop.app.data.repository.ProfileRepoImpl import dev.arkbuilders.drop.app.data.repository.TransferHistoryItemRepositoryImpl +import dev.arkbuilders.drop.app.domain.PermissionsHelper import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository @@ -56,4 +58,10 @@ object AppModule { fun provideTransferHistoryItemRepository( impl: TransferHistoryItemRepositoryImpl ): TransferHistoryItemRepository = impl + + @Provides + @Singleton + fun providePermissionHelper( + impl: PermissionsHelperImpl + ): PermissionsHelper = impl } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt new file mode 100644 index 0000000..a9ce310 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt @@ -0,0 +1,5 @@ +package dev.arkbuilders.drop.app.domain + +interface PermissionsHelper { + fun isCameraGranted(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt index 55ebb8b..b6a98d5 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt @@ -4,4 +4,8 @@ data class UserProfile( val name: String, val avatarB64: String, val avatarId: String, -) \ No newline at end of file +) { + companion object { + fun empty() = UserProfile("", "", "") + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt index 33ab306..91813ce 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -36,7 +35,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.ArrowDownCircle @@ -62,11 +61,10 @@ import dev.arkbuilders.drop.app.ui.components.EmptyState import dev.arkbuilders.drop.app.ui.profile.AvatarUtils import dev.arkbuilders.drop.app.ui.theme.DesignTokens import kotlinx.coroutines.delay -import java.text.SimpleDateFormat +import org.orbitmvi.orbit.compose.collectAsState import java.time.Duration import java.time.OffsetDateTime import java.time.format.DateTimeFormatter -import java.util.Date import java.util.Locale @Composable @@ -75,9 +73,8 @@ fun Home( profileRepo: ProfileRepo, transferHistoryItemRepository: TransferHistoryItemRepository, ) { - val profile = remember { profileRepo.getCurrentProfile() } - val historyItems by transferHistoryItemRepository - .historyItems.collectAsStateWithLifecycle(emptyList()) + val viewModel: HomeViewModel = hiltViewModel() + val state by viewModel.collectAsState() var logoScale by remember { mutableStateOf(0f) } @@ -107,7 +104,7 @@ fun Home( HeaderSection( logoScale = animatedLogoScale, onProfileClick = { navController.navigate(DropDestination.EditProfile.route) }, - profile = profile + profile = state.profile ) } @@ -121,11 +118,11 @@ fun Home( // Recent Transfers Section item { - if (historyItems.isNotEmpty()) { + if (state.historyItems.isNotEmpty()) { RecentTransfersSection( - historyItems = historyItems.take(5), + historyItems = state.historyItems.take(5), onViewAllClick = { navController.navigate(DropDestination.History.route) }, - showViewAll = historyItems.isNotEmpty() + showViewAll = state.historyItems.isNotEmpty() ) } else { EmptyTransfersSection() diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/HomeViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/HomeViewModel.kt new file mode 100644 index 0000000..e98829a --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/HomeViewModel.kt @@ -0,0 +1,54 @@ +package dev.arkbuilders.drop.app.ui.Home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.domain.model.UserProfile +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo +import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +data class HomeScreenState( + val historyItems: List, + val profile: UserProfile, +) + +sealed class HomeScreenEffect + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val historyItemRepository: TransferHistoryItemRepository, + private val profileRepo: ProfileRepo, +): ViewModel(), ContainerHost{ + override val container: Container = + container(HomeScreenState(emptyList(), UserProfile.empty())) + + init { + historyItemRepository.historyItems.onEach { + intent { + reduce { + state.copy(historyItems = it) + } + } + }.launchIn(viewModelScope) + + intent { + val items = historyItemRepository.historyItems.first() + val profile = profileRepo.getCurrentProfile() + reduce { + state.copy( + historyItems = items, + profile = profile, + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt index e8ae5de..9722532 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt @@ -29,17 +29,14 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.ClearAll @@ -51,12 +48,10 @@ import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import dev.arkbuilders.drop.app.ui.profile.AvatarUtils -import kotlinx.coroutines.launch -import java.text.SimpleDateFormat +import org.orbitmvi.orbit.compose.collectAsState import java.time.Duration import java.time.OffsetDateTime import java.time.format.DateTimeFormatter -import java.util.Date import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @@ -65,10 +60,9 @@ fun History( navController: NavController, transferHistoryItemRepository: TransferHistoryItemRepository ) { - val historyItems by transferHistoryItemRepository - .historyItems.collectAsState(emptyList()) - var showClearDialog by remember { mutableStateOf(false) } + val viewModel: HistoryViewModel = hiltViewModel() val scope = rememberCoroutineScope() + val state by viewModel.collectAsState() Column( modifier = Modifier @@ -90,9 +84,8 @@ fun History( modifier = Modifier.weight(1f) ) - // Clear all button - if (historyItems.isNotEmpty()) { - IconButton(onClick = { showClearDialog = true }) { + if (state.historyItems.isNotEmpty()) { + IconButton(onClick = { viewModel.onShowClearDialog() }) { Icon( TablerIcons.ClearAll, contentDescription = "Clear All", @@ -104,7 +97,7 @@ fun History( Spacer(modifier = Modifier.height(16.dp)) - if (historyItems.isEmpty()) { + if (state.historyItems.isEmpty()) { // Empty state Card( modifier = Modifier.fillMaxWidth(), @@ -143,13 +136,14 @@ fun History( LazyColumn( verticalArrangement = Arrangement.spacedBy(12.dp) ) { - items(historyItems) { item -> + items(state.historyItems) { item -> HistoryItemCard( + state = state, item = item, + onShowDeleteDialog = viewModel::onShowDeleteDialog, + onDismissDeleteDialog = viewModel::onDismissDeleteDialog, onDelete = { - scope.launch { - transferHistoryItemRepository.deleteHistoryItem(item.id) - } + viewModel.onDelete(item.id) } ) } @@ -158,9 +152,9 @@ fun History( } // Clear all confirmation dialog - if (showClearDialog) { + if (state.showClearDialog) { AlertDialog( - onDismissRequest = { showClearDialog = false }, + onDismissRequest = { viewModel.onDismissClearDialog() }, title = { Text( "Clear All History", @@ -177,10 +171,7 @@ fun History( confirmButton = { Button( onClick = { - scope.launch { - transferHistoryItemRepository.clearHistory() - } - showClearDialog = false + viewModel.onClear() }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error @@ -190,7 +181,7 @@ fun History( } }, dismissButton = { - TextButton(onClick = { showClearDialog = false }) { + TextButton(onClick = { viewModel.onClear() }) { Text("Cancel", fontWeight = FontWeight.Medium) } }, @@ -201,11 +192,12 @@ fun History( @Composable private fun HistoryItemCard( + state: HistoryScreenState, item: TransferHistoryItem, + onShowDeleteDialog: () -> Unit, + onDismissDeleteDialog: () -> Unit, onDelete: () -> Unit ) { - var showDeleteDialog by remember { mutableStateOf(false) } - ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), @@ -332,7 +324,7 @@ private fun HistoryItemCard( } // Delete button - IconButton(onClick = { showDeleteDialog = true }) { + IconButton(onClick = { onShowDeleteDialog() }) { Icon( Icons.Default.Delete, contentDescription = "Delete", @@ -343,9 +335,9 @@ private fun HistoryItemCard( } // Delete confirmation dialog - if (showDeleteDialog) { + if (state.showDeleteDialog) { AlertDialog( - onDismissRequest = { showDeleteDialog = false }, + onDismissRequest = { onDismissDeleteDialog() }, title = { Text( "Delete History Item", @@ -363,7 +355,6 @@ private fun HistoryItemCard( Button( onClick = { onDelete() - showDeleteDialog = false }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error @@ -373,7 +364,7 @@ private fun HistoryItemCard( } }, dismissButton = { - TextButton(onClick = { showDeleteDialog = false }) { + TextButton(onClick = { onDismissDeleteDialog() }) { Text("Cancel", fontWeight = FontWeight.Medium) } }, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/HistoryViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/history/HistoryViewModel.kt new file mode 100644 index 0000000..6f29d0b --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/history/HistoryViewModel.kt @@ -0,0 +1,82 @@ +package dev.arkbuilders.drop.app.ui.history + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +data class HistoryScreenState( + val historyItems: List, + val showClearDialog: Boolean, + val showDeleteDialog: Boolean, +) + +sealed class HistoryScreenEffect + +@HiltViewModel +class HistoryViewModel @Inject constructor( + private val historyItemRepository: TransferHistoryItemRepository, +) : ViewModel(), ContainerHost { + override val container: Container = container( + HistoryScreenState( + historyItems = emptyList(), + showClearDialog = false, + showDeleteDialog = false + ) + ) + + init { + historyItemRepository.historyItems.onEach { items -> + intent { + reduce { + state.copy(historyItems = items) + } + } + }.launchIn(viewModelScope) + } + + fun onShowClearDialog() = intent { + reduce { + state.copy(showClearDialog = true) + } + } + + fun onClear() = intent { + historyItemRepository.clearHistory() + reduce { + state.copy(showClearDialog = false) + } + } + + fun onDismissClearDialog() = intent { + reduce { + state.copy(showClearDialog = false) + } + } + + fun onShowDeleteDialog() = intent { + reduce { + state.copy(showDeleteDialog = true) + } + } + + fun onDelete(id: Long) = intent { + historyItemRepository.deleteHistoryItem(id) + reduce { + state.copy(showDeleteDialog = false) + } + } + + fun onDismissDeleteDialog() = intent { + reduce { + state.copy(showDeleteDialog = false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt index 91504a0..feb2bfe 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt @@ -3,13 +3,6 @@ package dev.arkbuilders.drop.app.ui.receive import android.Manifest import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.camera.core.CameraSelector -import androidx.camera.core.ExperimentalGetImage -import androidx.camera.core.ImageAnalysis -import androidx.camera.core.ImageProxy -import androidx.camera.core.Preview -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.camera.view.PreviewView import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Spring @@ -24,50 +17,31 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -84,36 +58,28 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat -import androidx.core.net.toUri +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import com.google.mlkit.vision.barcode.BarcodeScanning -import com.google.mlkit.vision.barcode.common.Barcode -import com.google.mlkit.vision.common.InputImage -import compose.icons.TablerIcons -import compose.icons.tablericons.AlertCircle -import compose.icons.tablericons.ArrowForward -import compose.icons.tablericons.Camera -import compose.icons.tablericons.CameraOff -import compose.icons.tablericons.Qrcode -import dev.arkbuilders.drop.app.R import dev.arkbuilders.drop.app.TransferManager -import dev.arkbuilders.drop.app.data.ReceiveFileInfo -import dev.arkbuilders.drop.app.data.ReceivingProgress -import dev.arkbuilders.drop.app.ui.profile.AvatarUtils +import dev.arkbuilders.drop.app.ui.components.DropLogoIcon +import dev.arkbuilders.drop.app.ui.receive.components.ReceiveCompleteCard +import dev.arkbuilders.drop.app.ui.receive.components.ReceiveErrorCard +import dev.arkbuilders.drop.app.ui.receive.components.ReceiveLoadingCard +import dev.arkbuilders.drop.app.ui.receive.components.ReceiveManualInputCard +import dev.arkbuilders.drop.app.ui.receive.components.ReceivePermissionRequestCard +import dev.arkbuilders.drop.app.ui.receive.components.ReceiveProgressCard +import dev.arkbuilders.drop.app.ui.receive.components.ReceiveQRCodeScannedCard +import dev.arkbuilders.drop.app.ui.receive.components.ReceiveReadyToScanCard +import dev.arkbuilders.drop.app.ui.receive.components.ReceiveScanningCard import dev.arkbuilders.drop.app.ui.theme.DesignTokens import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect sealed class ReceiveError(val message: String, val isRecoverable: Boolean = true) { object CameraPermissionDenied : @@ -160,130 +126,41 @@ sealed class ReceiveWorkflowState { @Composable fun Receive( navController: NavController, - transferManager: TransferManager + transferManager: TransferManager, ) { + val viewModel: ReceiveViewModel = hiltViewModel() val context = LocalContext.current val scope = rememberCoroutineScope() val clipboardManager = LocalClipboardManager.current val keyboardController = LocalSoftwareKeyboardController.current - - var workflowState by remember { - val receiveProgress = transferManager.receiveProgress?.value - if (receiveProgress != null && receiveProgress.isConnected) { - mutableStateOf(ReceiveWorkflowState.Receiving) - } else { - mutableStateOf(ReceiveWorkflowState.Initial) - } - } - var scannedTicket by remember { mutableStateOf(null) } - var scannedConfirmation by remember { mutableStateOf(null) } - var manualInputText by remember { mutableStateOf("") } - var manualInputError by remember { mutableStateOf(null) } - var receivedFiles by remember { mutableStateOf>(emptyList()) } - var showSuccessAnimation by remember { mutableStateOf(false) } - val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA) - val receiveProgress by (transferManager.receiveProgress?.collectAsState() - ?: remember { mutableStateOf(null) }) - val requestPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> - if (isGranted) { - workflowState = ReceiveWorkflowState.Scanning - } else { - workflowState = ReceiveWorkflowState.Error(ReceiveError.CameraPermissionDenied) - } + viewModel.onCameraPermissionGranted(isGranted) } - // Function to parse manual input - fun parseManualInput(input: String): Pair? { - return try { - val trimmed = input.trim() - val parts = trimmed.split(" ") - - if (parts.size == 2) { - val ticket = parts[0].trim() - val confirmation = parts[1].trim().toUByte() - - if (ticket.isNotEmpty()) { - Pair(ticket, confirmation) - } else { - null - } - } else { - null + val uiState by viewModel.collectAsState() + viewModel.collectSideEffect { effect -> + when (effect) { + ReceiveScreenEffect.HideKeyboard -> { + keyboardController?.hide() } - } catch (e: Exception) { - null - } - } - - // Function to handle manual input submission - fun handleManualInputSubmit() { - val parsed = parseManualInput(manualInputText) - if (parsed != null) { - scannedTicket = parsed.first - scannedConfirmation = parsed.second - workflowState = ReceiveWorkflowState.QRCodeScanned - manualInputError = null - keyboardController?.hide() - } else { - manualInputError = "Invalid format. Please enter: ticket confirmation" - } - } - - // Function to paste from clipboard - fun pasteFromClipboard() { - val clipText = clipboardManager.getText()?.text - if (!clipText.isNullOrEmpty()) { - manualInputText = clipText - manualInputError = null - } - } - - // Monitor receive progress and handle completion - LaunchedEffect(receiveProgress) { - receiveProgress?.let { progress -> - // Check if we're connected and have files - if (progress.isConnected && progress.files.isNotEmpty()) { - // Check if all files are complete - val allFilesComplete = progress.files.all { file -> - val fileProgress = progress.fileProgress[file.id] - fileProgress?.isComplete == true - } - - if (allFilesComplete) { - // Small delay to ensure UI updates are visible - delay(1000) - try { - val savedFiles = transferManager.saveReceivedFiles() - if (savedFiles.isNotEmpty()) { - receivedFiles = savedFiles.map { it.name } - workflowState = ReceiveWorkflowState.Success - showSuccessAnimation = true - } else { - workflowState = ReceiveWorkflowState.Error(ReceiveError.NoFilesReceived) - } - } catch (e: Exception) { - workflowState = ReceiveWorkflowState.Error( - when { - e.message?.contains("storage", ignoreCase = true) == true -> - ReceiveError.StorageError - - e.message?.contains("network", ignoreCase = true) == true -> - ReceiveError.NetworkError + ReceiveScreenEffect.NavigateBack -> { + navController.navigateUp() + } + ReceiveScreenEffect.RequestCameraPermission -> { + requestPermissionLauncher.launch(Manifest.permission.CAMERA) + } + ReceiveScreenEffect.ShowSuccessAnimation -> { - else -> ReceiveError.UnknownError - } - ) - } - } } } } + var showSuccessAnimation by remember { mutableStateOf(false) } + LaunchedEffect(showSuccessAnimation) { if (showSuccessAnimation) { delay(3000) @@ -428,7 +305,7 @@ fun Receive( } AnimatedContent( - targetState = workflowState, + targetState = uiState.workflowState, transitionSpec = { slideInVertically( initialOffsetY = { it / 3 }, @@ -444,120 +321,75 @@ fun Receive( when (state) { is ReceiveWorkflowState.Initial -> { if (!cameraPermissionState.status.isGranted) { - PermissionRequestCard( + ReceivePermissionRequestCard( onRequestPermission = { - workflowState = ReceiveWorkflowState.RequestingPermission - requestPermissionLauncher.launch(Manifest.permission.CAMERA) + viewModel.onRequestCameraPermission() }, onEnterManually = { - workflowState = ReceiveWorkflowState.ManualInput + viewModel.onEnterManually() } ) } else { - ReadyToScanCard( - onStartScanning = { workflowState = ReceiveWorkflowState.Scanning }, - onEnterManually = { workflowState = ReceiveWorkflowState.ManualInput } + ReceiveReadyToScanCard( + onStartScanning = { viewModel.onStartScanning() }, + onEnterManually = { viewModel.onEnterManually() } ) } } is ReceiveWorkflowState.RequestingPermission -> { - LoadingCard(message = "Requesting camera permission...") + ReceiveLoadingCard(message = "Requesting camera permission...") } is ReceiveWorkflowState.Scanning -> { - ScanningCard( + ReceiveScanningCard( onQRCodeScanned = { ticket, confirmation -> - scannedTicket = ticket - scannedConfirmation = confirmation - workflowState = ReceiveWorkflowState.QRCodeScanned + viewModel.onQrCodeScanned(ticket, confirmation) }, onError = { error -> - workflowState = ReceiveWorkflowState.Error(error) + viewModel.onError(error) }, - onStopScanning = { workflowState = ReceiveWorkflowState.Initial }, - onEnterManually = { workflowState = ReceiveWorkflowState.ManualInput } + onStopScanning = { viewModel.onStopScanning() }, + onEnterManually = { viewModel.onEnterManually() } ) } is ReceiveWorkflowState.ManualInput -> { - ManualInputCard( - inputText = manualInputText, + ReceiveManualInputCard( + inputText = uiState.manualInputText, onInputChange = { - manualInputText = it - manualInputError = null + viewModel.onManualInputChanged(it) }, - inputError = manualInputError, - onPasteFromClipboard = { pasteFromClipboard() }, - onSubmit = { handleManualInputSubmit() }, + inputError = uiState.manualInputError, + onPasteFromClipboard = { viewModel.onPasteFromClipboard(clipboardManager.getText()?.text) }, + onSubmit = { viewModel.handleManualInputSubmit() }, onCancel = { - workflowState = ReceiveWorkflowState.Initial - manualInputText = "" - manualInputError = null - keyboardController?.hide() + viewModel.onCancelManualInput() } ) } is ReceiveWorkflowState.QRCodeScanned -> { - QRCodeScannedCard( + ReceiveQRCodeScannedCard( onAccept = { - scope.launch { - try { - workflowState = ReceiveWorkflowState.Connecting - val ticket = scannedTicket!! - val confirmation = scannedConfirmation!! - - val bubble = transferManager.receiveFiles(ticket, confirmation) - if (bubble != null) { - workflowState = ReceiveWorkflowState.Receiving - } else { - workflowState = - ReceiveWorkflowState.Error(ReceiveError.ConnectionFailed) - } - } catch (e: Exception) { - workflowState = ReceiveWorkflowState.Error( - when { - e.message?.contains( - "network", - ignoreCase = true - ) == true -> ReceiveError.NetworkError - - else -> ReceiveError.ConnectionFailed - } - ) - } - } + viewModel.onAccept() }, onScanAgain = { - scannedTicket = null - scannedConfirmation = null - manualInputText = "" - manualInputError = null - if (cameraPermissionState.status.isGranted) { - workflowState = ReceiveWorkflowState.Scanning - } else { - workflowState = ReceiveWorkflowState.ManualInput - } + viewModel.onScanAgain() } ) } is ReceiveWorkflowState.Connecting -> { - LoadingCard(message = "Connecting to sender...") + ReceiveLoadingCard(message = "Connecting to sender...") } is ReceiveWorkflowState.Receiving -> { - receiveProgress?.let { progress -> - ReceivingCard( + uiState.receiveProgress?.let { progress -> + ReceiveProgressCard( progress = progress, onCancel = { - transferManager.cancelReceive() - workflowState = ReceiveWorkflowState.Initial - scannedTicket = null - scannedConfirmation = null - manualInputText = "" - manualInputError = null + viewModel.onCancelReceiving() } ) } @@ -565,46 +397,34 @@ fun Receive( is ReceiveWorkflowState.Success -> { if (!showSuccessAnimation) { - TransferCompleteCard( - receivedFiles = receivedFiles, + ReceiveCompleteCard( + receivedFiles = uiState.receivedFiles, onReceiveMore = { - receivedFiles = emptyList() - workflowState = ReceiveWorkflowState.Initial - scannedTicket = null - scannedConfirmation = null - manualInputText = "" - manualInputError = null - transferManager.cancelReceive() + viewModel.onReceiveMore() }, onDone = { - transferManager.cancelReceive() - navController.navigateUp() + viewModel.onDone() } ) } } is ReceiveWorkflowState.Error -> { - ErrorCard( + ReceiveErrorCard( error = state.error, onRetry = { - workflowState = ReceiveWorkflowState.Initial - scannedTicket = null - scannedConfirmation = null - manualInputText = "" - manualInputError = null + viewModel.onErrorRetry() }, onDismiss = { - transferManager.cancelReceive() - navController.navigateUp() - + viewModel.onErrorDismiss() } ) } } } - if (workflowState !is ReceiveWorkflowState.Success && workflowState !is ReceiveWorkflowState.Error) { + if (uiState.workflowState !is ReceiveWorkflowState.Success + && uiState.workflowState !is ReceiveWorkflowState.Error) { Spacer(modifier = Modifier.weight(1f)) Card( @@ -655,1108 +475,3 @@ fun Receive( } } } - -@Composable -private fun PermissionRequestCard( - onRequestPermission: () -> Unit, - onEnterManually: () -> Unit -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) - ) { - Column( - modifier = Modifier.padding(DesignTokens.Spacing.xxl), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .size(80.dp) - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - Icon( - TablerIcons.Camera, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - - Text( - text = "Camera Permission Required", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) - - Text( - text = "We need camera access to scan QR codes for receiving files. Your privacy is protected - we only use the camera for QR code scanning.", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) - - Button( - onClick = onRequestPermission, - modifier = Modifier - .fillMaxWidth() - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) - ) { - Text( - "Grant Permission", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) - - Text( - text = "Or", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) - - OutlinedButton( - onClick = onEnterManually, - modifier = Modifier - .fillMaxWidth() - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) - ) { - Icon( - TablerIcons.ArrowForward, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) - Text( - "Enter Code Manually", - fontWeight = FontWeight.Medium - ) - } - } - } -} - -@Composable -private fun ReadyToScanCard( - onStartScanning: () -> Unit, - onEnterManually: () -> Unit -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) - ) { - Column( - modifier = Modifier.padding(DesignTokens.Spacing.xxl), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .size(80.dp) - .background( - brush = Brush.radialGradient( - colors = listOf( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) - ) - ), - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - Icon( - TablerIcons.Qrcode, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.onPrimary - ) - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - - Text( - text = "Ready to Receive", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) - - Text( - text = "Scan the QR code from the sender's device or enter the transfer code manually to start receiving files securely.", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) - - Button( - onClick = onStartScanning, - modifier = Modifier - .fillMaxWidth() - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon( - TablerIcons.Camera, - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) - Text( - "Start Scanning", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) - - Text( - text = "Or", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) - - OutlinedButton( - onClick = onEnterManually, - modifier = Modifier - .fillMaxWidth() - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) - ) { - Icon( - TablerIcons.ArrowForward, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) - Text( - "Enter Code Manually", - fontWeight = FontWeight.Medium - ) - } - } - } -} - -@Composable -private fun LoadingCard( - message: String -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.md) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(DesignTokens.Spacing.xl), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - strokeWidth = 3.dp, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.lg)) - Text( - text = message, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - } - } -} - -@Composable -private fun ScanningCard( - onQRCodeScanned: (String, UByte) -> Unit, - onError: (ReceiveError) -> Unit, - onStopScanning: () -> Unit, - onEnterManually: () -> Unit -) { - Column { - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f), - shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) - ) { - QRCodeScanner( - onQRCodeScanned = onQRCodeScanned, - onError = onError - ) - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) - - Text( - text = "Point your camera at the QR code", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onSurface - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) - ) { - OutlinedButton( - onClick = onStopScanning, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) - ) { - Icon( - TablerIcons.CameraOff, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) - Text( - "Stop Scanning", - fontWeight = FontWeight.Medium - ) - } - - Button( - onClick = onEnterManually, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) - ) { - Icon( - TablerIcons.ArrowForward, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) - Text( - "Enter Code", - fontWeight = FontWeight.Medium - ) - } - } - } -} - -@Composable -private fun ManualInputCard( - inputText: String, - onInputChange: (String) -> Unit, - inputError: String?, - onPasteFromClipboard: () -> Unit, - onSubmit: () -> Unit, - onCancel: () -> Unit -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) - ) { - Column( - modifier = Modifier.padding(DesignTokens.Spacing.xxl), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .size(80.dp) - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - Icon( - TablerIcons.ArrowForward, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - - Text( - text = "Enter Transfer Code", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) - - Text( - text = "Paste or type the transfer code from the sender in the format: ticket confirmation", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) - - OutlinedTextField( - value = inputText, - onValueChange = onInputChange, - label = { Text("Transfer Code") }, - placeholder = { Text("ticket confirmation") }, - modifier = Modifier.fillMaxWidth(), - isError = inputError != null, - supportingText = inputError?.let { error -> - { Text(error, color = MaterialTheme.colorScheme.error) } - }, - trailingIcon = { - IconButton(onClick = onPasteFromClipboard) { - Icon( - TablerIcons.ArrowForward, - contentDescription = "Paste", - tint = MaterialTheme.colorScheme.primary - ) - } - }, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { onSubmit() } - ), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) - ) { - OutlinedButton( - onClick = onCancel, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) - ) { - Text( - "Cancel", - fontWeight = FontWeight.Medium - ) - } - - Button( - onClick = onSubmit, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ), - enabled = inputText.trim().isNotEmpty() - ) { - Text( - "Connect", - fontWeight = FontWeight.SemiBold - ) - } - } - } - } -} - -@Composable -private fun QRCodeScannedCard( - onAccept: () -> Unit, - onScanAgain: () -> Unit -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) - ) { - Column( - modifier = Modifier.padding(DesignTokens.Spacing.xxl), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .size(80.dp) - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Default.CheckCircle, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.primary - ) - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - - Text( - text = "Code Received!", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) - - Text( - text = "Ready to receive files from sender. Tap Accept to start the transfer.", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2 - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) - ) { - OutlinedButton( - onClick = onScanAgain, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) - ) { - Text( - "Try Again", - fontWeight = FontWeight.Medium - ) - } - - Button( - onClick = onAccept, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) - ) { - Text( - "Accept", - fontWeight = FontWeight.SemiBold - ) - } - } - } - } -} - -@Composable -private fun ReceivingCard( - progress: ReceivingProgress, - onCancel: () -> Unit -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) - ) { - Column( - modifier = Modifier.padding(DesignTokens.Spacing.lg) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = if (progress.isConnected) "Receiving Files..." else "Connecting...", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) - - Surface( - onClick = onCancel, - shape = CircleShape, - color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), - modifier = Modifier.size(40.dp) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() - ) { - Icon( - Icons.Default.Close, - contentDescription = "Cancel", - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(20.dp) - ) - } - } - } - - if (progress.isConnected) { - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) - ) { - AvatarUtils.AvatarImageWithFallback( - base64String = progress.senderAvatar, - fallbackText = progress.senderName, - size = 36.dp - ) - - Column { - Text( - text = "From:", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - ) - Text( - text = progress.senderName, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - - Text( - text = "Files (${progress.files.size}):", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) - - LazyColumn( - modifier = Modifier.heightIn(max = 280.dp), - verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.sm) - ) { - items(progress.files) { file -> - val fileProgress = progress.fileProgress[file.id] - ReceivingFileItem( - file = file, - progress = if (file.size > 0UL && fileProgress != null) { - (fileProgress.receivedBytes.toFloat() / file.size.toFloat()).coerceIn(0f, 1f) - } else 0f, - receivedBytes = fileProgress?.receivedBytes ?: 0L, - isComplete = fileProgress?.isComplete ?: false - ) - } - } - } else { - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - modifier = Modifier.size(48.dp), - strokeWidth = 4.dp, - color = MaterialTheme.colorScheme.primary - ) - } - } - } - } -} - -@Composable -private fun TransferCompleteCard( - receivedFiles: List, - onReceiveMore: () -> Unit, - onDone: () -> Unit -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) - ) { - Column( - modifier = Modifier.padding(DesignTokens.Spacing.xl), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .size(80.dp) - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Default.CheckCircle, - contentDescription = "Complete", - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary - ) - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - - Text( - text = "Files Received Successfully!", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) - - Text( - text = "${receivedFiles.size} file${if (receivedFiles.size != 1) "s" else ""} saved to Downloads", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f) - ) - - if (receivedFiles.isNotEmpty()) { - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) - ) { - Column( - modifier = Modifier.padding(DesignTokens.Spacing.lg) - ) { - Text( - text = "Received Files:", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) - - // Show first 3 files, then "... and X more" if needed - receivedFiles.take(3).forEach { fileName -> - Text( - text = "• $fileName", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - if (receivedFiles.size > 3) { - Text( - text = "• ... and ${receivedFiles.size - 3} more", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), - fontWeight = FontWeight.Medium - ) - } - } - } - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) - ) { - OutlinedButton( - onClick = onReceiveMore, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) - ) { - Icon( - Icons.Default.Refresh, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) - Text("Receive More", fontWeight = FontWeight.Medium) - } - - Button( - onClick = onDone, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) - ) { - Text("Done", fontWeight = FontWeight.Medium) - } - } - } - } -} - -@Composable -private fun ErrorCard( - error: ReceiveError, - onRetry: () -> Unit, - onDismiss: () -> Unit -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) - ) { - Column( - modifier = Modifier.padding(DesignTokens.Spacing.xl), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .size(80.dp) - .background( - color = MaterialTheme.colorScheme.error.copy(alpha = 0.1f), - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - Icon( - if (error.isRecoverable) Icons.Default.Warning else TablerIcons.AlertCircle, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.error - ) - } - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - - Text( - text = if (error.isRecoverable) "Something went wrong" else "Error occurred", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) - - Text( - text = error.message, - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f), - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2 - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) - ) { - OutlinedButton( - onClick = onDismiss, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) - ) { - Text("Cancel", fontWeight = FontWeight.Medium) - } - - if (error.isRecoverable) { - Button( - onClick = onRetry, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) - ) { - Text("Try Again", fontWeight = FontWeight.SemiBold) - } - } - } - } - } -} - -@Composable -private fun ReceivingFileItem( - file: ReceiveFileInfo, - progress: Float, - receivedBytes: Long, - isComplete: Boolean -) { - - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.xs) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(DesignTokens.Spacing.lg), - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = file.name, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) - - LinearProgressIndicator( - progress = { progress }, - modifier = Modifier - .fillMaxWidth() - .height(6.dp) - .clip(RoundedCornerShape(3.dp)), - color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), - ) - - Spacer(modifier = Modifier.height(6.dp)) - - Text( - text = "${formatBytes(receivedBytes)} / ${formatBytes(file.size.toLong())}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - if (isComplete) { - Spacer(modifier = Modifier.width(DesignTokens.Spacing.md)) - Box( - modifier = Modifier - .size(32.dp) - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Default.CheckCircle, - contentDescription = "Complete", - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp) - ) - } - } - } - } -} - -@androidx.annotation.OptIn(ExperimentalGetImage::class) -@Composable -private fun QRCodeScanner( - onQRCodeScanned: (String, UByte) -> Unit, - onError: (ReceiveError) -> Unit -) { - val context = LocalContext.current - val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current - val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() } - - AndroidView( - factory = { ctx -> - val previewView = PreviewView(ctx) - val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx) - - cameraProviderFuture.addListener({ - try { - val cameraProvider = cameraProviderFuture.get() - - val preview = Preview.Builder().build().also { - it.setSurfaceProvider(previewView.surfaceProvider) - } - - val imageAnalyzer = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() - .also { - it.setAnalyzer(cameraExecutor) { imageProxy -> - processImageProxy(imageProxy, onQRCodeScanned, onError) - } - } - - val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA - - cameraProvider.unbindAll() - cameraProvider.bindToLifecycle( - lifecycleOwner, - cameraSelector, - preview, - imageAnalyzer - ) - } catch (exc: Exception) { - onError(ReceiveError.CameraInitializationFailed) - } - }, ContextCompat.getMainExecutor(ctx)) - - previewView - }, - modifier = Modifier.fillMaxSize() - ) - - DisposableEffect(Unit) { - onDispose { - cameraExecutor.shutdown() - } - } -} - -@ExperimentalGetImage -private fun processImageProxy( - imageProxy: ImageProxy, - onQRCodeScanned: (String, UByte) -> Unit, - onError: (ReceiveError) -> Unit -) { - val mediaImage = imageProxy.image - if (mediaImage != null) { - val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) - val scanner = BarcodeScanning.getClient() - - scanner.process(image) - .addOnSuccessListener { barcodes -> - for (barcode in barcodes) { - when (barcode.valueType) { - Barcode.TYPE_TEXT, Barcode.TYPE_URL -> { - barcode.rawValue?.let { value -> - // Parse Drop QR code format: drop://receive?ticket=...&confirmation=... - if (value.startsWith("drop://receive?")) { - try { - val uri = value.toUri() - val ticket = uri.getQueryParameter("ticket") - val confirmationStr = uri.getQueryParameter("confirmation") - - if (ticket != null && confirmationStr != null) { - val confirmation = confirmationStr.toUByte() - onQRCodeScanned(ticket, confirmation) - return@addOnSuccessListener - } - } catch (_: Exception) { - onError(ReceiveError.InvalidQRCode) - return@addOnSuccessListener - } - } else { - onError(ReceiveError.InvalidQRCode) - return@addOnSuccessListener - } - } - } - } - } - } - .addOnFailureListener { exception -> - onError( - when { - exception.message?.contains( - "camera", - ignoreCase = true - ) == true -> ReceiveError.CameraInitializationFailed - - else -> ReceiveError.UnknownError - } - ) - } - .addOnCompleteListener { - imageProxy.close() - } - } else { - imageProxy.close() - } -} - -private fun formatBytes(bytes: Long): String { - if (bytes < 1024) return "$bytes B" - val kb = bytes / 1024.0 - if (kb < 1024) return "%.1f KB".format(kb) - val mb = kb / 1024.0 - if (mb < 1024) return "%.1f MB".format(mb) - val gb = mb / 1024.0 - return "%.1f GB".format(gb) -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt new file mode 100644 index 0000000..0ea9777 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt @@ -0,0 +1,391 @@ +package dev.arkbuilders.drop.app.ui.receive + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.accompanist.permissions.isGranted +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.arkbuilders.drop.app.TransferManager +import dev.arkbuilders.drop.app.data.ReceivingProgress +import dev.arkbuilders.drop.app.domain.PermissionsHelper +import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +data class ReceiveScreenState( + val workflowState: ReceiveWorkflowState, + val scannedTicket: String?, + val scannedConfirmation: UByte?, + val manualInputText: String, + val manualInputError: String?, + val receivedFiles: List, + val isCameraPermissionGranted: Boolean, + val receiveProgress: ReceivingProgress?, +) { + companion object { + fun initial() = ReceiveScreenState( + workflowState = ReceiveWorkflowState.Initial, + scannedTicket = null, + scannedConfirmation = null, + manualInputText = "", + manualInputError = null, + receivedFiles = emptyList(), + isCameraPermissionGranted = false, + receiveProgress = null, + ) + } +} + +sealed class ReceiveScreenEffect { + data object HideKeyboard : ReceiveScreenEffect() + data object NavigateBack : ReceiveScreenEffect() + data object ShowSuccessAnimation : ReceiveScreenEffect() + data object RequestCameraPermission : ReceiveScreenEffect() +} + +@HiltViewModel +class ReceiveViewModel @Inject constructor( + private val transferManager: TransferManager, + private val permissionsHelper: PermissionsHelper, +) : ViewModel(), ContainerHost { + override val container: Container = + container(ReceiveScreenState.initial()) + + init { + intent { + val receiveProgress = transferManager.receiveProgress?.value + val workflowState = if (receiveProgress != null && receiveProgress.isConnected) { + listenToProgress() + ReceiveWorkflowState.Receiving + } else { + ReceiveWorkflowState.Initial + } + reduce { + state.copy( + workflowState = workflowState, + receiveProgress = receiveProgress, + ) + } + } + } + + fun onRequestCameraPermission() = intent { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.RequestingPermission + ) + } + postSideEffect(ReceiveScreenEffect.RequestCameraPermission) + } + + fun onEnterManually() = intent { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.ManualInput + ) + } + } + + fun onStartScanning() = intent { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.Scanning + ) + } + } + + fun onStopScanning() = intent { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.Initial + ) + } + } + + fun onError(error: ReceiveError) = intent { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.Error(error) + ) + } + } + + fun onAccept() = intent { + try { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.Connecting + ) + } + + val bubble = + transferManager.receiveFiles(state.scannedTicket!!, state.scannedConfirmation!!) + if (bubble != null) { + reduce { + state.copy( + receiveProgress = transferManager.receiveProgress!!.value, + workflowState = ReceiveWorkflowState.Receiving + ) + } + listenToProgress() + } else { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.Error(ReceiveError.ConnectionFailed) + ) + } + } + } catch (e: Exception) { + val workflowState = ReceiveWorkflowState.Error( + when { + e.message?.contains( + "network", + ignoreCase = true + ) == true -> ReceiveError.NetworkError + + else -> ReceiveError.ConnectionFailed + } + ) + reduce { + state.copy( + workflowState = workflowState + ) + } + } + } + + fun onCameraPermissionGranted(isGranted: Boolean) = intent { + val workflowState = if (isGranted) { + ReceiveWorkflowState.Scanning + } else { + ReceiveWorkflowState.Error(ReceiveError.CameraPermissionDenied) + } + reduce { + state.copy( + workflowState = workflowState, + isCameraPermissionGranted = isGranted, + ) + } + } + + fun onScanAgain() = intent { + val workflowState = if (permissionsHelper.isCameraGranted()) { + ReceiveWorkflowState.Scanning + } else { + ReceiveWorkflowState.ManualInput + } + reduce { + state.copy( + workflowState = workflowState, + scannedTicket = null, + scannedConfirmation = null, + manualInputText = "", + manualInputError = null, + ) + } + } + + fun onReceiveMore() = intent { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.Initial, + receivedFiles = emptyList(), + scannedTicket = null, + scannedConfirmation = null, + manualInputText = "", + manualInputError = null, + ) + } + transferManager.cancelReceive() + } + + fun onDone() = intent { + transferManager.cancelReceive() + postSideEffect(ReceiveScreenEffect.NavigateBack) + } + + fun onPasteFromClipboard(clipText: String?) = intent { + if (!clipText.isNullOrEmpty()) { + reduce { + state.copy( + manualInputText = clipText, + manualInputError = null, + ) + } + } + } + + fun onErrorRetry() = intent { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.Initial, + scannedTicket = null, + scannedConfirmation = null, + manualInputText = "", + manualInputError = null, + ) + } + } + + fun onErrorDismiss() = intent { + transferManager.cancelReceive() + postSideEffect(ReceiveScreenEffect.NavigateBack) + } + + fun onQrCodeScanned(ticket: String, confirmation: UByte) = intent { + reduce { + state.copy( + scannedTicket = ticket, + scannedConfirmation = confirmation, + workflowState = ReceiveWorkflowState.QRCodeScanned, + ) + } + } + + fun onManualInputChanged(input: String) = blockingIntent { + reduce { + state.copy( + manualInputText = input, + manualInputError = null, + ) + } + } + + fun onCancelReceiving() = intent { + transferManager.cancelReceive() + reduce { + state.copy( + workflowState = ReceiveWorkflowState.Initial, + scannedTicket = null, + scannedConfirmation = null, + manualInputText = "", + manualInputError = null, + ) + } + } + + fun onCancelManualInput() = intent { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.Initial, + manualInputText = "", + manualInputError = null, + ) + } + postSideEffect(ReceiveScreenEffect.HideKeyboard) + } + + fun handleManualInputSubmit() = intent { + val parsed = parseManualInput(state.manualInputText) + if (parsed != null) { + reduce { + state.copy( + scannedTicket = parsed.first, + scannedConfirmation = parsed.second, + workflowState = ReceiveWorkflowState.QRCodeScanned, + manualInputError = null, + ) + } + postSideEffect(ReceiveScreenEffect.HideKeyboard) + } else { + reduce { + state.copy( + manualInputError = "Invalid format. Please enter: ticket confirmation" + ) + } + } + } + + private fun listenToProgress() { + transferManager.receiveProgress!!.onEach { progress -> + intent { + reduce { + state.copy( + receiveProgress = progress + ) + } + } + + if (progress.isConnected && progress.files.isNotEmpty()) { + // Check if all files are complete + val allFilesComplete = progress.files.all { file -> + val fileProgress = progress.fileProgress[file.id] + fileProgress?.isComplete == true + } + + if (allFilesComplete) { + // Small delay to ensure UI updates are visible + delay(1000) + try { + val savedFiles = transferManager.saveReceivedFiles() + if (savedFiles.isNotEmpty()) { + intent { + reduce { + state.copy( + receivedFiles = savedFiles.map { it.name }, + workflowState = ReceiveWorkflowState.Success, + ) + } + postSideEffect(ReceiveScreenEffect.ShowSuccessAnimation) + } + } else { + intent { + reduce { + state.copy( + workflowState = ReceiveWorkflowState.Error(ReceiveError.NoFilesReceived) + ) + } + } + } + } catch (e: Exception) { + val workflowState = ReceiveWorkflowState.Error( + when { + e.message?.contains("storage", ignoreCase = true) == true -> + ReceiveError.StorageError + + e.message?.contains("network", ignoreCase = true) == true -> + ReceiveError.NetworkError + + else -> ReceiveError.UnknownError + } + ) + intent { + reduce { + state.copy( + workflowState = workflowState + ) + } + } + } + } + } + }.launchIn(viewModelScope) + } + + private fun parseManualInput(input: String): Pair? { + return try { + val trimmed = input.trim() + val parts = trimmed.split(" ") + + if (parts.size == 2) { + val ticket = parts[0].trim() + val confirmation = parts[1].trim().toUByte() + + if (ticket.isNotEmpty()) { + Pair(ticket, confirmation) + } else { + null + } + } else { + null + } + } catch (e: Exception) { + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveCompleteCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveCompleteCard.kt new file mode 100644 index 0000000..85c98f0 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveCompleteCard.kt @@ -0,0 +1,166 @@ +package dev.arkbuilders.drop.app.ui.receive.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.ui.theme.DesignTokens + +@Composable +fun ReceiveCompleteCard( + receivedFiles: List, + onReceiveMore: () -> Unit, + onDone: () -> Unit +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + ) { + Column( + modifier = Modifier.padding(DesignTokens.Spacing.xl), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(80.dp) + .background( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = "Complete", + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + + Text( + text = "Files Received Successfully!", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) + + Text( + text = "${receivedFiles.size} file${if (receivedFiles.size != 1) "s" else ""} saved to Downloads", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f) + ) + + if (receivedFiles.isNotEmpty()) { + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + ) { + Column( + modifier = Modifier.padding(DesignTokens.Spacing.lg) + ) { + Text( + text = "Received Files:", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) + + // Show first 3 files, then "... and X more" if needed + receivedFiles.take(3).forEach { fileName -> + Text( + text = "• $fileName", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (receivedFiles.size > 3) { + Text( + text = "• ... and ${receivedFiles.size - 3} more", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + fontWeight = FontWeight.Medium + ) + } + } + } + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + ) { + OutlinedButton( + onClick = onReceiveMore, + modifier = Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + ) { + Icon( + Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Text("Receive More", fontWeight = FontWeight.Medium) + } + + Button( + onClick = onDone, + modifier = Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + ) { + Text("Done", fontWeight = FontWeight.Medium) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveErrorCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveErrorCard.kt new file mode 100644 index 0000000..8a025f2 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveErrorCard.kt @@ -0,0 +1,124 @@ +package dev.arkbuilders.drop.app.ui.receive.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import compose.icons.TablerIcons +import compose.icons.tablericons.AlertCircle +import dev.arkbuilders.drop.app.ui.receive.ReceiveError +import dev.arkbuilders.drop.app.ui.theme.DesignTokens + +@Composable +fun ReceiveErrorCard( + error: ReceiveError, + onRetry: () -> Unit, + onDismiss: () -> Unit +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + ) { + Column( + modifier = Modifier.padding(DesignTokens.Spacing.xl), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(80.dp) + .background( + color = MaterialTheme.colorScheme.error.copy(alpha = 0.1f), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + if (error.isRecoverable) Icons.Default.Warning else TablerIcons.AlertCircle, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.error + ) + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + + Text( + text = if (error.isRecoverable) "Something went wrong" else "Error occurred", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) + + Text( + text = error.message, + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f), + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2 + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + ) { + OutlinedButton( + onClick = onDismiss, + modifier = Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + ) { + Text("Cancel", fontWeight = FontWeight.Medium) + } + + if (error.isRecoverable) { + Button( + onClick = onRetry, + modifier = Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Text("Try Again", fontWeight = FontWeight.SemiBold) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveLoadingCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveLoadingCard.kt new file mode 100644 index 0000000..1324b0f --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveLoadingCard.kt @@ -0,0 +1,56 @@ +package dev.arkbuilders.drop.app.ui.receive.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.ui.theme.DesignTokens + +@Composable +fun ReceiveLoadingCard( + message: String +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.md) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(DesignTokens.Spacing.xl), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 3.dp, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(DesignTokens.Spacing.lg)) + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveManualInputCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveManualInputCard.kt new file mode 100644 index 0000000..9f597ae --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveManualInputCard.kt @@ -0,0 +1,164 @@ +package dev.arkbuilders.drop.app.ui.receive.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import compose.icons.TablerIcons +import compose.icons.tablericons.ArrowForward +import dev.arkbuilders.drop.app.ui.theme.DesignTokens + +@Composable +fun ReceiveManualInputCard( + inputText: String, + onInputChange: (String) -> Unit, + inputError: String?, + onPasteFromClipboard: () -> Unit, + onSubmit: () -> Unit, + onCancel: () -> Unit +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + ) { + Column( + modifier = Modifier.padding(DesignTokens.Spacing.xxl), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(80.dp) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + TablerIcons.ArrowForward, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + + Text( + text = "Enter Transfer Code", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + + Text( + text = "Paste or type the transfer code from the sender in the format: ticket confirmation", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + + OutlinedTextField( + value = inputText, + onValueChange = onInputChange, + label = { Text("Transfer Code") }, + placeholder = { Text("ticket confirmation") }, + modifier = Modifier.fillMaxWidth(), + isError = inputError != null, + supportingText = inputError?.let { error -> + { Text(error, color = MaterialTheme.colorScheme.error) } + }, + trailingIcon = { + IconButton(onClick = onPasteFromClipboard) { + Icon( + TablerIcons.ArrowForward, + contentDescription = "Paste", + tint = MaterialTheme.colorScheme.primary + ) + } + }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { onSubmit() } + ), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + ) { + OutlinedButton( + onClick = onCancel, + modifier = Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + ) { + Text( + "Cancel", + fontWeight = FontWeight.Medium + ) + } + + Button( + onClick = onSubmit, + modifier = Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ), + enabled = inputText.trim().isNotEmpty() + ) { + Text( + "Connect", + fontWeight = FontWeight.SemiBold + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceivePermissionRequestCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceivePermissionRequestCard.kt new file mode 100644 index 0000000..24a0e19 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceivePermissionRequestCard.kt @@ -0,0 +1,137 @@ +package dev.arkbuilders.drop.app.ui.receive.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import compose.icons.TablerIcons +import compose.icons.tablericons.ArrowForward +import compose.icons.tablericons.Camera +import dev.arkbuilders.drop.app.ui.theme.DesignTokens + +@Composable +fun ReceivePermissionRequestCard( + onRequestPermission: () -> Unit, + onEnterManually: () -> Unit +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + ) { + Column( + modifier = Modifier.padding(DesignTokens.Spacing.xxl), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(80.dp) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + TablerIcons.Camera, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + + Text( + text = "Camera Permission Required", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + + Text( + text = "We need camera access to scan QR codes for receiving files. Your privacy is protected - we only use the camera for QR code scanning.", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + + Button( + onClick = onRequestPermission, + modifier = Modifier + .fillMaxWidth() + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Text( + "Grant Permission", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + + Text( + text = "Or", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + + OutlinedButton( + onClick = onEnterManually, + modifier = Modifier + .fillMaxWidth() + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + ) { + Icon( + TablerIcons.ArrowForward, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Text( + "Enter Code Manually", + fontWeight = FontWeight.Medium + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt new file mode 100644 index 0000000..00cff29 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt @@ -0,0 +1,247 @@ +package dev.arkbuilders.drop.app.ui.receive.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.data.ReceiveFileInfo +import dev.arkbuilders.drop.app.data.ReceivingProgress +import dev.arkbuilders.drop.app.ui.profile.AvatarUtils +import dev.arkbuilders.drop.app.ui.theme.DesignTokens + +@Composable +fun ReceiveProgressCard( + progress: ReceivingProgress, + onCancel: () -> Unit +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + ) { + Column( + modifier = Modifier.padding(DesignTokens.Spacing.lg) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = if (progress.isConnected) "Receiving Files..." else "Connecting...", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + + Surface( + onClick = onCancel, + shape = CircleShape, + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), + modifier = Modifier.size(40.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Icon( + Icons.Default.Close, + contentDescription = "Cancel", + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(20.dp) + ) + } + } + } + + if (progress.isConnected) { + Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + ) { + AvatarUtils.AvatarImageWithFallback( + base64String = progress.senderAvatar, + fallbackText = progress.senderName, + size = 36.dp + ) + + Column { + Text( + text = "From:", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + ) + Text( + text = progress.senderName, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + + Text( + text = "Files (${progress.files.size}):", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + + LazyColumn( + modifier = Modifier.heightIn(max = 280.dp), + verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.sm) + ) { + items(progress.files) { file -> + val fileProgress = progress.fileProgress[file.id] + ReceivingFileItem( + file = file, + progress = if (file.size > 0UL && fileProgress != null) { + (fileProgress.receivedBytes.toFloat() / file.size.toFloat()).coerceIn(0f, 1f) + } else 0f, + receivedBytes = fileProgress?.receivedBytes ?: 0L, + isComplete = fileProgress?.isComplete ?: false + ) + } + } + } else { + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(48.dp), + strokeWidth = 4.dp, + color = MaterialTheme.colorScheme.primary + ) + } + } + } + } +} + +@Composable +private fun ReceivingFileItem( + file: ReceiveFileInfo, + progress: Float, + receivedBytes: Long, + isComplete: Boolean +) { + + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.xs) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(DesignTokens.Spacing.lg), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = file.name, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) + + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .clip(RoundedCornerShape(3.dp)), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), + ) + + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = "${formatBytes(receivedBytes)} / ${formatBytes(file.size.toLong())}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + if (isComplete) { + Spacer(modifier = Modifier.width(DesignTokens.Spacing.md)) + Box( + modifier = Modifier + .size(32.dp) + .background( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = "Complete", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + } + } + } + } +} + +private fun formatBytes(bytes: Long): String { + if (bytes < 1024) return "$bytes B" + val kb = bytes / 1024.0 + if (kb < 1024) return "%.1f KB".format(kb) + val mb = kb / 1024.0 + if (mb < 1024) return "%.1f MB".format(mb) + val gb = mb / 1024.0 + return "%.1f GB".format(gb) +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveQRCodeScannedCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveQRCodeScannedCard.kt new file mode 100644 index 0000000..426e324 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveQRCodeScannedCard.kt @@ -0,0 +1,124 @@ +package dev.arkbuilders.drop.app.ui.receive.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.ui.theme.DesignTokens + +@Composable +fun ReceiveQRCodeScannedCard( + onAccept: () -> Unit, + onScanAgain: () -> Unit +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + ) { + Column( + modifier = Modifier.padding(DesignTokens.Spacing.xxl), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(80.dp) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + + Text( + text = "Code Received!", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + + Text( + text = "Ready to receive files from sender. Tap Accept to start the transfer.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2 + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + ) { + OutlinedButton( + onClick = onScanAgain, + modifier = Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + ) { + Text( + "Try Again", + fontWeight = FontWeight.Medium + ) + } + + Button( + onClick = onAccept, + modifier = Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Text( + "Accept", + fontWeight = FontWeight.SemiBold + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveReadyToScanCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveReadyToScanCard.kt new file mode 100644 index 0000000..68dd319 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveReadyToScanCard.kt @@ -0,0 +1,149 @@ +package dev.arkbuilders.drop.app.ui.receive.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import compose.icons.TablerIcons +import compose.icons.tablericons.ArrowForward +import compose.icons.tablericons.Camera +import compose.icons.tablericons.Qrcode +import dev.arkbuilders.drop.app.ui.theme.DesignTokens + +@Composable +fun ReceiveReadyToScanCard( + onStartScanning: () -> Unit, + onEnterManually: () -> Unit +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + ) { + Column( + modifier = Modifier.padding(DesignTokens.Spacing.xxl), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(80.dp) + .background( + brush = Brush.radialGradient( + colors = listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) + ) + ), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + TablerIcons.Qrcode, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.onPrimary + ) + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + + Text( + text = "Ready to Receive", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + + Text( + text = "Scan the QR code from the sender's device or enter the transfer code manually to start receiving files securely.", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + + Button( + onClick = onStartScanning, + modifier = Modifier + .fillMaxWidth() + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Icon( + TablerIcons.Camera, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Text( + "Start Scanning", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + + Text( + text = "Or", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + + OutlinedButton( + onClick = onEnterManually, + modifier = Modifier + .fillMaxWidth() + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + ) { + Icon( + TablerIcons.ArrowForward, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Text( + "Enter Code Manually", + fontWeight = FontWeight.Medium + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveScanningCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveScanningCard.kt new file mode 100644 index 0000000..54f44ba --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveScanningCard.kt @@ -0,0 +1,250 @@ +package dev.arkbuilders.drop.app.ui.receive.components + +import androidx.annotation.OptIn +import androidx.camera.core.CameraSelector +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import compose.icons.TablerIcons +import compose.icons.tablericons.ArrowForward +import compose.icons.tablericons.CameraOff +import dev.arkbuilders.drop.app.ui.receive.ReceiveError +import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import kotlin.text.toUByte + +@Composable +fun ReceiveScanningCard( + onQRCodeScanned: (String, UByte) -> Unit, + onError: (ReceiveError) -> Unit, + onStopScanning: () -> Unit, + onEnterManually: () -> Unit +) { + Column { + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f), + shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + ) { + QRCodeScanner( + onQRCodeScanned = onQRCodeScanned, + onError = onError + ) + } + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + + Text( + text = "Point your camera at the QR code", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + ) { + OutlinedButton( + onClick = onStopScanning, + modifier = Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + ) { + Icon( + TablerIcons.CameraOff, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Text( + "Stop Scanning", + fontWeight = FontWeight.Medium + ) + } + + Button( + onClick = onEnterManually, + modifier = Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + ) { + Icon( + TablerIcons.ArrowForward, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Text( + "Enter Code", + fontWeight = FontWeight.Medium + ) + } + } + } +} + +@OptIn(ExperimentalGetImage::class) +@Composable +private fun QRCodeScanner( + onQRCodeScanned: (String, UByte) -> Unit, + onError: (ReceiveError) -> Unit +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() } + + AndroidView( + factory = { ctx -> + val previewView = PreviewView(ctx) + val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx) + + cameraProviderFuture.addListener({ + try { + val cameraProvider = cameraProviderFuture.get() + + val preview = Preview.Builder().build().also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + + val imageAnalyzer = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + it.setAnalyzer(cameraExecutor) { imageProxy -> + processImageProxy(imageProxy, onQRCodeScanned, onError) + } + } + + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + preview, + imageAnalyzer + ) + } catch (exc: Exception) { + onError(ReceiveError.CameraInitializationFailed) + } + }, ContextCompat.getMainExecutor(ctx)) + + previewView + }, + modifier = Modifier.fillMaxSize() + ) + + DisposableEffect(Unit) { + onDispose { + cameraExecutor.shutdown() + } + } +} + +@ExperimentalGetImage +private fun processImageProxy( + imageProxy: ImageProxy, + onQRCodeScanned: (String, UByte) -> Unit, + onError: (ReceiveError) -> Unit +) { + val mediaImage = imageProxy.image + if (mediaImage != null) { + val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) + val scanner = BarcodeScanning.getClient() + + scanner.process(image) + .addOnSuccessListener { barcodes -> + for (barcode in barcodes) { + when (barcode.valueType) { + Barcode.TYPE_TEXT, Barcode.TYPE_URL -> { + barcode.rawValue?.let { value -> + // Parse Drop QR code format: drop://receive?ticket=...&confirmation=... + if (value.startsWith("drop://receive?")) { + try { + val uri = value.toUri() + val ticket = uri.getQueryParameter("ticket") + val confirmationStr = uri.getQueryParameter("confirmation") + + if (ticket != null && confirmationStr != null) { + val confirmation = confirmationStr.toUByte() + onQRCodeScanned(ticket, confirmation) + return@addOnSuccessListener + } + } catch (_: Exception) { + onError(ReceiveError.InvalidQRCode) + return@addOnSuccessListener + } + } else { + onError(ReceiveError.InvalidQRCode) + return@addOnSuccessListener + } + } + } + } + } + } + .addOnFailureListener { exception -> + onError( + when { + exception.message?.contains( + "camera", + ignoreCase = true + ) == true -> ReceiveError.CameraInitializationFailed + + else -> ReceiveError.UnknownError + } + ) + } + .addOnCompleteListener { + imageProxy.close() + } + } else { + imageProxy.close() + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0261bec..cce9c10 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ mlkitBarcodeScanning = "17.3.0" accompanistPermissions = "0.36.0" timber = "5.0.1" room = "2.8.1" +orbitMVI = "9.0.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -52,6 +53,8 @@ room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-testing = { module = "androidx.room:room-testing", version.ref = "room" } +orbit-compose = { module = "org.orbit-mvi:orbit-compose", version.ref = "orbitMVI" } +orbit-viewmodel = { module = "org.orbit-mvi:orbit-viewmodel", version.ref = "orbitMVI" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 373f761db21ce7a3ceeca1a29440ea2eef2a4593 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Thu, 30 Oct 2025 00:16:00 +0600 Subject: [PATCH 06/33] Refactor send screen --- .../arkbuilders/drop/app/DropApplication.kt | 8 +- .../drop/app/data/ResourcesHelperImpl.kt | 39 +- .../app/data/repository/NetworkStatusImpl.kt | 64 + .../dev/arkbuilders/drop/app/di/AppModule.kt | 8 + .../arkbuilders/drop/app/di/TmpEntryPoint.kt | 2 + .../drop/app/domain/ResourcesHelper.kt | 4 +- .../app/domain/repository/NetworkStatus.kt | 9 + .../app/domain/usecase/SendFilesUseCase.kt | 2 +- .../arkbuilders/drop/app/ui/DisplayUtils.kt | 25 + .../dev/arkbuilders/drop/app/ui/Home/Home.kt | 1 + .../drop/app/ui/receive/Receive.kt | 2 +- .../dev/arkbuilders/drop/app/ui/send/Send.kt | 1534 +---------------- .../drop/app/ui/send/SendViewModel.kt | 262 +++ .../drop/app/ui/send/components/SendButton.kt | 87 + .../drop/app/ui/send/components/SendCard.kt | 27 + .../app/ui/send/components/SendEmptyState.kt | 59 + .../ui/send/components/SendErrorOverlay.kt | 104 ++ .../app/ui/send/components/SendFileItem.kt | 125 ++ .../send/components/SendInstructionsCard.kt | 79 + .../send/components/SendLoadingIndicator.kt | 39 + .../app/ui/send/components/SendProgressBar.kt | 24 + .../ui/send/components/phase/ErrorPhase.kt | 108 ++ .../components/phase/FileSelectionPhase.kt | 157 ++ .../components/phase/GeneratingQRPhase.kt | 46 + .../components/phase/TransferCompletePhase.kt | 157 ++ .../components/phase/TransferringPhase.kt | 163 ++ .../phase/WaitingForReceiverPhase.kt | 92 + 27 files changed, 1761 insertions(+), 1466 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/DisplayUtils.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/SendViewModel.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendButton.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendCard.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendEmptyState.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendErrorOverlay.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendFileItem.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendInstructionsCard.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendLoadingIndicator.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendProgressBar.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/ErrorPhase.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/FileSelectionPhase.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/GeneratingQRPhase.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferCompletePhase.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/WaitingForReceiverPhase.kt diff --git a/app/src/main/java/dev/arkbuilders/drop/app/DropApplication.kt b/app/src/main/java/dev/arkbuilders/drop/app/DropApplication.kt index 0ed5b59..4344b72 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/DropApplication.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/DropApplication.kt @@ -2,6 +2,12 @@ package dev.arkbuilders.drop.app import android.app.Application import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber @HiltAndroidApp -class DropApplication : Application() +class DropApplication : Application() { + override fun onCreate() { + super.onCreate() + Timber.plant(Timber.DebugTree()) + } +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt index d43f4fa..ab66ba4 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt @@ -3,6 +3,7 @@ package dev.arkbuilders.drop.app.data import android.content.Context import android.net.Uri import android.provider.OpenableColumns +import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.app.domain.ResourcesHelper import timber.log.Timber @@ -11,11 +12,11 @@ import javax.inject.Inject class ResourcesHelperImpl @Inject constructor( @ApplicationContext private val context: Context, ) : ResourcesHelper { - override fun getFileName(uri: Uri): String? { + override fun getFileName(uri: String): String? { return try { context .contentResolver - .query(uri, null, null, null, null) + .query(uri.toUri(), null, null, null, null) ?.use { cursor -> if (cursor.moveToFirst()) { val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) @@ -27,4 +28,38 @@ class ResourcesHelperImpl @Inject constructor( null } } + + override fun validateUris(uris: List): Pair, Int> { + val validFiles = mutableListOf() + var skippedCount = 0 + + uris.forEach { uri -> + try { + val size = getFileSize(uri) + if (size > 0 && size <= 2_000_000_000L) { // 2GB limit + validFiles.add(uri) + } else { + skippedCount++ + } + } catch (e: Exception) { + skippedCount++ + } + } + + return validFiles to skippedCount + } + + override fun getFileSize(uri: String): Long { + return try { + context.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val sizeIndex = cursor.getColumnIndex(android.provider.OpenableColumns.SIZE) + if (sizeIndex >= 0) cursor.getLong(sizeIndex) else 0L + } else 0L + } ?: 0L + } catch (e: Exception) { + 0L + } + } + } \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt new file mode 100644 index 0000000..cf75c58 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt @@ -0,0 +1,64 @@ +package dev.arkbuilders.drop.app.data.repository + +import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Build +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.arkbuilders.drop.app.domain.repository.NetworkStatus +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +class NetworkStatusImpl @Inject constructor( + @ApplicationContext private val context: Context, +) : NetworkStatus { + private val cm = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + private val _onlineStatus = MutableStateFlow(checkIsOnline()) + override val onlineStatus: StateFlow = _onlineStatus + + init { + val networkRequest = + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + } + .build() + + cm.registerNetworkCallback( + networkRequest, + object : ConnectivityManager.NetworkCallback() { + override fun onLost(network: Network) { + _onlineStatus.tryEmit(false) + } + + override fun onAvailable(network: Network) { + _onlineStatus.tryEmit(true) + } + }, + ) + } + + private fun checkIsOnline(): Boolean { + val network: Network = cm.activeNetwork ?: return false + val networkCapabilities: NetworkCapabilities = + cm.getNetworkCapabilities(network) ?: return false + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + } else { + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index a427cb6..9bff940 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -10,10 +10,12 @@ import dev.arkbuilders.drop.app.TransferManager import dev.arkbuilders.drop.app.data.ResourcesHelperImpl import dev.arkbuilders.drop.app.data.db.Database import dev.arkbuilders.drop.app.data.helper.PermissionsHelperImpl +import dev.arkbuilders.drop.app.data.repository.NetworkStatusImpl import dev.arkbuilders.drop.app.data.repository.ProfileRepoImpl import dev.arkbuilders.drop.app.data.repository.TransferHistoryItemRepositoryImpl import dev.arkbuilders.drop.app.domain.PermissionsHelper import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.app.domain.repository.NetworkStatus import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import javax.inject.Singleton @@ -64,4 +66,10 @@ object AppModule { fun providePermissionHelper( impl: PermissionsHelperImpl ): PermissionsHelper = impl + + @Provides + @Singleton + fun provideNetworkStatus( + impl: NetworkStatusImpl + ): NetworkStatus = impl } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt index 8f94c21..79005e8 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt @@ -3,12 +3,14 @@ package dev.arkbuilders.drop.app.di import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase @EntryPoint @InstallIn(SingletonComponent::class) interface TmpEntryPoint { + fun resourcesHelper(): ResourcesHelper fun sendFilesUseCase(): SendFilesUseCase fun receiveFilesUseCase(): ReceiveFilesUseCase } \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt index 8e1da6d..47bcd88 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt @@ -3,5 +3,7 @@ package dev.arkbuilders.drop.app.domain import android.net.Uri interface ResourcesHelper { - fun getFileName(uri: Uri): String? + fun getFileName(uri: String): String? + fun validateUris(uris: List): Pair, Int> + fun getFileSize(uri: String): Long } \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt new file mode 100644 index 0000000..8053cd8 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt @@ -0,0 +1,9 @@ +package dev.arkbuilders.drop.app.domain.repository + +import kotlinx.coroutines.flow.StateFlow + +interface NetworkStatus { + fun isOnline() = onlineStatus.value + + val onlineStatus: StateFlow +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt index a17e660..bf2bee9 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt @@ -35,7 +35,7 @@ class SendFilesUseCase @Inject constructor( ) val senderFiles = fileUris.mapNotNull { uri -> - val fileName = resourcesHelper.getFileName(uri) + val fileName = resourcesHelper.getFileName(uri.toString()) if (fileName != null) { val fileData = SenderFileDataImpl(context, uri) SenderFile( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/DisplayUtils.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/DisplayUtils.kt new file mode 100644 index 0000000..a42b9f9 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/DisplayUtils.kt @@ -0,0 +1,25 @@ +package dev.arkbuilders.drop.app.ui + +object DisplayUtils { + fun formatBytes(bytes: Long): String { + if (bytes < 0) return "0 B" + if (bytes < 1024) return "$bytes B" + + val kb = bytes / 1024.0 + if (kb < 1024) return "%.1f KB".format(kb) + + val mb = kb / 1024.0 + if (mb < 1024) return "%.1f MB".format(mb) + + val gb = mb / 1024.0 + return "%.1f GB".format(gb) + } + + fun formatDuration(seconds: Long): String { + return when { + seconds < 60 -> "${seconds}s" + seconds < 3600 -> "${seconds / 60}m ${seconds % 60}s" + else -> "${seconds / 3600}h ${(seconds % 3600) / 60}m" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt index 91813ce..f6e71d8 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt @@ -43,6 +43,7 @@ import compose.icons.tablericons.ArrowUpCircle import compose.icons.tablericons.CloudDownload import compose.icons.tablericons.CloudUpload import compose.icons.tablericons.History +import dev.arkbuilders.drop.app.R import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.model.UserProfile diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt index feb2bfe..b4f234f 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt @@ -65,8 +65,8 @@ import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import dev.arkbuilders.drop.app.R import dev.arkbuilders.drop.app.TransferManager -import dev.arkbuilders.drop.app.ui.components.DropLogoIcon import dev.arkbuilders.drop.app.ui.receive.components.ReceiveCompleteCard import dev.arkbuilders.drop.app.ui.receive.components.ReceiveErrorCard import dev.arkbuilders.drop.app.ui.receive.components.ReceiveLoadingCard diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/Send.kt index 14de362..b9be659 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/Send.kt @@ -4,38 +4,27 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.graphics.Bitmap -import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding @@ -43,29 +32,16 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -74,21 +50,15 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.vector.ImageVector @@ -100,52 +70,30 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.graphics.createBitmap -import androidx.core.graphics.set +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController -import com.google.zxing.BarcodeFormat -import com.google.zxing.WriterException -import com.google.zxing.common.BitMatrix -import com.google.zxing.qrcode.QRCodeWriter import compose.icons.TablerIcons import compose.icons.tablericons.AlertCircle import compose.icons.tablericons.CloudUpload import compose.icons.tablericons.Copy -import compose.icons.tablericons.FileText -import compose.icons.tablericons.Plus import compose.icons.tablericons.Qrcode import dev.arkbuilders.drop.app.R import dev.arkbuilders.drop.app.TransferManager -import dev.arkbuilders.drop.app.ui.profile.AvatarUtils -import kotlinx.coroutines.currentCoroutineContext +import dev.arkbuilders.drop.app.ui.send.components.ButtonSize +import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant +import dev.arkbuilders.drop.app.ui.send.components.phase.FileSelectionPhase +import dev.arkbuilders.drop.app.ui.send.components.phase.GeneratingQRPhase +import dev.arkbuilders.drop.app.ui.send.components.SendButton +import dev.arkbuilders.drop.app.ui.send.components.SendLoadingIndicator +import dev.arkbuilders.drop.app.ui.send.components.phase.TransferCompletePhase +import dev.arkbuilders.drop.app.ui.send.components.phase.TransferringPhase +import dev.arkbuilders.drop.app.ui.send.components.phase.WaitingForReceiverPhase import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch - -private enum class SendPhase { - FileSelection, - GeneratingQR, - WaitingForReceiver, - Transferring, - Complete, - Error -} - -private data class SendState( - val phase: SendPhase = SendPhase.FileSelection, - val isLoading: Boolean = false, - val error: SendException? = null, - val transferProgress: TransferProgressState? = null, - val qrBitmap: Bitmap? = null, - val showQRDialog: Boolean = false, - val showSuccessAnimation: Boolean = false, - val successCountdown: Int = 0, - val networkConnected: Boolean = true, - val copyString: String? = null -) +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect // Comprehensive exception handling sealed class SendException( @@ -235,242 +183,28 @@ fun Send( val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current val listState = rememberLazyListState() + val viewModel: SendViewModel = hiltViewModel() - // State management - var sendState by remember { mutableStateOf(SendState()) } - var selectedFiles by rememberSaveable { mutableStateOf>(emptyList()) } + val state by viewModel.collectAsState() - // Observe transfer progress with error handling - val sendProgress by (transferManager.sendProgress?.collectAsState() - ?: remember { mutableStateOf(null) }) - - // Derived states - val totalFileSize by remember { - derivedStateOf { - selectedFiles.sumOf { uri -> - getFileSize(context, uri) - } - } - } - - val canStartTransfer by remember { - derivedStateOf { - selectedFiles.isNotEmpty() && - sendState.phase == SendPhase.FileSelection && - !sendState.isLoading && - sendState.networkConnected - } - } - - // File picker with comprehensive validation val filePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetMultipleContents() ) { uris -> - if (uris.isNotEmpty()) { - try { - val validatedFiles = validateAndFilterFiles(context, uris) - - if (validatedFiles.skippedCount > 0) { - sendState = sendState.copy( - error = SendException.FileTooLarge - ) - } - - selectedFiles = selectedFiles + validatedFiles.validFiles - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - - } catch (e: Exception) { - sendState = sendState.copy( - error = SendException.UnknownError("File validation failed: ${e.message}") - ) - } - } - } - - val transferStatus by produceState( - initialValue = false, - key1 = sendState.phase - ) { - if (sendState.phase == SendPhase.Transferring) { - while (currentCoroutineContext().isActive) { - delay(700) - val isFinished = transferManager.isSendFinished() - value = isFinished - if (isFinished) { - break - } - } - } - } - - // Handle transfer completion - LaunchedEffect(transferStatus) { - if (transferStatus && sendState.phase == SendPhase.Transferring) { - sendState = sendState.copy( - phase = SendPhase.Complete, - showSuccessAnimation = true, - successCountdown = 3000, - error = null - ) - try { - transferManager.recordSendCompletion(selectedFiles) - } catch (e: Exception) { - println("Failed to record completion: ${e.message}") - } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - } + viewModel.onFilesAdded(uris.map { it.toString() }) } - // Network monitoring - LaunchedEffect(Unit) { - while (true) { - val isConnected = checkNetworkConnection(context) - if (sendState.networkConnected != isConnected) { - sendState = sendState.copy(networkConnected = isConnected) - - if (!isConnected && sendState.phase in listOf( - SendPhase.WaitingForReceiver, SendPhase.Transferring - ) - ) { - sendState = sendState.copy( - phase = SendPhase.Error, error = SendException.NetworkUnavailable - ) - } - } - delay(2000) - } - } - - // Transfer progress monitoring with error handling - LaunchedEffect(sendProgress) { - sendProgress?.let { progress -> - try { - val progressState = TransferProgressState( - isConnected = progress.isConnected, - receiverName = progress.receiverName, - receiverAvatar = progress.receiverAvatar, - currentFileName = progress.fileName, - bytesTransferred = progress.sent.toLong(), - totalBytes = (progress.sent + progress.remaining).toLong(), - transferSpeedBps = calculateTransferSpeed(progress.sent.toLong()), - estimatedTimeRemaining = calculateETA( - progress.sent.toLong(), progress.remaining.toLong() - ) - ) - when { - progress.isConnected && sendState.phase == SendPhase.WaitingForReceiver -> { - sendState = sendState.copy( - phase = SendPhase.Transferring, - transferProgress = progressState, - showQRDialog = false, - error = null - ) - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - } - - else -> { - sendState = sendState.copy(transferProgress = progressState) - } - } - } catch (e: Exception) { - val stacktrace = e.stackTraceToString() - println("DEBUG: $stacktrace") - if (!stacktrace.startsWith("androidx.compose.runtime.LeftCompositionCancellationException:")) { - sendState = sendState.copy( - phase = SendPhase.Error, - error = SendException.UnknownError("Progress monitoring failed: ${e.message}") - ) - } + viewModel.collectSideEffect { effect -> + when (effect) { + SendScreenEffect.LaunchFilePicker -> { + filePickerLauncher.launch("*/*") } - } - } - -// Success animation countdown - LaunchedEffect(sendState.successCountdown) { - if (sendState.successCountdown > 0) { - delay(1000) - sendState = sendState.copy(successCountdown = sendState.successCountdown - 1000) - } - } - -// Core functions - val startTransfer = { - if (canStartTransfer) { - scope.launch { - try { - sendState = sendState.copy( - phase = SendPhase.GeneratingQR, isLoading = true, error = null - ) - - val bubble = transferManager.sendFiles(selectedFiles) - if (bubble != null) { - val ticket = transferManager.getCurrentSendTicket() ?: "" - val confirmation = transferManager.getCurrentSendConfirmation() ?: 0u - - if (ticket.isEmpty()) { - throw Exception("Invalid transfer ticket") - } - - val copyString = "${bubble.getTicket()} ${bubble.getConfirmation()}" - - val qrBitmap = generateQRCodeSafely(ticket, confirmation) - sendState = sendState.copy( - phase = SendPhase.WaitingForReceiver, - isLoading = false, - qrBitmap = qrBitmap, - showQRDialog = true, - copyString = copyString - ) - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - } else { - throw Exception("Transfer initialization returned null") - } - } catch (e: Exception) { - sendState = sendState.copy( - phase = SendPhase.Error, isLoading = false, error = when { - e.message?.contains("QR") == true -> SendException.QRGenerationFailed - e.message?.contains("network") == true -> SendException.NetworkUnavailable - else -> SendException.TransferInitializationFailed - } - ) - } + SendScreenEffect.NavigateBack -> { + navController.popBackStack() } - } else if (selectedFiles.isEmpty()) { - sendState = sendState.copy(error = SendException.NoFilesSelected) - } else if (!sendState.networkConnected) { - sendState = sendState.copy(error = SendException.NetworkUnavailable) - } - Unit - } - - val cancelTransfer = { - try { - transferManager.cancelSend() - sendState = SendState(networkConnected = sendState.networkConnected) - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - } catch (e: Exception) { - // Silent fail for cancel operation } } - val resetForNewTransfer = { - selectedFiles = emptyList() - sendState = SendState(networkConnected = sendState.networkConnected) - transferManager.cancelSend() - } - - val handleError = { action: String -> - when (action) { - "Retry" -> { - sendState = sendState.copy(error = null, phase = SendPhase.FileSelection) - } - - "Continue" -> { - sendState = sendState.copy(error = null) - } - } - } Scaffold( modifier = Modifier @@ -483,7 +217,7 @@ fun Send( onBackClick = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) navController.navigateUp() - }, networkConnected = sendState.networkConnected + } ) }, containerColor = MaterialTheme.colorScheme.background @@ -496,7 +230,7 @@ fun Send( ) { // Main content with phase-based transitions AnimatedContent( - targetState = sendState.phase, transitionSpec = { + targetState = state, transitionSpec = { slideInVertically( initialOffsetY = { it / 3 }, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, @@ -511,84 +245,84 @@ fun Send( }, label = "phaseTransition" ) { phase -> when (phase) { - SendPhase.FileSelection -> { + is SendScreenState.FileSelection -> { FileSelectionPhase( - selectedFiles = selectedFiles, - totalFileSize = totalFileSize, + selectedFiles = phase.files, + totalFileSize = phase.size, onAddFiles = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - filePickerLauncher.launch("*/*") + viewModel.onAddFiles() }, onRemoveFile = { uri -> - selectedFiles = selectedFiles - uri - haptic.performHapticFeedback(HapticFeedbackType.LongPress) + viewModel.onFileRemove(uri) + }, + onStartTransfer = { + viewModel.onStartTransfer() }, - onStartTransfer = startTransfer, - canStartTransfer = canStartTransfer, - isLoading = sendState.isLoading, - networkConnected = sendState.networkConnected, + canStartTransfer = phase.canStartTransfer, listState = listState ) } - SendPhase.GeneratingQR -> { - GeneratingQRPhase(onCancel = cancelTransfer) + is SendScreenState.GeneratingQR -> { + GeneratingQRPhase(onCancel = { viewModel.onCancelQrGeneration() }) } - SendPhase.WaitingForReceiver -> { + is SendScreenState.WaitingForReceiver -> { WaitingForReceiverPhase( - fileCount = selectedFiles.size, onCancel = cancelTransfer + fileCount = phase.files.size, + onCancel = { viewModel.onCancelTransfer() } ) } - SendPhase.Transferring -> { + is SendScreenState.Transfer -> { TransferringPhase( - progress = sendState.transferProgress, onCancel = cancelTransfer + progress = phase, + onCancel = { viewModel.onCancelTransfer() } ) } - SendPhase.Complete -> { + is SendScreenState.Complete -> { TransferCompletePhase( - fileCount = selectedFiles.size, - onSendMore = resetForNewTransfer, + fileCount = phase.files.size, + onSendMore = { + viewModel.onSendMore() + }, onDone = { - transferManager.cancelSend() - navController.navigateUp() + viewModel.onDone() }, - showSuccessAnimation = sendState.showSuccessAnimation, - successCountdown = sendState.successCountdown ) } - SendPhase.Error -> { - ErrorPhase( - error = sendState.error, - onRetry = { handleError("Retry") }, - onCancel = { - transferManager.cancelSend() - navController.navigateUp() - }) + is SendScreenState.Error -> { +// ErrorPhase( +// error = sendState.error, +// onRetry = { handleError("Retry") }, +// onCancel = { +// transferManager.cancelSend() +// navController.navigateUp() +// } +// ) } } } - sendState.error?.let { error -> - if (sendState.phase != SendPhase.Error) { - SendErrorOverlay( - error = error, - onDismiss = { sendState = sendState.copy(error = null) }, - onAction = { action -> handleError(action) }) - } - } +// sendState.error?.let { error -> +// if (sendState.phase != SendPhase.Error) { +// SendErrorOverlay( +// error = error, +// onDismiss = { sendState = sendState.copy(error = null) }, +// onAction = { action -> handleError(action) }) +// } +// } - // QR Code Dialog - if (sendState.showQRDialog && sendState.qrBitmap != null) { + val s = state + if (s is SendScreenState.WaitingForReceiver) { SendQRDialog( - qrBitmap = sendState.qrBitmap!!, - fileCount = selectedFiles.size, - copyString = sendState.copyString, - onDismiss = { sendState = sendState.copy(showQRDialog = false) }, - onCancel = cancelTransfer + qrBitmap = s.qrBitmap, + fileCount = s.files.size, + copyString = s.copyString, + onDismiss = { }, + onCancel = {} ) } } @@ -598,7 +332,7 @@ fun Send( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SendTopBar( - onBackClick: () -> Unit, networkConnected: Boolean + onBackClick: () -> Unit ) { TopAppBar( title = { @@ -634,9 +368,9 @@ private fun SendTopBar( } }, actions = { // Network status indicator - NetworkStatusIndicator( - connected = networkConnected, modifier = Modifier.padding(end = 8.dp) - ) +// NetworkStatusIndicator( +// connected = networkConnected, modifier = Modifier.padding(end = 8.dp) +// ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f), titleContentColor = MaterialTheme.colorScheme.onSurface @@ -665,992 +399,6 @@ private fun NetworkStatusIndicator( ) } -@Composable -private fun FileSelectionPhase( - selectedFiles: List, - totalFileSize: Long, - onAddFiles: () -> Unit, - onRemoveFile: (Uri) -> Unit, - onStartTransfer: () -> Unit, - canStartTransfer: Boolean, - isLoading: Boolean, - networkConnected: Boolean, - listState: androidx.compose.foundation.lazy.LazyListState -) { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(20.dp), - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - // File selection section - item { - SendCard { - Column( - modifier = Modifier.padding(20.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = "Selected Files", - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.SemiBold - ), - color = MaterialTheme.colorScheme.onSurface - ) - - if (selectedFiles.isNotEmpty()) { - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "${selectedFiles.size} file${if (selectedFiles.size != 1) "s" else ""} • ${ - formatBytes( - totalFileSize - ) - }", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - SendButton( - onClick = onAddFiles, - variant = ButtonVariant.Secondary, - size = ButtonSize.Medium - ) { - Icon( - TablerIcons.Plus, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - "Add Files", style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Medium - ) - ) - } - } - - Spacer(modifier = Modifier.height(20.dp)) - - if (selectedFiles.isEmpty()) { - SendEmptyState( - title = "No Files Selected", - description = "Tap 'Add Files' to choose files you want to send.", - icon = TablerIcons.FileText - ) - } else { - LazyColumn( - modifier = Modifier.heightIn(max = 300.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(selectedFiles) { uri -> - SendFileItem( - uri = uri, onRemove = { onRemoveFile(uri) }) - } - } - } - } - } - } - - // Transfer button - item { - SendButton( - onClick = onStartTransfer, - variant = ButtonVariant.Primary, - size = ButtonSize.Large, - enabled = canStartTransfer && networkConnected, - loading = isLoading, - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - ) { - if (!isLoading) { - Icon( - TablerIcons.CloudUpload, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - } - Text( - text = when { - isLoading -> "Starting Transfer..." - !networkConnected -> "No Network Connection" - selectedFiles.isEmpty() -> "Select Files First" - else -> "Send ${selectedFiles.size} File${if (selectedFiles.size != 1) "s" else ""}" - }, style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold - ) - ) - } - } - - // Instructions - item { - SendInstructionsCard() - } - } -} - -@Composable -private fun GeneratingQRPhase( - onCancel: () -> Unit -) { - Box( - modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center - ) { - SendCard { - Column( - modifier = Modifier.padding(40.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - SendLoadingIndicator( - message = "Generating QR Code..." - ) - - Spacer(modifier = Modifier.height(32.dp)) - - SendButton( - onClick = onCancel, variant = ButtonVariant.Secondary, size = ButtonSize.Medium - ) { - Text("Cancel") - } - } - } - } -} - -@Composable -private fun WaitingForReceiverPhase( - fileCount: Int, onCancel: () -> Unit -) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - item { - SendCard { - Column( - modifier = Modifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Ready to Send", style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.Bold - ), color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "$fileCount file${if (fileCount != 1) "s" else ""} ready for transfer", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(32.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - SendLoadingIndicator() - Text( - text = "Waiting for receiver to scan...", - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium - ), - color = MaterialTheme.colorScheme.primary - ) - } - } - } - } - - item { - SendButton( - onClick = onCancel, - variant = ButtonVariant.Secondary, - size = ButtonSize.Large, - modifier = Modifier.fillMaxWidth() - ) { - Text( - "Cancel Transfer", style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Medium - ) - ) - } - } - } -} - -@Composable -private fun TransferringPhase( - progress: TransferProgressState?, onCancel: () -> Unit -) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(20.dp), - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - item { - progress?.let { p -> - SendCard( - backgroundColor = MaterialTheme.colorScheme.primaryContainer - ) { - Column( - modifier = Modifier.padding(24.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Sending Files", - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold - ), - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - - IconButton( - onClick = onCancel, - modifier = Modifier - .size(32.dp) - .clip(CircleShape) - ) { - Icon( - Icons.Default.Close, - contentDescription = "Cancel transfer", - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } - - if (p.receiverName.isNotEmpty()) { - Spacer(modifier = Modifier.height(16.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - AvatarUtils.AvatarImageWithFallback( - base64String = p.receiverAvatar ?: "", - fallbackText = p.receiverName, - size = 32.dp - ) - - Text( - text = "Connected to: ${p.receiverName}", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) - ) - } - } - - if (p.currentFileName.isNotEmpty()) { - Spacer(modifier = Modifier.height(20.dp)) - - Text( - text = "Sending: ${p.currentFileName}", - style = MaterialTheme.typography.bodyLarge.copy( - fontWeight = FontWeight.Medium - ), - color = MaterialTheme.colorScheme.onPrimaryContainer, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - val progressValue = if (p.totalBytes > 0) { - (p.bytesTransferred.toFloat() / p.totalBytes.toFloat()).coerceIn( - 0f, 1f - ) - } else 0f - - Spacer(modifier = Modifier.height(16.dp)) - - SendProgressBar( - progress = progressValue, modifier = Modifier.fillMaxWidth() - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = "${formatBytes(p.bytesTransferred)} / ${formatBytes(p.totalBytes)}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - ) - - if (p.transferSpeedBps > 0) { - Text( - text = "${formatBytes(p.transferSpeedBps)}/s", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy( - alpha = 0.7f - ) - ) - } - } - - if (p.estimatedTimeRemaining > 0) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Time remaining: ${formatDuration(p.estimatedTimeRemaining)}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f) - ) - } - } - } - } - } - } - } -} - -@Composable -private fun TransferCompletePhase( - fileCount: Int, - onSendMore: () -> Unit, - onDone: () -> Unit, - showSuccessAnimation: Boolean, - successCountdown: Int -) { - val haptic = LocalHapticFeedback.current - val successScale = remember { Animatable(0f) } - - LaunchedEffect(showSuccessAnimation) { - if (showSuccessAnimation) { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - successScale.animateTo( - targetValue = 1f, animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow - ) - ) - } - } - - Box( - modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier.padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - // Success animation - AnimatedVisibility( - visible = showSuccessAnimation && successCountdown > 0, enter = scaleIn( - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow - ) - ) + fadeIn(), exit = scaleOut() + fadeOut() - ) { - SendCard( - backgroundColor = MaterialTheme.colorScheme.primaryContainer, - modifier = Modifier.scale(successScale.value) - ) { - Column( - modifier = Modifier.padding(40.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - Icons.Default.CheckCircle, - contentDescription = "Success", - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Text( - text = "Transfer Complete!", - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.Bold - ), - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "$fileCount file${if (fileCount != 1) "s" else ""} sent successfully", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) - ) - } - } - } - - // Action buttons - AnimatedVisibility( - visible = !showSuccessAnimation || successCountdown <= 0, enter = slideInVertically( - initialOffsetY = { it }, - animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) - ) + fadeIn(), exit = slideOutVertically() + fadeOut() - ) { - SendCard( - backgroundColor = MaterialTheme.colorScheme.tertiaryContainer - ) { - Column( - modifier = Modifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - Icons.Default.CheckCircle, - contentDescription = "Complete", - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Text( - text = "Files Sent Successfully!", - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold - ), - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onTertiaryContainer - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "$fileCount file${if (fileCount != 1) "s" else ""} transferred successfully", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f) - ) - - Spacer(modifier = Modifier.height(32.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - SendButton( - onClick = onSendMore, - variant = ButtonVariant.Secondary, - size = ButtonSize.Large, - modifier = Modifier.weight(1f) - ) { - Icon( - Icons.Default.Refresh, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - "Send More", fontWeight = FontWeight.Medium - ) - } - - SendButton( - onClick = onDone, - variant = ButtonVariant.Primary, - size = ButtonSize.Large, - modifier = Modifier.weight(1f) - ) { - Text( - "Done", fontWeight = FontWeight.Medium - ) - } - } - } - } - } - } - } -} - -@Composable -private fun ErrorPhase( - error: SendException?, onRetry: () -> Unit, onCancel: () -> Unit -) { - Box( - modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier.padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - error?.let { err -> - SendErrorCard( - error = err, onAction = if (err.isRecoverable) onRetry else null - ) - } - - SendButton( - onClick = onCancel, - variant = ButtonVariant.Secondary, - size = ButtonSize.Large, - modifier = Modifier.fillMaxWidth() - ) { - Text( - "Cancel", style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Medium - ) - ) - } - } - } -} - -@Composable -private fun SendCard( - modifier: Modifier = Modifier, - backgroundColor: Color = MaterialTheme.colorScheme.surface, - content: @Composable () -> Unit -) { - Card( - modifier = modifier, shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( - containerColor = backgroundColor - ), elevation = CardDefaults.cardElevation( - defaultElevation = 1.dp, pressedElevation = 2.dp - ) - ) { - content() - } -} - -enum class ButtonVariant { Primary, Secondary } -enum class ButtonSize { Medium, Large } - -@Composable -private fun SendButton( - onClick: () -> Unit, - variant: ButtonVariant, - size: ButtonSize, - modifier: Modifier = Modifier, - enabled: Boolean = true, - loading: Boolean = false, - content: @Composable () -> Unit -) { - val height = when (size) { - ButtonSize.Medium -> 44.dp - ButtonSize.Large -> 56.dp - } - - when (variant) { - ButtonVariant.Primary -> { - Button( - onClick = onClick, - modifier = modifier.height(height), - enabled = enabled && !loading, - shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), - disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f) - ), - elevation = ButtonDefaults.buttonElevation( - defaultElevation = 2.dp, pressedElevation = 4.dp, disabledElevation = 0.dp - ) - ) { - if (loading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - Spacer(modifier = Modifier.width(12.dp)) - } - content() - } - } - - ButtonVariant.Secondary -> { - FilledTonalButton( - onClick = onClick, - modifier = modifier.height(height), - enabled = enabled && !loading, - shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer - ) - ) { - if (loading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onSecondaryContainer, - strokeWidth = 2.dp - ) - Spacer(modifier = Modifier.width(12.dp)) - } - content() - } - } - } -} - -@Composable -private fun SendFileItem( - uri: Uri, onRemove: () -> Unit -) { - val context = LocalContext.current - val haptic = LocalHapticFeedback.current - var fileName by remember { mutableStateOf("Loading...") } - var fileSize by remember { mutableStateOf(0L) } - - LaunchedEffect(uri) { - try { - val fileInfo = getFileInfo(context, uri) - fileName = fileInfo.first - fileSize = fileInfo.second - } catch (e: Exception) { - fileName = "Unknown file" - fileSize = 0L - } - } - - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), - tonalElevation = 1.dp - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - TablerIcons.FileText, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.width(12.dp)) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = fileName, - style = MaterialTheme.typography.bodyLarge.copy( - fontWeight = FontWeight.Medium - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - if (fileSize > 0) { - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = formatBytes(fileSize), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - } - } - - IconButton( - onClick = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - onRemove() - }, modifier = Modifier - .size(36.dp) - .clip(CircleShape) - ) { - Icon( - Icons.Default.Delete, - contentDescription = "Remove file", - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.error - ) - } - } - } -} - -@Composable -private fun SendEmptyState( - title: String, description: String, icon: ImageVector -) { - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - ) { - Column( - modifier = Modifier.padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = title, style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold - ), color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = description, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f), - textAlign = TextAlign.Center - ) - } - } -} - -@Composable -private fun SendInstructionsCard() { - SendCard( - backgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - ) { - Column( - modifier = Modifier.padding(20.dp) - ) { - Text( - text = "How to Send Files", style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Bold - ), color = MaterialTheme.colorScheme.onSurface - ) - - Spacer(modifier = Modifier.height(16.dp)) - - val instructions = listOf( - "Select files you want to send", - "Tap 'Send Files' to generate QR code", - "Let the receiver scan the QR code", - "Files transfer automatically" - ) - - instructions.forEachIndexed { index, instruction -> - Row( - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Surface( - modifier = Modifier.size(20.dp), - shape = CircleShape, - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) { - Box(contentAlignment = Alignment.Center) { - Text( - text = "${index + 1}", - style = MaterialTheme.typography.labelSmall.copy( - fontWeight = FontWeight.Bold - ), - color = MaterialTheme.colorScheme.primary - ) - } - } - - Text( - text = instruction, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 2.dp) - ) - } - - if (index < instructions.size - 1) { - Spacer(modifier = Modifier.height(12.dp)) - } - } - } - } -} - -@Composable -private fun SendLoadingIndicator( - message: String? = null -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - CircularProgressIndicator( - modifier = Modifier.size(32.dp), - color = MaterialTheme.colorScheme.primary, - strokeWidth = 3.dp - ) - - message?.let { - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = it, style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium - ), color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center - ) - } - } -} - -@Composable -private fun SendProgressBar( - progress: Float, modifier: Modifier = Modifier -) { - LinearProgressIndicator( - progress = { progress }, - modifier = modifier - .height(6.dp) - .clip(RoundedCornerShape(3.dp)), - color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) - ) -} - -@Composable -private fun SendErrorCard( - error: SendException, onAction: (() -> Unit)? = null -) { - SendCard( - backgroundColor = MaterialTheme.colorScheme.errorContainer - ) { - Column( - modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - imageVector = error.icon, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = error.title, style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold - ), color = MaterialTheme.colorScheme.onErrorContainer, textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = error.message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f), - textAlign = TextAlign.Center - ) - - if (onAction != null && error.actionLabel != null) { - Spacer(modifier = Modifier.height(20.dp)) - - SendButton( - onClick = onAction, variant = ButtonVariant.Primary, size = ButtonSize.Medium - ) { - Text( - error.actionLabel, fontWeight = FontWeight.Medium - ) - } - } - } - } -} - -@Composable -private fun SendErrorOverlay( - error: SendException, onDismiss: () -> Unit, onAction: (String) -> Unit -) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.5f)) - .clickable( - interactionSource = remember { MutableInteractionSource() }, indication = null - ) { onDismiss() }, contentAlignment = Alignment.Center - ) { - SendCard( - modifier = Modifier - .padding(20.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, indication = null - ) { /* Prevent dismiss on card click */ }) { - Column( - modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - imageVector = error.icon, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = error.title, style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold - ), color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = error.message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - if (error.isRecoverable && error.actionLabel != null) { - SendButton( - onClick = { onAction(error.actionLabel) }, - variant = ButtonVariant.Primary, - size = ButtonSize.Medium - ) { - Text( - error.actionLabel, fontWeight = FontWeight.Medium - ) - } - } - - SendButton( - onClick = onDismiss, - variant = ButtonVariant.Secondary, - size = ButtonSize.Medium - ) { - Text( - "Dismiss", fontWeight = FontWeight.Medium - ) - } - } - } - } - } -} - private fun copyToClipboard(context: Context, text: String, label: String = "Transfer Info") { val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipData = ClipData.newPlainText(label, text) @@ -1799,137 +547,3 @@ private fun SendQRDialog( shape = RoundedCornerShape(20.dp) ) } -// Utility Functions with Error Handling - -data class FileValidationResult( - val validFiles: List, val skippedCount: Int -) - -private fun validateAndFilterFiles( - context: android.content.Context, uris: List -): FileValidationResult { - val validFiles = mutableListOf() - var skippedCount = 0 - - uris.forEach { uri -> - try { - val size = getFileSize(context, uri) - if (size > 0 && size <= 2_000_000_000L) { // 2GB limit - validFiles.add(uri) - } else { - skippedCount++ - } - } catch (e: Exception) { - skippedCount++ - } - } - - return FileValidationResult(validFiles, skippedCount) -} - -private fun generateQRCodeSafely(ticket: String, confirmation: UByte): Bitmap { - val writer = QRCodeWriter() - try { - if (ticket.isEmpty()) { - throw IllegalArgumentException("Ticket cannot be empty") - } - - val qrData = "drop://receive?ticket=$ticket&confirmation=$confirmation" - val bitMatrix: BitMatrix = writer.encode(qrData, BarcodeFormat.QR_CODE, 512, 512) - val width = bitMatrix.width - val height = bitMatrix.height - val bitmap = createBitmap(width, height, Bitmap.Config.RGB_565) - - for (x in 0 until width) { - for (y in 0 until height) { - bitmap[x, y] = if (bitMatrix[x, y]) { - android.graphics.Color.BLACK - } else { - android.graphics.Color.WHITE - } - } - } - return bitmap - } catch (e: WriterException) { - throw RuntimeException("QR code generation failed: ${e.message}", e) - } catch (e: Exception) { - throw RuntimeException("Unexpected error during QR code generation: ${e.message}", e) - } -} - -private fun formatBytes(bytes: Long): String { - if (bytes < 0) return "0 B" - if (bytes < 1024) return "$bytes B" - - val kb = bytes / 1024.0 - if (kb < 1024) return "%.1f KB".format(kb) - - val mb = kb / 1024.0 - if (mb < 1024) return "%.1f MB".format(mb) - - val gb = mb / 1024.0 - return "%.1f GB".format(gb) -} - -private fun formatDuration(seconds: Long): String { - return when { - seconds < 60 -> "${seconds}s" - seconds < 3600 -> "${seconds / 60}m ${seconds % 60}s" - else -> "${seconds / 3600}h ${(seconds % 3600) / 60}m" - } -} - -private fun getFileSize(context: android.content.Context, uri: Uri): Long { - return try { - context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val sizeIndex = cursor.getColumnIndex(android.provider.OpenableColumns.SIZE) - if (sizeIndex >= 0) cursor.getLong(sizeIndex) else 0L - } else 0L - } ?: 0L - } catch (e: Exception) { - 0L - } -} - -private fun getFileInfo(context: android.content.Context, uri: Uri): Pair { - return try { - context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME) - val sizeIndex = cursor.getColumnIndex(android.provider.OpenableColumns.SIZE) - - val name = - if (nameIndex >= 0) cursor.getString(nameIndex) ?: "Unknown" else "Unknown" - val size = if (sizeIndex >= 0) cursor.getLong(sizeIndex) else 0L - - Pair(name, size) - } else Pair("Unknown", 0L) - } ?: Pair("Unknown", 0L) - } catch (e: Exception) { - Pair("Unknown", 0L) - } -} - -private fun checkNetworkConnection(context: android.content.Context): Boolean { - return try { - val connectivityManager = - context.getSystemService(android.content.Context.CONNECTIVITY_SERVICE) as? android.net.ConnectivityManager - val activeNetwork = connectivityManager?.activeNetworkInfo - activeNetwork?.isConnected == true - } catch (e: Exception) { - true // Assume connected if we can't check - } -} - -private fun calculateTransferSpeed(bytesTransferred: Long): Long { - // This would be implemented with actual timing data - // For now, return 0 to indicate no speed calculation - return 0L -} - -private fun calculateETA(bytesTransferred: Long, bytesRemaining: Long): Long { - // This would be implemented with actual transfer speed data - // For now, return 0 to indicate no ETA calculation - return 0L -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/SendViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/SendViewModel.kt new file mode 100644 index 0000000..780ebee --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/SendViewModel.kt @@ -0,0 +1,262 @@ +package dev.arkbuilders.drop.app.ui.send + +import android.graphics.Bitmap +import androidx.core.graphics.createBitmap +import androidx.core.graphics.set +import androidx.core.net.toUri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.zxing.BarcodeFormat +import com.google.zxing.WriterException +import com.google.zxing.common.BitMatrix +import com.google.zxing.qrcode.QRCodeWriter +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.arkbuilders.drop.app.TransferManager +import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.app.domain.repository.NetworkStatus +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import timber.log.Timber +import javax.inject.Inject + +sealed class SendScreenState { + data class FileSelection( + val files: List = emptyList(), + val size: Long = 0L, + val canStartTransfer: Boolean = false, + ) : SendScreenState() + + data class GeneratingQR( + val files: List, + ) : SendScreenState() + + data class WaitingForReceiver( + val files: List, + val qrBitmap: Bitmap, + val copyString: String, + ) : SendScreenState() + + data class Transfer( + val files: List, + val isConnected: Boolean = false, + val receiverName: String = "", + val receiverAvatar: String? = null, + val currentFileName: String = "", + val filesCompleted: Int = 0, + val totalFiles: Int = 0, + val bytesTransferred: Long = 0L, + val totalBytes: Long = 0L, + val transferSpeedBps: Long = 0L, + val estimatedTimeRemaining: Long = 0L + ) : SendScreenState() + + data class Complete(val files: List) : SendScreenState() + + data class Error(val error: Throwable) : SendScreenState() +} + +sealed class SendScreenEffect { + data object LaunchFilePicker : SendScreenEffect() + data object NavigateBack : SendScreenEffect() +} + +@HiltViewModel +class SendViewModel @Inject constructor( + private val resourcesHelper: ResourcesHelper, + private val networkStatus: NetworkStatus, + private val transferManager: TransferManager, +): ViewModel(), ContainerHost { + override val container: Container = + container(SendScreenState.FileSelection()) + + fun onAddFiles() = intent { + postSideEffect(SendScreenEffect.LaunchFilePicker) + } + + fun onFilesAdded(newFiles: List) = intent { + val s = state + if (s is SendScreenState.FileSelection) { + val validated = resourcesHelper.validateUris(newFiles) + val allFiles = s.files + validated.first + val canStartTransfer = allFiles.isNotEmpty() && networkStatus.isOnline() + + val size = allFiles.sumOf { resourcesHelper.getFileSize(it) } + + reduce { + s.copy(files = allFiles, size = size, canStartTransfer = canStartTransfer) + } + } + } + + fun onFileRemove(file: String) = intent { + val s = state + if (s is SendScreenState.FileSelection) { + val newFiles = s.files - file + val size = newFiles.sumOf { resourcesHelper.getFileSize(it) } + reduce { + s.copy(files = newFiles, size = size) + } + } + } + + fun onStartTransfer() = intent { + val s = state + if (s !is SendScreenState.FileSelection) { + reduce { + SendScreenState.Error(Error("")) + } + return@intent + } + val bubble = transferManager.sendFiles(s.files.map { it.toUri() }) + if (bubble == null) { + reduce { + SendScreenState.Error(Error("")) + } + return@intent + } + val ticket = transferManager.getCurrentSendTicket() ?: "" + val confirmation = transferManager.getCurrentSendConfirmation() ?: 0u + + if (ticket.isEmpty()) { + reduce { + SendScreenState.Error(Error("")) + } + return@intent + } + val copyString = "${bubble.getTicket()} ${bubble.getConfirmation()}" + + val qrBitmap = generateQRCodeSafely(ticket, confirmation) + if (qrBitmap == null) { + reduce { + SendScreenState.Error(Error("")) + } + return@intent + } + listenToSendProgress() + monitorTransferCompletion() + reduce { + SendScreenState.WaitingForReceiver( + files = s.files, + qrBitmap = qrBitmap, + copyString = copyString, + ) + } + } + + fun onCancelTransfer() = intent { + transferManager.cancelSend() + postSideEffect(SendScreenEffect.NavigateBack) + } + + fun onCancelQrGeneration() = intent { + reduce { + SendScreenState.FileSelection() + } + } + + fun onComplete() = intent { + val s = state + if (s is SendScreenState.Transfer) { + transferManager.recordSendCompletion(s.files.map { it.toUri() }) + reduce { + SendScreenState.Complete(files = s.files) + } + } + } + + fun onDone() = intent { + transferManager.cancelSend() + postSideEffect(SendScreenEffect.NavigateBack) + } + + fun onSendMore() = intent { + transferManager.cancelSend() + reduce { + SendScreenState.FileSelection() + } + } + + private fun listenToSendProgress() { + transferManager.sendProgress!!.onEach { progress -> + intent { + if (progress.isConnected.not()) + return@intent + + val s = state + val files = when (s) { + is SendScreenState.Transfer -> s.files + is SendScreenState.WaitingForReceiver -> s.files + else -> return@intent + } + + val transfer = SendScreenState.Transfer( + files = files, + isConnected = progress.isConnected, + receiverName = progress.receiverName, + receiverAvatar = progress.receiverAvatar, + currentFileName = progress.fileName, + bytesTransferred = progress.sent.toLong(), + totalBytes = (progress.sent + progress.remaining).toLong(), + transferSpeedBps = 0L, + estimatedTimeRemaining = 0L, + ) + + reduce { + transfer + } + } + }.launchIn(viewModelScope) + } + + private fun monitorTransferCompletion() { + viewModelScope.launch { + while (coroutineContext.isActive) { + val isFinished = transferManager.isSendFinished() + if (isFinished) { + onComplete() + break + } + delay(500) + } + } + } + + private fun generateQRCodeSafely(ticket: String, confirmation: UByte): Bitmap? { + val writer = QRCodeWriter() + try { + if (ticket.isEmpty()) { + throw IllegalArgumentException("Ticket cannot be empty") + } + + val qrData = "drop://receive?ticket=$ticket&confirmation=$confirmation" + val bitMatrix: BitMatrix = writer.encode(qrData, BarcodeFormat.QR_CODE, 512, 512) + val width = bitMatrix.width + val height = bitMatrix.height + val bitmap = createBitmap(width, height, Bitmap.Config.RGB_565) + + for (x in 0 until width) { + for (y in 0 until height) { + bitmap[x, y] = if (bitMatrix[x, y]) { + android.graphics.Color.BLACK + } else { + android.graphics.Color.WHITE + } + } + } + return bitmap + } catch (e: WriterException) { + // TODO + // throw RuntimeException("QR code generation failed: ${e.message}", e) + return null + } catch (e: Exception) { + // throw RuntimeException("Unexpected error during QR code generation: ${e.message}", e) + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendButton.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendButton.kt new file mode 100644 index 0000000..1ca727f --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendButton.kt @@ -0,0 +1,87 @@ +package dev.arkbuilders.drop.app.ui.send.components + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +enum class ButtonVariant { Primary, Secondary } +enum class ButtonSize { Medium, Large } + +@Composable +fun SendButton( + onClick: () -> Unit, + variant: ButtonVariant, + size: ButtonSize, + modifier: Modifier = Modifier, + enabled: Boolean = true, + loading: Boolean = false, + content: @Composable () -> Unit +) { + val height = when (size) { + ButtonSize.Medium -> 44.dp + ButtonSize.Large -> 56.dp + } + + when (variant) { + ButtonVariant.Primary -> { + Button( + onClick = onClick, + modifier = modifier.height(height), + enabled = enabled && !loading, + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), + disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f) + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 2.dp, pressedElevation = 4.dp, disabledElevation = 0.dp + ) + ) { + if (loading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.width(12.dp)) + } + content() + } + } + + ButtonVariant.Secondary -> { + FilledTonalButton( + onClick = onClick, + modifier = modifier.height(height), + enabled = enabled && !loading, + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) + ) { + if (loading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onSecondaryContainer, + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.width(12.dp)) + } + content() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendCard.kt new file mode 100644 index 0000000..d6a59e4 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendCard.kt @@ -0,0 +1,27 @@ +package dev.arkbuilders.drop.app.ui.send.components + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun SendCard( + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colorScheme.surface, + content: @Composable () -> Unit +) { + Card( + modifier = modifier, shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( + containerColor = backgroundColor + ), elevation = CardDefaults.cardElevation( + defaultElevation = 1.dp, pressedElevation = 2.dp + ) + ) { + content() + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendEmptyState.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendEmptyState.kt new file mode 100644 index 0000000..adec2e1 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendEmptyState.kt @@ -0,0 +1,59 @@ +package dev.arkbuilders.drop.app.ui.send.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun SendEmptyState( + title: String, description: String, icon: ImageVector +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) { + Column( + modifier = Modifier.padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = title, style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold + ), color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f), + textAlign = TextAlign.Center + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendErrorOverlay.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendErrorOverlay.kt new file mode 100644 index 0000000..cdb57a3 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendErrorOverlay.kt @@ -0,0 +1,104 @@ +package dev.arkbuilders.drop.app.ui.send.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.ui.send.SendException + +@Composable +fun SendErrorOverlay( + error: SendException, onDismiss: () -> Unit, onAction: (String) -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, indication = null + ) { onDismiss() }, contentAlignment = Alignment.Center + ) { + SendCard( + modifier = Modifier + .padding(20.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, indication = null + ) { /* Prevent dismiss on card click */ }) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = error.icon, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.error + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = error.title, style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold + ), color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = error.message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (error.isRecoverable && error.actionLabel != null) { + SendButton( + onClick = { onAction(error.actionLabel) }, + variant = ButtonVariant.Primary, + size = ButtonSize.Medium + ) { + Text( + error.actionLabel, fontWeight = FontWeight.Medium + ) + } + } + + SendButton( + onClick = onDismiss, + variant = ButtonVariant.Secondary, + size = ButtonSize.Medium + ) { + Text( + "Dismiss", fontWeight = FontWeight.Medium + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendFileItem.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendFileItem.kt new file mode 100644 index 0000000..6dc1779 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendFileItem.kt @@ -0,0 +1,125 @@ +package dev.arkbuilders.drop.app.ui.send.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import compose.icons.TablerIcons +import compose.icons.tablericons.FileText +import dagger.hilt.android.EntryPointAccessors +import dev.arkbuilders.drop.app.di.TmpEntryPoint +import dev.arkbuilders.drop.app.ui.DisplayUtils.formatBytes + +@Composable +fun SendFileItem( + uri: String, onRemove: () -> Unit +) { + val context = LocalContext.current + val haptic = LocalHapticFeedback.current + var fileName by remember { mutableStateOf("Loading...") } + var fileSize by remember { mutableStateOf(0L) } + + LaunchedEffect(uri) { + try { + val resourcesHelper = EntryPointAccessors.fromApplication( + context.applicationContext, + TmpEntryPoint::class.java + ).resourcesHelper() + + fileName = resourcesHelper.getFileName(uri) ?: "Unknown file" + fileSize = resourcesHelper.getFileSize(uri) + } catch (e: Exception) { + fileName = "Unknown file" + fileSize = 0L + } + } + + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), + tonalElevation = 1.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + TablerIcons.FileText, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = fileName, + style = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + if (fileSize > 0) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = formatBytes(fileSize), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + + IconButton( + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onRemove() + }, modifier = Modifier + .size(36.dp) + .clip(CircleShape) + ) { + Icon( + Icons.Default.Delete, + contentDescription = "Remove file", + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.error + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendInstructionsCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendInstructionsCard.kt new file mode 100644 index 0000000..5b32241 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendInstructionsCard.kt @@ -0,0 +1,79 @@ +package dev.arkbuilders.drop.app.ui.send.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun SendInstructionsCard() { + SendCard( + backgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) { + Column( + modifier = Modifier.padding(20.dp) + ) { + Text( + text = "How to Send Files", style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold + ), color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(16.dp)) + + val instructions = listOf( + "Select files you want to send", + "Tap 'Send Files' to generate QR code", + "Let the receiver scan the QR code", + "Files transfer automatically" + ) + + instructions.forEachIndexed { index, instruction -> + Row( + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Surface( + modifier = Modifier.size(20.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) + ) { + Box(contentAlignment = Alignment.Center) { + Text( + text = "${index + 1}", + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.Bold + ), + color = MaterialTheme.colorScheme.primary + ) + } + } + + Text( + text = instruction, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 2.dp) + ) + } + + if (index < instructions.size - 1) { + Spacer(modifier = Modifier.height(12.dp)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendLoadingIndicator.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendLoadingIndicator.kt new file mode 100644 index 0000000..3cf4e18 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendLoadingIndicator.kt @@ -0,0 +1,39 @@ +package dev.arkbuilders.drop.app.ui.send.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun SendLoadingIndicator( + message: String? = null +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + modifier = Modifier.size(32.dp), + color = MaterialTheme.colorScheme.primary, + strokeWidth = 3.dp + ) + + message?.let { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = it, style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium + ), color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendProgressBar.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendProgressBar.kt new file mode 100644 index 0000000..70d6017 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendProgressBar.kt @@ -0,0 +1,24 @@ +package dev.arkbuilders.drop.app.ui.send.components + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp + +@Composable +fun SendProgressBar( + progress: Float, modifier: Modifier = Modifier +) { + LinearProgressIndicator( + progress = { progress }, + modifier = modifier + .height(6.dp) + .clip(RoundedCornerShape(3.dp)), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) + ) +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/ErrorPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/ErrorPhase.kt new file mode 100644 index 0000000..1582361 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/ErrorPhase.kt @@ -0,0 +1,108 @@ +package dev.arkbuilders.drop.app.ui.send.components.phase + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.ui.send.SendException +import dev.arkbuilders.drop.app.ui.send.components.ButtonSize +import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant +import dev.arkbuilders.drop.app.ui.send.components.SendButton +import dev.arkbuilders.drop.app.ui.send.components.SendCard + +@Composable +private fun ErrorPhase( + error: SendException?, onRetry: () -> Unit, onCancel: () -> Unit +) { + Box( + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + error?.let { err -> + SendErrorCard( + error = err, onAction = if (err.isRecoverable) onRetry else null + ) + } + + SendButton( + onClick = onCancel, + variant = ButtonVariant.Secondary, + size = ButtonSize.Large, + modifier = Modifier.fillMaxWidth() + ) { + Text( + "Cancel", style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Medium + ) + ) + } + } + } +} + +@Composable +private fun SendErrorCard( + error: SendException, onAction: (() -> Unit)? = null +) { + SendCard( + backgroundColor = MaterialTheme.colorScheme.errorContainer + ) { + Column( + modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = error.icon, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.error + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = error.title, style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold + ), color = MaterialTheme.colorScheme.onErrorContainer, textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = error.message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f), + textAlign = TextAlign.Center + ) + + if (onAction != null && error.actionLabel != null) { + Spacer(modifier = Modifier.height(20.dp)) + + SendButton( + onClick = onAction, variant = ButtonVariant.Primary, size = ButtonSize.Medium + ) { + Text( + error.actionLabel, fontWeight = FontWeight.Medium + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/FileSelectionPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/FileSelectionPhase.kt new file mode 100644 index 0000000..902d755 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/FileSelectionPhase.kt @@ -0,0 +1,157 @@ +package dev.arkbuilders.drop.app.ui.send.components.phase + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import compose.icons.TablerIcons +import compose.icons.tablericons.FileText +import compose.icons.tablericons.Plus +import dev.arkbuilders.drop.app.ui.DisplayUtils.formatBytes +import dev.arkbuilders.drop.app.ui.send.components.ButtonSize +import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant +import dev.arkbuilders.drop.app.ui.send.components.SendButton +import dev.arkbuilders.drop.app.ui.send.components.SendCard +import dev.arkbuilders.drop.app.ui.send.components.SendEmptyState +import dev.arkbuilders.drop.app.ui.send.components.SendFileItem +import dev.arkbuilders.drop.app.ui.send.components.SendInstructionsCard + +@Composable +fun FileSelectionPhase( + selectedFiles: List, + totalFileSize: Long, + onAddFiles: () -> Unit, + onRemoveFile: (String) -> Unit, + onStartTransfer: () -> Unit, + canStartTransfer: Boolean, + listState: LazyListState +) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(20.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + // File selection section + item { + SendCard { + Column( + modifier = Modifier.padding(20.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = "Selected Files", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold + ), + color = MaterialTheme.colorScheme.onSurface + ) + + if (selectedFiles.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "${selectedFiles.size} file${if (selectedFiles.size != 1) "s" else ""} • ${ + formatBytes( + totalFileSize + ) + }", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + SendButton( + onClick = onAddFiles, + variant = ButtonVariant.Secondary, + size = ButtonSize.Medium + ) { + Icon( + TablerIcons.Plus, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Add Files", style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Medium + ) + ) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + if (selectedFiles.isEmpty()) { + SendEmptyState( + title = "No Files Selected", + description = "Tap 'Add Files' to choose files you want to send.", + icon = TablerIcons.FileText + ) + } else { + LazyColumn( + modifier = Modifier.heightIn(max = 300.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(selectedFiles) { uri -> + SendFileItem( + uri = uri, onRemove = { onRemoveFile(uri) }) + } + } + } + } + } + } + + // Transfer button + item { + SendButton( + onClick = onStartTransfer, + variant = ButtonVariant.Primary, + size = ButtonSize.Large, + enabled = canStartTransfer, + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + ) { + Text( + text = when { + selectedFiles.isEmpty() -> "Select Files First" + else -> "Send ${selectedFiles.size} File${if (selectedFiles.size != 1) "s" else ""}" + }, style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold + ) + ) + } + } + + // Instructions + item { + SendInstructionsCard() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/GeneratingQRPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/GeneratingQRPhase.kt new file mode 100644 index 0000000..ecb32ce --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/GeneratingQRPhase.kt @@ -0,0 +1,46 @@ +package dev.arkbuilders.drop.app.ui.send.components.phase + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.ui.send.components.ButtonSize +import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant +import dev.arkbuilders.drop.app.ui.send.components.SendButton +import dev.arkbuilders.drop.app.ui.send.components.SendCard +import dev.arkbuilders.drop.app.ui.send.components.SendLoadingIndicator + +@Composable +fun GeneratingQRPhase( + onCancel: () -> Unit +) { + Box( + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center + ) { + SendCard { + Column( + modifier = Modifier.padding(40.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + SendLoadingIndicator( + message = "Generating QR Code..." + ) + + Spacer(modifier = Modifier.height(32.dp)) + + SendButton( + onClick = onCancel, variant = ButtonVariant.Secondary, size = ButtonSize.Medium + ) { + Text("Cancel") + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferCompletePhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferCompletePhase.kt new file mode 100644 index 0000000..bcf55f8 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferCompletePhase.kt @@ -0,0 +1,157 @@ +package dev.arkbuilders.drop.app.ui.send.components.phase + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.ui.send.components.ButtonSize +import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant +import dev.arkbuilders.drop.app.ui.send.components.SendButton +import dev.arkbuilders.drop.app.ui.send.components.SendCard + +@Composable +fun TransferCompletePhase( + fileCount: Int, + onSendMore: () -> Unit, + onDone: () -> Unit, +) { + val haptic = LocalHapticFeedback.current + + Box( + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + SendCard( + backgroundColor = MaterialTheme.colorScheme.primaryContainer, + modifier = Modifier + ) { + Column( + modifier = Modifier.padding(40.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = "Success", + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = "Transfer Complete!", + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold + ), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "$fileCount file${if (fileCount != 1) "s" else ""} sent successfully", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) + ) + } + } + + SendCard( + backgroundColor = MaterialTheme.colorScheme.tertiaryContainer + ) { + Column( + modifier = Modifier.padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = "Complete", + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = "Files Sent Successfully!", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold + ), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onTertiaryContainer + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "$fileCount file${if (fileCount != 1) "s" else ""} transferred successfully", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + SendButton( + onClick = onSendMore, + variant = ButtonVariant.Secondary, + size = ButtonSize.Large, + modifier = Modifier.weight(1f) + ) { + Icon( + Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Send More", fontWeight = FontWeight.Medium + ) + } + + SendButton( + onClick = onDone, + variant = ButtonVariant.Primary, + size = ButtonSize.Large, + modifier = Modifier.weight(1f) + ) { + Text( + "Done", fontWeight = FontWeight.Medium + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt new file mode 100644 index 0000000..6ea6ba9 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt @@ -0,0 +1,163 @@ +package dev.arkbuilders.drop.app.ui.send.components.phase + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.ui.DisplayUtils.formatBytes +import dev.arkbuilders.drop.app.ui.DisplayUtils.formatDuration +import dev.arkbuilders.drop.app.ui.profile.AvatarUtils +import dev.arkbuilders.drop.app.ui.send.SendScreenState +import dev.arkbuilders.drop.app.ui.send.components.SendCard +import dev.arkbuilders.drop.app.ui.send.components.SendProgressBar + +@Composable +fun TransferringPhase( + progress: SendScreenState.Transfer?, onCancel: () -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(20.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + item { + progress?.let { p -> + SendCard( + backgroundColor = MaterialTheme.colorScheme.primaryContainer + ) { + Column( + modifier = Modifier.padding(24.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Sending Files", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold + ), + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + + IconButton( + onClick = onCancel, + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + ) { + Icon( + Icons.Default.Close, + contentDescription = "Cancel transfer", + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + if (p.receiverName.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + AvatarUtils.AvatarImageWithFallback( + base64String = p.receiverAvatar ?: "", + fallbackText = p.receiverName, + size = 32.dp + ) + + Text( + text = "Connected to: ${p.receiverName}", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) + ) + } + } + + if (p.currentFileName.isNotEmpty()) { + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = "Sending: ${p.currentFileName}", + style = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium + ), + color = MaterialTheme.colorScheme.onPrimaryContainer, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + val progressValue = if (p.totalBytes > 0) { + (p.bytesTransferred.toFloat() / p.totalBytes.toFloat()).coerceIn( + 0f, 1f + ) + } else 0f + + Spacer(modifier = Modifier.height(16.dp)) + + SendProgressBar( + progress = progressValue, modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "${formatBytes(p.bytesTransferred)} / ${formatBytes(p.totalBytes)}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + ) + + if (p.transferSpeedBps > 0) { + Text( + text = "${formatBytes(p.transferSpeedBps)}/s", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.7f + ) + ) + } + } + + if (p.estimatedTimeRemaining > 0) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Time remaining: ${formatDuration(p.estimatedTimeRemaining)}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f) + ) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/WaitingForReceiverPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/WaitingForReceiverPhase.kt new file mode 100644 index 0000000..110db29 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/WaitingForReceiverPhase.kt @@ -0,0 +1,92 @@ +package dev.arkbuilders.drop.app.ui.send.components.phase + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.ui.send.components.ButtonSize +import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant +import dev.arkbuilders.drop.app.ui.send.components.SendButton +import dev.arkbuilders.drop.app.ui.send.components.SendCard +import dev.arkbuilders.drop.app.ui.send.components.SendLoadingIndicator + +@Composable +fun WaitingForReceiverPhase( + fileCount: Int, onCancel: () -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + item { + SendCard { + Column( + modifier = Modifier.padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Ready to Send", style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold + ), color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "$fileCount file${if (fileCount != 1) "s" else ""} ready for transfer", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + SendLoadingIndicator() + Text( + text = "Waiting for receiver to scan...", + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium + ), + color = MaterialTheme.colorScheme.primary + ) + } + } + } + } + + item { + SendButton( + onClick = onCancel, + variant = ButtonVariant.Secondary, + size = ButtonSize.Large, + modifier = Modifier.fillMaxWidth() + ) { + Text( + "Cancel Transfer", style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Medium + ) + ) + } + } + } +} \ No newline at end of file From fa58ac0fc557a080be48be10c49cf366fa21cb1c Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Sat, 1 Nov 2025 03:38:30 +0600 Subject: [PATCH 07/33] Refactor Profile screen --- .../dev/arkbuilders/drop/app/MainActivity.kt | 1 - .../data/datasource/ProfileLocalDataSource.kt | 19 +- .../drop/app/data/helper/AvatarHelperImpl.kt | 102 +++++ .../data/{ => helper}/ResourcesHelperImpl.kt | 7 +- .../drop/app/data/model/UserProfileDto.kt | 4 +- .../app/data/repository/ProfileRepoImpl.kt | 13 +- .../dev/arkbuilders/drop/app/di/AppModule.kt | 10 +- .../arkbuilders/drop/app/di/TmpEntryPoint.kt | 2 + .../drop/app/domain/AvatarHelper.kt | 15 + .../drop/app/domain/model/UserAvatar.kt | 16 + .../drop/app/domain/model/UserProfile.kt | 5 +- .../drop/app/domain/repository/ProfileRepo.kt | 4 +- .../app/domain/usecase/ReceiveFilesUseCase.kt | 2 +- .../app/domain/usecase/SendFilesUseCase.kt | 2 +- .../dev/arkbuilders/drop/app/ui/Home/Home.kt | 12 +- .../drop/app/ui/components/AvatarImage.kt | 106 +++++ .../drop/app/ui/history/History.kt | 6 +- .../drop/app/ui/profile/AvatarUtils.kt | 209 --------- .../app/ui/profile/AvatarUtilsEnhanced.kt | 252 ---------- .../drop/app/ui/profile/EditProfile.kt | 281 ------------ ...rofileEnhanced.kt => EditProfileScreen.kt} | 429 ++++++------------ .../app/ui/profile/EditProfileViewModel.kt | 113 +++++ .../receive/components/ReceiveProgressCard.kt | 6 +- .../components/phase/TransferringPhase.kt | 6 +- 24 files changed, 547 insertions(+), 1075 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt rename app/src/main/java/dev/arkbuilders/drop/app/data/{ => helper}/ResourcesHelperImpl.kt (89%) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/components/AvatarImage.kt delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/profile/AvatarUtils.kt delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/profile/AvatarUtilsEnhanced.kt delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfile.kt rename app/src/main/java/dev/arkbuilders/drop/app/ui/profile/{EditProfileEnhanced.kt => EditProfileScreen.kt} (64%) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt diff --git a/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt index fdc58d0..42106b5 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt @@ -108,7 +108,6 @@ fun DropNavigation( composable(DropDestination.EditProfile.route) { EditProfileEnhanced( navController = navController, - profileRepo = profileRepo, ) } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt index 73a4684..0d276fd 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt @@ -3,16 +3,18 @@ package dev.arkbuilders.drop.app.data.datasource import android.content.Context import android.content.SharedPreferences import dagger.hilt.android.qualifiers.ApplicationContext -import dev.arkbuilders.drop.app.ui.profile.AvatarUtils import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import javax.inject.Inject import androidx.core.content.edit import dev.arkbuilders.drop.app.data.model.UserProfileDto +import dev.arkbuilders.drop.app.domain.AvatarHelper +import dev.arkbuilders.drop.app.domain.model.UserAvatar import dev.arkbuilders.drop.app.domain.model.UserProfile class ProfileLocalDataSource @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + private val avatarHelper: AvatarHelper, ) { private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -35,10 +37,13 @@ class ProfileLocalDataSource @Inject constructor( } private fun createDefaultProfile(): UserProfile { + val defaultAvatarId = "avatar_00" val default = UserProfile( name = "Anonymous", - avatarB64 = AvatarUtils.getDefaultAvatarBase64(context, "avatar_00"), - avatarId = "avatar_00" + avatar = UserAvatar( + base64 = avatarHelper.getDefaultAvatarBase64(defaultAvatarId), + predefinedId = defaultAvatarId, + ) ) saveProfile(default) return default @@ -59,12 +64,10 @@ class ProfileLocalDataSource @Inject constructor( private fun UserProfileDto.toDomain() = UserProfile( name = name, - avatarB64 = avatarB64, - avatarId = avatarId, + avatar = avatar ) private fun UserProfile.toDto() = UserProfileDto( name = name, - avatarB64 = avatarB64, - avatarId = avatarId, + avatar = avatar ) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt new file mode 100644 index 0000000..6a48ba1 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt @@ -0,0 +1,102 @@ +package dev.arkbuilders.drop.app.data.helper + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.ImageDecoder +import android.net.Uri +import android.util.Base64 +import androidx.core.graphics.scale +import androidx.core.net.toUri +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.arkbuilders.drop.app.domain.AvatarHelper +import dev.arkbuilders.drop.app.domain.AvatarHelper.Companion.JPEG_QUALITY +import dev.arkbuilders.drop.app.domain.AvatarHelper.Companion.MAX_FILE_SIZE +import dev.arkbuilders.drop.app.domain.AvatarHelper.Companion.MAX_IMAGE_SIZE +import java.io.ByteArrayOutputStream +import java.io.IOException +import javax.inject.Inject + +class AvatarHelperImpl @Inject constructor( + @ApplicationContext + private val context: Context, +) : AvatarHelper { + + override fun uriToBase64(uri: String): String? { + return try { + val bitmap = loadBitmapFromUri(uri.toUri()) ?: return null + val optimizedBitmap = optimizeBitmap(bitmap) + bitmapToBase64(optimizedBitmap) + } catch (e: Exception) { + null + } + } + + + private fun loadBitmapFromUri(uri: Uri): Bitmap? { + return try { + val source = ImageDecoder.createSource(context.contentResolver, uri) + ImageDecoder.decodeBitmap(source) + } catch (e: IOException) { + null + } catch (e: SecurityException) { + null + } + } + + private fun optimizeBitmap(bitmap: Bitmap): Bitmap { + val width = bitmap.width + val height = bitmap.height + + // Calculate scaling factor + val scaleFactor = if (width > height) { + MAX_IMAGE_SIZE.toFloat() / width + } else { + MAX_IMAGE_SIZE.toFloat() / height + } + + return if (scaleFactor < 1f) { + val newWidth = (width * scaleFactor).toInt() + val newHeight = (height * scaleFactor).toInt() + bitmap.scale(newWidth, newHeight) + } else { + bitmap + } + } + + private fun bitmapToBase64(bitmap: Bitmap): String? { + return try { + val outputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, outputStream) + val byteArray = outputStream.toByteArray() + + // Check file size + if (byteArray.size > MAX_FILE_SIZE) { + return null + } + + Base64.encodeToString(byteArray, Base64.DEFAULT) + } catch (e: Exception) { + null + } + } + + + @SuppressLint("DiscouragedApi") + override fun getDefaultAvatarBase64(avatarId: String): String { + return try { + val resourceId = context.resources.getIdentifier( + avatarId, "drawable", context.packageName + ) + if (resourceId != 0) { + val bitmap = BitmapFactory.decodeResource(context.resources, resourceId) + bitmapToBase64(bitmap) ?: "" + } else { + "" + } + } catch (e: Exception) { + "" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt similarity index 89% rename from app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt rename to app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt index ab66ba4..7fd137d 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/ResourcesHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt @@ -1,7 +1,6 @@ -package dev.arkbuilders.drop.app.data +package dev.arkbuilders.drop.app.data.helper import android.content.Context -import android.net.Uri import android.provider.OpenableColumns import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext @@ -24,7 +23,7 @@ class ResourcesHelperImpl @Inject constructor( } else null } } catch (e: Exception) { - Timber.e(e, "Error getting filename for URI: $uri") + Timber.Forest.e(e, "Error getting filename for URI: $uri") null } } @@ -53,7 +52,7 @@ class ResourcesHelperImpl @Inject constructor( return try { context.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor -> if (cursor.moveToFirst()) { - val sizeIndex = cursor.getColumnIndex(android.provider.OpenableColumns.SIZE) + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) if (sizeIndex >= 0) cursor.getLong(sizeIndex) else 0L } else 0L } ?: 0L diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt index cb6079c..c071148 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt @@ -1,10 +1,10 @@ package dev.arkbuilders.drop.app.data.model +import dev.arkbuilders.drop.app.domain.model.UserAvatar import kotlinx.serialization.Serializable @Serializable data class UserProfileDto( val name: String, - val avatarB64: String, - val avatarId: String + val avatar: UserAvatar, ) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt index fd0ed69..1284115 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt @@ -3,9 +3,9 @@ package dev.arkbuilders.drop.app.data.repository import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.app.data.datasource.ProfileLocalDataSource +import dev.arkbuilders.drop.app.domain.model.UserAvatar import dev.arkbuilders.drop.app.domain.model.UserProfile import dev.arkbuilders.drop.app.domain.repository.ProfileRepo -import dev.arkbuilders.drop.app.ui.profile.AvatarUtils import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -15,7 +15,7 @@ import javax.inject.Singleton @Singleton class ProfileRepoImpl @Inject constructor( private val localDataSource: ProfileLocalDataSource, - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, ) : ProfileRepo { private val _profile = MutableStateFlow(localDataSource.loadProfile()) @@ -30,12 +30,7 @@ class ProfileRepoImpl @Inject constructor( updateProfile(_profile.value.copy(name = name)) } - override fun updateAvatar(avatarId: String) { - val avatarB64 = AvatarUtils.getDefaultAvatarBase64(context, avatarId) - updateProfile(_profile.value.copy(avatarId = avatarId, avatarB64 = avatarB64)) - } - - override fun updateCustomAvatar(base64: String) { - updateProfile(_profile.value.copy(avatarId = "custom", avatarB64 = base64)) + override fun updateAvatar(avatar: UserAvatar) { + updateProfile(_profile.value.copy(avatar = avatar)) } } \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index 9bff940..fd99fbc 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -7,12 +7,14 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import dev.arkbuilders.drop.app.TransferManager -import dev.arkbuilders.drop.app.data.ResourcesHelperImpl +import dev.arkbuilders.drop.app.data.helper.ResourcesHelperImpl import dev.arkbuilders.drop.app.data.db.Database +import dev.arkbuilders.drop.app.data.helper.AvatarHelperImpl import dev.arkbuilders.drop.app.data.helper.PermissionsHelperImpl import dev.arkbuilders.drop.app.data.repository.NetworkStatusImpl import dev.arkbuilders.drop.app.data.repository.ProfileRepoImpl import dev.arkbuilders.drop.app.data.repository.TransferHistoryItemRepositoryImpl +import dev.arkbuilders.drop.app.domain.AvatarHelper import dev.arkbuilders.drop.app.domain.PermissionsHelper import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.domain.repository.NetworkStatus @@ -72,4 +74,10 @@ object AppModule { fun provideNetworkStatus( impl: NetworkStatusImpl ): NetworkStatus = impl + + @Provides + @Singleton + fun provideAvatarHelper( + impl: AvatarHelperImpl + ): AvatarHelper = impl } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt index 79005e8..28ac3ee 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt @@ -3,6 +3,7 @@ package dev.arkbuilders.drop.app.di import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dev.arkbuilders.drop.app.domain.AvatarHelper import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase @@ -10,6 +11,7 @@ import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase @EntryPoint @InstallIn(SingletonComponent::class) interface TmpEntryPoint { + fun avatarHelper(): AvatarHelper fun resourcesHelper(): ResourcesHelper fun sendFilesUseCase(): SendFilesUseCase fun receiveFilesUseCase(): ReceiveFilesUseCase diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt new file mode 100644 index 0000000..5a89d67 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt @@ -0,0 +1,15 @@ +package dev.arkbuilders.drop.app.domain + +import android.content.Context +import android.net.Uri + +interface AvatarHelper { + fun uriToBase64(uri: String): String? + fun getDefaultAvatarBase64(avatarId: String): String + + companion object { + const val MAX_IMAGE_SIZE = 512 // Maximum width/height in pixels + const val JPEG_QUALITY = 85 // JPEG compression quality + const val MAX_FILE_SIZE = 500 * 1024 // 500KB max file size + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt new file mode 100644 index 0000000..cb558cd --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt @@ -0,0 +1,16 @@ +package dev.arkbuilders.drop.app.domain.model + +import kotlinx.serialization.Serializable + +@Serializable +data class UserAvatar( + val base64: String, + val predefinedId: String? +) { + companion object { + val predefinedIds = listOf( + "avatar_00", "avatar_01", "avatar_02", "avatar_03", + "avatar_04", "avatar_05", "avatar_06", "avatar_07", "avatar_08" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt index b6a98d5..fbaa45c 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt @@ -2,10 +2,9 @@ package dev.arkbuilders.drop.app.domain.model data class UserProfile( val name: String, - val avatarB64: String, - val avatarId: String, + val avatar: UserAvatar, ) { companion object { - fun empty() = UserProfile("", "", "") + fun empty() = UserProfile("", UserAvatar("", null)) } } \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt index c933a8f..a623116 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt @@ -1,5 +1,6 @@ package dev.arkbuilders.drop.app.domain.repository +import dev.arkbuilders.drop.app.domain.model.UserAvatar import dev.arkbuilders.drop.app.domain.model.UserProfile import kotlinx.coroutines.flow.StateFlow @@ -8,6 +9,5 @@ interface ProfileRepo { fun getCurrentProfile() = profile.value fun updateProfile(profile: UserProfile) fun updateName(name: String) - fun updateAvatar(avatarId: String) - fun updateCustomAvatar(base64: String) + fun updateAvatar(avatar: UserAvatar) } \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt index 24b250a..bf6bcd2 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt @@ -29,7 +29,7 @@ class ReceiveFilesUseCase @Inject constructor( val profile = profileRepo.getCurrentProfile() val receiverProfile = ReceiverProfile( name = profile.name.ifEmpty { "Anonymous" }, - avatarB64 = profile.avatarB64.takeIf { it.isNotEmpty() } + avatarB64 = profile.avatar.base64.takeIf { it.isNotEmpty() } ) val request = ReceiveFilesRequest( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt index bf2bee9..a468a91 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt @@ -31,7 +31,7 @@ class SendFilesUseCase @Inject constructor( val profile = profileRepo.getCurrentProfile() val senderProfile = SenderProfile( name = profile.name.ifEmpty { "Anonymous" }, - avatarB64 = profile.avatarB64.takeIf { it.isNotEmpty() } + avatarB64 = profile.avatar.base64.takeIf { it.isNotEmpty() } ) val senderFiles = fileUris.mapNotNull { uri -> diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt index f6e71d8..a338755 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt @@ -50,6 +50,8 @@ import dev.arkbuilders.drop.app.domain.model.UserProfile import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import dev.arkbuilders.drop.app.navigation.DropDestination +import dev.arkbuilders.drop.app.ui.components.AvatarImage +import dev.arkbuilders.drop.app.ui.components.AvatarImageWithFallback import dev.arkbuilders.drop.app.ui.components.DropButton import dev.arkbuilders.drop.app.ui.components.DropButtonSize import dev.arkbuilders.drop.app.ui.components.DropButtonVariant @@ -59,7 +61,6 @@ import dev.arkbuilders.drop.app.ui.components.DropCardSize import dev.arkbuilders.drop.app.ui.components.DropCardVariant import dev.arkbuilders.drop.app.ui.components.DropOutlinedButton import dev.arkbuilders.drop.app.ui.components.EmptyState -import dev.arkbuilders.drop.app.ui.profile.AvatarUtils import dev.arkbuilders.drop.app.ui.theme.DesignTokens import kotlinx.coroutines.delay import org.orbitmvi.orbit.compose.collectAsState @@ -176,14 +177,13 @@ private fun HeaderSection( } } - // Profile access IconButton( onClick = onProfileClick, modifier = Modifier.semantics { contentDescription = "Open profile settings" } ) { - AvatarUtils.AvatarImageWithFallback(profile.avatarB64) + AvatarImage(avatarB64 = profile.avatar.base64) } } } @@ -362,10 +362,10 @@ private fun EnhancedTransferHistoryCard(item: TransferHistoryItem) { } // Peer avatar - AvatarUtils.AvatarImageWithFallback( - base64String = item.peerAvatar, + AvatarImageWithFallback( + avatarB64 = item.peerAvatar, fallbackText = item.peerName, - size = 40.dp + size = 40.dp, ) } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/AvatarImage.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/components/AvatarImage.kt new file mode 100644 index 0000000..86f62b1 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/components/AvatarImage.kt @@ -0,0 +1,106 @@ +package dev.arkbuilders.drop.app.ui.components + +import android.graphics.BitmapFactory +import android.util.Base64 +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun AvatarImage( + avatarB64: String, + modifier: Modifier = Modifier, + contentDescription: String? = null +) { + val imageBytes = Base64.decode(avatarB64, Base64.DEFAULT) + val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + + if (bitmap != null) { + Image( + painter = BitmapPainter(bitmap.asImageBitmap()), + contentDescription = contentDescription, + modifier = modifier + .clip(CircleShape) + .semantics { + this.contentDescription = contentDescription ?: "Profile avatar" + }, + contentScale = ContentScale.Crop + ) + } else { + AvatarFallback(modifier = modifier, contentDescription = contentDescription) + } +} + +@Composable +fun AvatarImageWithFallback( + avatarB64: String?, + fallbackText: String = "", + size: Dp = 48.dp, + contentDescription: String? = null +) { + if (avatarB64 != null && avatarB64.isNotEmpty()) { + AvatarImage( + avatarB64 = avatarB64, + modifier = Modifier.size(size), + contentDescription = contentDescription + ) + } else { + AvatarFallback( + modifier = Modifier.size(size), + fallbackText = fallbackText, + contentDescription = contentDescription + ) + } +} + +@Composable +private fun AvatarFallback( + modifier: Modifier = Modifier, + fallbackText: String = "", + contentDescription: String? = null +) { + Box( + modifier = modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer) + .semantics { + this.contentDescription = contentDescription ?: "Default profile avatar" + }, + contentAlignment = Alignment.Center + ) { + if (fallbackText.isNotEmpty()) { + androidx.compose.material3.Text( + text = fallbackText.take(2).uppercase(), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } else { + Icon( + Icons.Default.Person, + contentDescription = null, + modifier = Modifier.fillMaxSize(0.6f), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt index 9722532..ed1e3d1 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt @@ -47,7 +47,7 @@ import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository -import dev.arkbuilders.drop.app.ui.profile.AvatarUtils +import dev.arkbuilders.drop.app.ui.components.AvatarImageWithFallback import org.orbitmvi.orbit.compose.collectAsState import java.time.Duration import java.time.OffsetDateTime @@ -213,8 +213,8 @@ private fun HistoryItemCard( verticalAlignment = Alignment.CenterVertically ) { // Peer avatar - AvatarUtils.AvatarImageWithFallback( - base64String = item.peerAvatar, + AvatarImageWithFallback( + avatarB64 = item.peerAvatar, fallbackText = item.peerName, size = 48.dp ) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/AvatarUtils.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/AvatarUtils.kt deleted file mode 100644 index ca1b5cc..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/AvatarUtils.kt +++ /dev/null @@ -1,209 +0,0 @@ -//package dev.arkbuilders.drop.app.ui.profile -// -//import android.content.Context -//import android.graphics.Bitmap -//import android.graphics.BitmapFactory -//import android.graphics.Canvas -//import android.graphics.Paint -//import android.graphics.PorterDuff -//import android.graphics.PorterDuffXfermode -//import android.graphics.Rect -//import android.graphics.RectF -//import android.net.Uri -//import android.util.Base64 -//import androidx.compose.foundation.Image -//import androidx.compose.foundation.background -//import androidx.compose.foundation.layout.Box -//import androidx.compose.foundation.layout.size -//import androidx.compose.foundation.shape.CircleShape -//import androidx.compose.material3.MaterialTheme -//import androidx.compose.runtime.Composable -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.draw.clip -//import androidx.compose.ui.graphics.asImageBitmap -//import androidx.compose.ui.layout.ContentScale -//import androidx.compose.ui.unit.Dp -//import androidx.compose.ui.unit.dp -//import java.io.ByteArrayOutputStream -//import java.io.InputStream -// -//object AvatarUtils { -// private const val AVATAR_SIZE = 512 -// private const val COMPRESSION_QUALITY = 85 -// -// fun getDefaultAvatarBase64(context: Context, avatarId: String): String { -// return try { -// val resourceId = context.resources.getIdentifier( -// avatarId, -// "drawable", -// context.packageName -// ) -// -// if (resourceId != 0) { -// val inputStream = context.resources.openRawResource(resourceId) -// val bitmap = BitmapFactory.decodeStream(inputStream) -// inputStream.close() -// -// val resizedBitmap = resizeBitmap(bitmap, AVATAR_SIZE, AVATAR_SIZE) -// val circularBitmap = getCircularBitmap(resizedBitmap) -// bitmapToBase64(circularBitmap) -// } else { -// // Fallback to generated avatar -// generateDefaultAvatar(avatarId) -// } -// } catch (e: Exception) { -// generateDefaultAvatar(avatarId) -// } -// } -// -// fun uriToBase64(context: Context, uri: Uri): String? { -// return try { -// val inputStream: InputStream? = context.contentResolver.openInputStream(uri) -// inputStream?.use { stream -> -// val bitmap = BitmapFactory.decodeStream(stream) -// val resizedBitmap = resizeBitmap(bitmap, AVATAR_SIZE, AVATAR_SIZE) -// val circularBitmap = getCircularBitmap(resizedBitmap) -// bitmapToBase64(circularBitmap) -// } -// } catch (e: Exception) { -// null -// } -// } -// -// private fun resizeBitmap(bitmap: Bitmap, width: Int, height: Int): Bitmap { -// return Bitmap.createScaledBitmap(bitmap, width, height, true) -// } -// -// private fun getCircularBitmap(bitmap: Bitmap): Bitmap { -// val size = minOf(bitmap.width, bitmap.height) -// val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) -// -// val canvas = Canvas(output) -// val paint = Paint().apply { -// isAntiAlias = true -// } -// -// val rect = Rect(0, 0, size, size) -// val rectF = RectF(rect) -// -// canvas.drawOval(rectF, paint) -// -// paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) -// -// val sourceRect = Rect( -// (bitmap.width - size) / 2, -// (bitmap.height - size) / 2, -// (bitmap.width + size) / 2, -// (bitmap.height + size) / 2 -// ) -// -// canvas.drawBitmap(bitmap, sourceRect, rect, paint) -// -// return output -// } -// -// private fun bitmapToBase64(bitmap: Bitmap): String { -// val byteArrayOutputStream = ByteArrayOutputStream() -// bitmap.compress(Bitmap.CompressFormat.PNG, COMPRESSION_QUALITY, byteArrayOutputStream) -// val byteArray = byteArrayOutputStream.toByteArray() -// return Base64.encodeToString(byteArray, Base64.DEFAULT) -// } -// -// private fun base64ToBitmap(base64String: String): Bitmap? { -// return try { -// val decodedBytes = Base64.decode(base64String, Base64.DEFAULT) -// BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size) -// } catch (e: Exception) { -// null -// } -// } -// -// private fun generateDefaultAvatar(avatarId: String): String { -// // Generate a simple colored circle as fallback -// val bitmap = Bitmap.createBitmap(AVATAR_SIZE, AVATAR_SIZE, Bitmap.Config.ARGB_8888) -// val canvas = Canvas(bitmap) -// val paint = Paint().apply { -// isAntiAlias = true -// color = when (avatarId.hashCode() % 6) { -// 0 -> android.graphics.Color.parseColor("#FF6B6B") -// 1 -> android.graphics.Color.parseColor("#4ECDC4") -// 2 -> android.graphics.Color.parseColor("#45B7D1") -// 3 -> android.graphics.Color.parseColor("#96CEB4") -// 4 -> android.graphics.Color.parseColor("#FFEAA7") -// else -> android.graphics.Color.parseColor("#DDA0DD") -// } -// } -// -// canvas.drawCircle( -// AVATAR_SIZE / 2f, -// AVATAR_SIZE / 2f, -// AVATAR_SIZE / 2f, -// paint -// ) -// -// return bitmapToBase64(bitmap) -// } -// -// @Composable -// fun AvatarImage( -// base64String: String, -// modifier: Modifier = Modifier, -// size: Dp = 48.dp -// ) { -// val bitmap = base64ToBitmap(base64String) -// -// if (bitmap != null) { -// Image( -// bitmap = bitmap.asImageBitmap(), -// contentDescription = "Avatar", -// modifier = modifier -// .size(size) -// .clip(CircleShape), -// contentScale = ContentScale.Crop -// ) -// } else { -// // Fallback to colored circle -// Box( -// modifier = modifier -// .size(size) -// .clip(CircleShape) -// .background(MaterialTheme.colorScheme.primary), -// contentAlignment = Alignment.Center -// ) { -// // Empty fallback -// } -// } -// } -// -// @Composable -// fun AvatarImageWithFallback( -// base64String: String?, -// fallbackText: String = "?", -// modifier: Modifier = Modifier, -// size: Dp = 48.dp -// ) { -// if (!base64String.isNullOrEmpty()) { -// AvatarImage( -// base64String = base64String, -// modifier = modifier, -// size = size -// ) -// } else { -// // Text-based fallback -// Box( -// modifier = modifier -// .size(size) -// .clip(CircleShape) -// .background(MaterialTheme.colorScheme.primary), -// contentAlignment = Alignment.Center -// ) { -// androidx.compose.material3.Text( -// text = fallbackText.take(1).uppercase(), -// color = MaterialTheme.colorScheme.onPrimary, -// style = MaterialTheme.typography.titleMedium -// ) -// } -// } -// } -//} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/AvatarUtilsEnhanced.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/AvatarUtilsEnhanced.kt deleted file mode 100644 index 46dfdf9..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/AvatarUtilsEnhanced.kt +++ /dev/null @@ -1,252 +0,0 @@ -package dev.arkbuilders.drop.app.ui.profile - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.ImageDecoder -import android.net.Uri -import android.os.Build -import android.provider.MediaStore -import android.util.Base64 -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Person -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import java.io.ByteArrayOutputStream -import java.io.IOException -import androidx.core.graphics.scale - -object AvatarUtils { - - private const val MAX_IMAGE_SIZE = 512 // Maximum width/height in pixels - private const val JPEG_QUALITY = 85 // JPEG compression quality - private const val MAX_FILE_SIZE = 500 * 1024 // 500KB max file size - - /** - * Convert URI to Base64 with comprehensive error handling and optimization - */ - fun uriToBase64(context: Context, uri: Uri): String? { - return try { - val bitmap = loadBitmapFromUri(context, uri) ?: return null - val optimizedBitmap = optimizeBitmap(bitmap) - bitmapToBase64(optimizedBitmap) - } catch (e: Exception) { - null - } - } - - /** - * Load bitmap from URI with proper error handling - */ - private fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap? { - return try { - val source = ImageDecoder.createSource(context.contentResolver, uri) - ImageDecoder.decodeBitmap(source) - } catch (e: IOException) { - null - } catch (e: SecurityException) { - null - } - } - - /** - * Optimize bitmap for storage and performance - */ - private fun optimizeBitmap(bitmap: Bitmap): Bitmap { - val width = bitmap.width - val height = bitmap.height - - // Calculate scaling factor - val scaleFactor = if (width > height) { - MAX_IMAGE_SIZE.toFloat() / width - } else { - MAX_IMAGE_SIZE.toFloat() / height - } - - return if (scaleFactor < 1f) { - val newWidth = (width * scaleFactor).toInt() - val newHeight = (height * scaleFactor).toInt() - bitmap.scale(newWidth, newHeight) - } else { - bitmap - } - } - - /** - * Convert bitmap to Base64 with size validation - */ - private fun bitmapToBase64(bitmap: Bitmap): String? { - return try { - val outputStream = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, outputStream) - val byteArray = outputStream.toByteArray() - - // Check file size - if (byteArray.size > MAX_FILE_SIZE) { - return null - } - - Base64.encodeToString(byteArray, Base64.DEFAULT) - } catch (e: Exception) { - null - } - } - - /** - * Get default avatar Base64 string - */ - @SuppressLint("DiscouragedApi") - fun getDefaultAvatarBase64(context: Context, avatarId: String): String { - return try { - val resourceId = context.resources.getIdentifier( - avatarId, "drawable", context.packageName - ) - if (resourceId != 0) { - val bitmap = BitmapFactory.decodeResource(context.resources, resourceId) - bitmapToBase64(bitmap) ?: "" - } else { - "" - } - } catch (e: Exception) { - "" - } - } - - /** - * Enhanced avatar image composable with error handling - */ - @Composable - fun AvatarImage( - base64String: String, - modifier: Modifier = Modifier, - contentDescription: String? = null - ) { - if (base64String.isNotEmpty()) { - val imageBytes = Base64.decode(base64String, Base64.DEFAULT) - val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) - - if (bitmap != null) { - Image( - painter = BitmapPainter(bitmap.asImageBitmap()), - contentDescription = contentDescription, - modifier = modifier - .clip(CircleShape) - .semantics { - this.contentDescription = contentDescription ?: "Profile avatar" - }, - contentScale = ContentScale.Crop - ) - } else { - AvatarFallback(modifier = modifier, contentDescription = contentDescription) - } - } else { - AvatarFallback(modifier = modifier, contentDescription = contentDescription) - } - } - - /** - * Avatar with fallback for better UX - */ - @Composable - fun AvatarImageWithFallback( - base64String: String?, - fallbackText: String = "", - size: Dp = 48.dp, - contentDescription: String? = null - ) { - if (base64String != null && base64String.isNotEmpty()) { - AvatarImage( - base64String = base64String, - modifier = Modifier.size(size), - contentDescription = contentDescription - ) - } else { - AvatarFallback( - modifier = Modifier.size(size), - fallbackText = fallbackText, - contentDescription = contentDescription - ) - } - } - - /** - * Fallback avatar component - */ - @Composable - private fun AvatarFallback( - modifier: Modifier = Modifier, - fallbackText: String = "", - contentDescription: String? = null - ) { - Box( - modifier = modifier - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer) - .semantics { - this.contentDescription = contentDescription ?: "Default profile avatar" - }, - contentAlignment = Alignment.Center - ) { - if (fallbackText.isNotEmpty()) { - androidx.compose.material3.Text( - text = fallbackText.take(2).uppercase(), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } else { - Icon( - Icons.Default.Person, - contentDescription = null, - modifier = Modifier.fillMaxSize(0.6f), - tint = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } - } - - /** - * Validate image format and size - */ - fun validateImage(context: Context, uri: Uri): ValidationResult { - return try { - val bitmap = loadBitmapFromUri(context, uri) - when { - bitmap == null -> ValidationResult.InvalidFormat - bitmap.width < 64 || bitmap.height < 64 -> ValidationResult.TooSmall - bitmap.width > 2048 || bitmap.height > 2048 -> ValidationResult.TooLarge - else -> ValidationResult.Valid - } - } catch (e: Exception) { - ValidationResult.Error - } - } - - enum class ValidationResult { - Valid, - InvalidFormat, - TooSmall, - TooLarge, - Error - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfile.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfile.kt deleted file mode 100644 index 991b424..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfile.kt +++ /dev/null @@ -1,281 +0,0 @@ -package dev.arkbuilders.drop.app.ui.profile - -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import compose.icons.TablerIcons -import compose.icons.tablericons.Camera -import dev.arkbuilders.drop.app.domain.repository.ProfileRepo - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun EditProfile( - navController: NavController, - profileRepo: ProfileRepo -) { - val context = LocalContext.current - val profile by profileRepo.profile.collectAsState() - var name by remember { mutableStateOf(profile.name) } - var selectedAvatarId by remember { mutableStateOf(profile.avatarId) } - var customAvatarBase64 by remember { mutableStateOf(null) } - - val availableAvatars = listOf( - "avatar_00", "avatar_01", "avatar_02", "avatar_03", - "avatar_04", "avatar_05", "avatar_06", "avatar_07", "avatar_08" - ) - - // Image picker launcher - val imagePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() - ) { uri: Uri? -> - uri?.let { - val base64 = AvatarUtils.uriToBase64(context, it) - if (base64 != null) { - customAvatarBase64 = base64 - selectedAvatarId = "custom" - } - } - } - - Column ( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - ) { - // Top bar - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = { navController.navigateUp() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - Text( - text = "Edit Profile", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.weight(1f) - ) - - // Save button - FilledTonalButton( - onClick = { - profileRepo.updateName(name) - if (selectedAvatarId == "custom" && customAvatarBase64 != null) { - profileRepo.updateCustomAvatar(customAvatarBase64!!) - } else { - profileRepo.updateAvatar(selectedAvatarId) - } - navController.navigateUp() - } - ) { - Icon(Icons.Default.Check, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) - Text("Save") - } - } - - Spacer(modifier = Modifier.height(24.dp)) - - // Current avatar preview - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp) - ) { - Column( - modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val displayAvatarBase64 = when { - selectedAvatarId == "custom" && customAvatarBase64 != null -> customAvatarBase64!! - selectedAvatarId == "custom" && profile.avatarId == "custom" -> profile.avatarB64 - else -> AvatarUtils.getDefaultAvatarBase64(context, selectedAvatarId) - } - - AvatarUtils.AvatarImage( - base64String = displayAvatarBase64, - modifier = Modifier.size(80.dp) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - OutlinedTextField( - value = name, - onValueChange = { name = it }, - label = { Text("Display Name") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) - } - } - - Spacer(modifier = Modifier.height(24.dp)) - - // Upload custom avatar button - OutlinedButton( - onClick = { imagePickerLauncher.launch("image/*") }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp) - ) { - Icon( - TablerIcons.Camera, - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Upload Custom Avatar", fontWeight = FontWeight.Medium) - } - - Spacer(modifier = Modifier.height(24.dp)) - - // Avatar selection - Text( - text = "Choose Default Avatar", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(16.dp)) - - LazyVerticalGrid( - columns = GridCells.Fixed(3), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(availableAvatars) { avatarId -> - AvatarOption( - avatarId = avatarId, - isSelected = selectedAvatarId == avatarId, - onClick = { - selectedAvatarId = avatarId - customAvatarBase64 = null - } - ) - } - } - - Spacer(modifier = Modifier.weight(1f)) - - // Instructions - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Text( - text = "Your profile information is only shared during file transfers and is not stored on any server. Custom avatars are stored locally on your device.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(16.dp) - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun AvatarOption( - avatarId: String, - isSelected: Boolean, - onClick: () -> Unit -) { - val context = LocalContext.current - - Card( - modifier = Modifier - .aspectRatio(1f) - .clip(CircleShape), - onClick = onClick, - colors = CardDefaults.cardColors( - containerColor = if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surface - } - ), - border = if (isSelected) { - CardDefaults.outlinedCardBorder().copy( - width = 2.dp, - brush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary) - ) - } else null - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - AvatarUtils.AvatarImage( - base64String = AvatarUtils.getDefaultAvatarBase64(context, avatarId), - modifier = Modifier.size(48.dp) - ) - - if (isSelected) { - Box( - modifier = Modifier - .fillMaxSize() - .clip(CircleShape), - contentAlignment = Alignment.BottomEnd - ) { - Surface( - modifier = Modifier.size(20.dp), - shape = CircleShape, - color = MaterialTheme.colorScheme.primary - ) { - Icon( - Icons.Default.Check, - contentDescription = "Selected", - modifier = Modifier - .fillMaxSize() - .padding(2.dp), - tint = MaterialTheme.colorScheme.onPrimary - ) - } - } - } - } - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileEnhanced.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileScreen.kt similarity index 64% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileEnhanced.kt rename to app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileScreen.kt index 1d5f9d9..8ba8b20 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileEnhanced.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileScreen.kt @@ -33,10 +33,11 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check @@ -58,12 +59,9 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -81,12 +79,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.Camera -import compose.icons.tablericons.Check -import dev.arkbuilders.drop.app.domain.model.UserProfile -import dev.arkbuilders.drop.app.domain.repository.ProfileRepo +import dagger.hilt.android.EntryPointAccessors +import dev.arkbuilders.drop.app.di.TmpEntryPoint +import dev.arkbuilders.drop.app.domain.model.UserAvatar +import dev.arkbuilders.drop.app.ui.components.AvatarImage import dev.arkbuilders.drop.app.ui.components.DropButton import dev.arkbuilders.drop.app.ui.components.DropButtonSize import dev.arkbuilders.drop.app.ui.components.DropButtonVariant @@ -96,139 +96,41 @@ import dev.arkbuilders.drop.app.ui.components.DropCardSize import dev.arkbuilders.drop.app.ui.components.DropCardVariant import dev.arkbuilders.drop.app.ui.components.ErrorStateDisplay import dev.arkbuilders.drop.app.ui.components.ErrorType -import dev.arkbuilders.drop.app.ui.components.LoadingIndicator import dev.arkbuilders.drop.app.ui.theme.DesignTokens import kotlinx.coroutines.delay - -// UI State Management -data class EditProfileUiState( - val isLoading: Boolean = false, - val isSaving: Boolean = false, - val error: String? = null, - val showSuccess: Boolean = false, - val nameError: String? = null, - val avatarError: String? = null -) +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @OptIn(ExperimentalMaterial3Api::class) @Composable fun EditProfileEnhanced( navController: NavController, - profileRepo: ProfileRepo ) { - val context = LocalContext.current - val haptic = LocalHapticFeedback.current + val viewModel: EditProfileViewModel = hiltViewModel() val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current - - // State management - val profile by profileRepo.profile.collectAsState() - var uiState by remember { mutableStateOf(EditProfileUiState()) } - var name by rememberSaveable { mutableStateOf(profile.name) } - var selectedAvatarId by rememberSaveable { mutableStateOf(profile.avatarId) } - var customAvatarBase64 by remember { mutableStateOf(null) } - - // Focus management val nameFocusRequester = remember { FocusRequester() } - // Validation - val isNameValid by remember { - derivedStateOf { - name.isNotBlank() && name.length <= 50 && name.trim().length >= 2 - } - } - - val hasChanges by remember { - derivedStateOf { - name != profile.name || - selectedAvatarId != profile.avatarId || - customAvatarBase64 != null - } - } + val state by viewModel.collectAsState() - val canSave by remember { - derivedStateOf { - isNameValid && hasChanges && !uiState.isSaving - } - } - // Available avatars - val availableAvatars = remember { - listOf( - "avatar_00", "avatar_01", "avatar_02", "avatar_03", - "avatar_04", "avatar_05", "avatar_06", "avatar_07", "avatar_08" - ) - } - - // Image picker launcher with error handling val imagePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri: Uri? -> if (uri != null) { - try { - uiState = uiState.copy(isLoading = true, error = null) - val base64 = AvatarUtils.uriToBase64(context, uri) - if (base64 != null) { - customAvatarBase64 = base64 - selectedAvatarId = "custom" - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - } else { - uiState = uiState.copy( - error = "Unable to process the selected image. Please try a different image.", - avatarError = "Invalid image format" - ) - } - } catch (e: Exception) { - uiState = uiState.copy( - error = "Failed to load image. Please check your storage permissions and try again.", - avatarError = "Image loading failed" - ) - } finally { - uiState = uiState.copy(isLoading = false) - } + viewModel.onImagePicked(uri.toString()) } } - // Save profile function - val saveProfile = { - if (canSave) { - uiState = uiState.copy(isSaving = true, error = null) - profileRepo.updateName(name.trim()) - if (selectedAvatarId == "custom" && customAvatarBase64 != null) { - profileRepo.updateCustomAvatar(customAvatarBase64!!) - } else { - profileRepo.updateAvatar(selectedAvatarId) + viewModel.collectSideEffect { effect -> + when (effect) { + EditProfileScreenEffect.LaunchImagePicker -> { + imagePickerLauncher.launch("image/*") } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - uiState = uiState.copy(isSaving = false, showSuccess = true) - } - } - - LaunchedEffect(uiState.showSuccess) { - if (uiState.showSuccess) { - delay(1500) - navController.navigateUp() - } - } - - - // Real-time name validation - LaunchedEffect(name) { - uiState = uiState.copy( - nameError = when { - name.isBlank() -> "Name cannot be empty" - name.trim().length < 2 -> "Name must be at least 2 characters" - name.length > 50 -> "Name cannot exceed 50 characters" - else -> null + EditProfileScreenEffect.NavigateBack -> { + navController.popBackStack() } - ) - } - - // Clear success state when user makes changes - LaunchedEffect(name, selectedAvatarId, customAvatarBase64) { - if (uiState.showSuccess) { - uiState = uiState.copy(showSuccess = false) } } @@ -236,6 +138,7 @@ fun EditProfileEnhanced( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.ime) + .verticalScroll(rememberScrollState()) ) { // Enhanced Top App Bar TopAppBar( @@ -249,7 +152,6 @@ fun EditProfileEnhanced( navigationIcon = { IconButton( onClick = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) navController.navigateUp() }, modifier = Modifier.semantics { @@ -265,31 +167,18 @@ fun EditProfileEnhanced( }, actions = { AnimatedVisibility( - visible = canSave, + visible = state.hasChanges, enter = scaleIn(spring(stiffness = Spring.StiffnessHigh)) + fadeIn(), exit = scaleOut(spring(stiffness = Spring.StiffnessHigh)) + fadeOut() ) { DropButton( - onClick = saveProfile, + onClick = { viewModel.onSave() }, variant = DropButtonVariant.Primary, size = DropButtonSize.Medium, - loading = uiState.isSaving, contentDescription = "Save profile changes" ) { - if (!uiState.isSaving) { - Icon( - if (uiState.showSuccess) TablerIcons.Check else Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.xs)) - } Text( - text = when { - uiState.showSuccess -> "Saved!" - uiState.isSaving -> "Saving..." - else -> "Save" - }, + text = "Save", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold ) @@ -302,9 +191,8 @@ fun EditProfileEnhanced( ) ) - // Error Display AnimatedVisibility( - visible = uiState.error != null, + visible = state.avatarImageLoadingFailed, enter = slideInVertically( initialOffsetY = { -it }, animationSpec = spring(stiffness = Spring.StiffnessMedium) @@ -314,91 +202,54 @@ fun EditProfileEnhanced( animationSpec = spring(stiffness = Spring.StiffnessMedium) ) + fadeOut() ) { - uiState.error?.let { error -> - ErrorStateDisplay( - errorState = dev.arkbuilders.drop.app.ui.components.ErrorState( - type = ErrorType.Generic, - title = "Profile Update Failed", - message = error, - actionLabel = "Dismiss", - onAction = { uiState = uiState.copy(error = null) } - ), - modifier = Modifier.padding(DesignTokens.Spacing.lg) - ) - } + ErrorStateDisplay( + errorState = dev.arkbuilders.drop.app.ui.components.ErrorState( + type = ErrorType.Generic, + title = "Profile Update Failed", + message = "Failed to load image. Please check your storage permissions and try again.", + actionLabel = "Dismiss", + onAction = { + viewModel.clearAvatarLoadingError() + } + ), + modifier = Modifier.padding(DesignTokens.Spacing.lg) + ) } - // Loading Overlay - if (uiState.isLoading) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(DesignTokens.Spacing.lg), - contentAlignment = Alignment.Center - ) { - LoadingIndicator(message = "Processing image...") - } - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(DesignTokens.Spacing.lg), - verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.xl) - ) { - // Profile Preview Section - item { - ProfilePreviewSection( - name = name, - selectedAvatarId = selectedAvatarId, - customAvatarBase64 = customAvatarBase64, - profile = profile, - onNameChange = { newName -> - name = newName - uiState = uiState.copy(error = null) - }, - nameError = uiState.nameError, - nameFocusRequester = nameFocusRequester, - onDone = { - keyboardController?.hide() - focusManager.clearFocus() - } - ) - } + Column( + modifier = Modifier + .fillMaxSize() + .padding(DesignTokens.Spacing.lg), + verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.xl) + ) { + ProfilePreviewSection( + name = state.name, + avatar = state.avatar, + onNameChange = { newName -> + viewModel.onNameChanged(newName) + }, + nameError = state.nameError, + nameFocusRequester = nameFocusRequester, + ) - // Custom Avatar Upload Section - item { - CustomAvatarSection( - onUploadClick = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - imagePickerLauncher.launch("image/*") - }, - hasError = uiState.avatarError != null - ) - } + CustomAvatarSection( + onUploadClick = { + viewModel.onPickImage() + }, + hasError = state.avatarImageLoadingFailed + ) - // Avatar Selection Section - item { - AvatarSelectionSection( - availableAvatars = availableAvatars, - selectedAvatarId = selectedAvatarId, - onAvatarSelected = { avatarId -> - selectedAvatarId = avatarId - customAvatarBase64 = null - uiState = uiState.copy(error = null, avatarError = null) - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - } - ) + AvatarSelectionSection( + availableAvatars = UserAvatar.predefinedIds, + avatar = state.avatar, + onAvatarSelected = { avatarId -> + viewModel.onAvatarSelected(avatarId) } + ) - // Privacy Notice Section - item { - PrivacyNoticeSection() - } + PrivacyNoticeSection() - // Bottom spacing for better UX - item { - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xxxl)) - } - } + Spacer(modifier = Modifier.height(DesignTokens.Spacing.xxxl)) } } } @@ -406,16 +257,11 @@ fun EditProfileEnhanced( @Composable private fun ProfilePreviewSection( name: String, - selectedAvatarId: String, - customAvatarBase64: String?, - profile: UserProfile, + avatar: UserAvatar, onNameChange: (String) -> Unit, - nameError: String?, + nameError: EditProfileNameError?, nameFocusRequester: FocusRequester, - onDone: () -> Unit ) { - val context = LocalContext.current - DropCard( variant = DropCardVariant.Elevated, size = DropCardSize.Large, @@ -425,13 +271,6 @@ private fun ProfilePreviewSection( Column( horizontalAlignment = Alignment.CenterHorizontally ) { - // Avatar Preview with Animation - val displayAvatarBase64 = when { - selectedAvatarId == "custom" && customAvatarBase64 != null -> customAvatarBase64!! - selectedAvatarId == "custom" && profile.avatarId == "custom" -> profile.avatarB64 - else -> AvatarUtils.getDefaultAvatarBase64(context, selectedAvatarId) - } - var avatarScale by remember { mutableStateOf(0.8f) } val animatedAvatarScale by animateFloatAsState( targetValue = avatarScale, @@ -442,7 +281,7 @@ private fun ProfilePreviewSection( label = "avatarScale" ) - LaunchedEffect(selectedAvatarId, customAvatarBase64) { + LaunchedEffect(avatar) { avatarScale = 0.8f delay(100) avatarScale = 1f @@ -454,16 +293,15 @@ private fun ProfilePreviewSection( .scale(animatedAvatarScale), contentAlignment = Alignment.Center ) { - AvatarUtils.AvatarImage( - base64String = displayAvatarBase64, + AvatarImage( modifier = Modifier .size(120.dp) .semantics { contentDescription = "Current profile avatar" - } + }, + avatarB64 = avatar.base64, ) - // Edit indicator Surface( modifier = Modifier .align(Alignment.BottomEnd) @@ -513,7 +351,7 @@ private fun ProfilePreviewSection( ) { nameError?.let { Text( - text = it, + text = it.toString(), color = colorScheme.error, style = MaterialTheme.typography.bodySmall ) @@ -541,9 +379,6 @@ private fun ProfilePreviewSection( capitalization = KeyboardCapitalization.Words, imeAction = ImeAction.Done ), - keyboardActions = KeyboardActions( - onDone = { onDone() } - ), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = colorScheme.primary, unfocusedBorderColor = colorScheme.outline, @@ -633,9 +468,11 @@ private fun CustomAvatarSection( @Composable private fun AvatarSelectionSection( availableAvatars: List, - selectedAvatarId: String, + avatar: UserAvatar, onAvatarSelected: (String) -> Unit ) { + val columns = 3 + Column { Text( text = "Choose Default Avatar", @@ -649,18 +486,35 @@ private fun AvatarSelectionSection( Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - LazyVerticalGrid( - columns = GridCells.Fixed(3), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.lg), - verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.lg), - modifier = Modifier.height(300.dp) // Fixed height to prevent layout issues + Column( + verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.lg) ) { - items(availableAvatars) { avatarId -> - EnhancedAvatarOption( - avatarId = avatarId, - isSelected = selectedAvatarId == avatarId, - onClick = { onAvatarSelected(avatarId) } - ) + availableAvatars.chunked(columns).forEach { rowAvatars -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.lg), + ) { + rowAvatars.forEach { avatarId -> + EnhancedAvatarOption( + modifier = Modifier + .aspectRatio(1f) + .weight(1f), + avatarId = avatarId, + isSelected = avatar.predefinedId == avatarId, + onClick = { onAvatarSelected(avatarId) } + ) + } + + if (rowAvatars.size < columns) { + repeat(columns - rowAvatars.size) { + Spacer( + modifier = Modifier + .weight(1f) + .aspectRatio(1f) + ) + } + } + } } } } @@ -668,12 +522,19 @@ private fun AvatarSelectionSection( @Composable private fun EnhancedAvatarOption( + modifier: Modifier, avatarId: String, isSelected: Boolean, onClick: () -> Unit ) { val context = LocalContext.current val haptic = LocalHapticFeedback.current + val avatarHelper = remember { + EntryPointAccessors.fromApplication( + context.applicationContext, + TmpEntryPoint::class.java + ).avatarHelper() + } var scale by remember { mutableStateOf(1f) } val animatedScale by animateFloatAsState( @@ -686,12 +547,8 @@ private fun EnhancedAvatarOption( ) Card( - modifier = Modifier - .aspectRatio(1f) - .scale(animatedScale) - .semantics { - contentDescription = "Avatar option ${avatarId.replace("avatar_", "")}" - }, + modifier = modifier + .scale(animatedScale), onClick = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) scale = 0.95f @@ -720,41 +577,41 @@ private fun EnhancedAvatarOption( ), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) ) { - // Selection indicator - AnimatedVisibility( - visible = isSelected, - enter = scaleIn(spring(stiffness = Spring.StiffnessHigh)) + fadeIn(), - exit = scaleOut(spring(stiffness = Spring.StiffnessHigh)) + fadeOut(), - modifier = Modifier.align(Alignment.End) - ) { - Surface( - modifier = Modifier - .padding(DesignTokens.Spacing.sm) - .size(24.dp), - shape = CircleShape, - color = colorScheme.primary, - shadowElevation = DesignTokens.Elevation.sm + + Box { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center ) { - Icon( - Icons.Default.Check, - contentDescription = "Selected", - modifier = Modifier - .fillMaxSize() - .padding(DesignTokens.Spacing.xs), - tint = colorScheme.onPrimary + AvatarImage( + avatarB64 = avatarHelper.getDefaultAvatarBase64(avatarId), + modifier = Modifier.size(56.dp), ) } - } - - - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - AvatarUtils.AvatarImage( - base64String = AvatarUtils.getDefaultAvatarBase64(context, avatarId), - modifier = Modifier.size(56.dp) - ) + this@Card.AnimatedVisibility( + visible = isSelected, + enter = scaleIn(spring(stiffness = Spring.StiffnessHigh)) + fadeIn(), + exit = scaleOut(spring(stiffness = Spring.StiffnessHigh)) + fadeOut(), + modifier = Modifier.align(Alignment.TopEnd) + ) { + Surface( + modifier = Modifier + .padding(DesignTokens.Spacing.sm) + .size(20.dp), + shape = CircleShape, + color = colorScheme.primary, + shadowElevation = DesignTokens.Elevation.sm + ) { + Icon( + Icons.Default.Check, + contentDescription = "Selected", + modifier = Modifier + .fillMaxSize() + .padding(DesignTokens.Spacing.xs), + tint = colorScheme.onPrimary + ) + } + } } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt new file mode 100644 index 0000000..4d64998 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt @@ -0,0 +1,113 @@ +package dev.arkbuilders.drop.app.ui.profile + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.arkbuilders.drop.app.domain.AvatarHelper +import dev.arkbuilders.drop.app.domain.model.UserAvatar +import dev.arkbuilders.drop.app.domain.model.UserProfile +import dev.arkbuilders.drop.app.domain.repository.ProfileRepo +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +data class EditProfileScreenState( + val currentProfile: UserProfile, + val name: String, + val nameError: EditProfileNameError?, + val avatar: UserAvatar, + val avatarImageLoadingFailed: Boolean = false, + val hasChanges: Boolean = false, +) + +enum class EditProfileNameError { + EMPTY, TOO_SHORT, TOO_LONG +} + +sealed class EditProfileScreenEffect { + data object LaunchImagePicker : EditProfileScreenEffect() + data object NavigateBack : EditProfileScreenEffect() +} + +@HiltViewModel +class EditProfileViewModel @Inject constructor( + private val profileRepo: ProfileRepo, + private val avatarHelper: AvatarHelper, +) : ViewModel(), ContainerHost { + + override val container: Container = + container( + EditProfileScreenState( + currentProfile = UserProfile.empty(), + name = "", + nameError = null, + avatar = UserAvatar("", null), + ) + ) + + init { + val profile = profileRepo.getCurrentProfile() + intent { + reduce { + state.copy(currentProfile = profile, name = profile.name, avatar = profile.avatar) + } + } + } + + fun onNameChanged(newName: String) = blockingIntent { + val nameError = when { + newName.isBlank() -> EditProfileNameError.EMPTY + newName.trim().length < 2 -> EditProfileNameError.TOO_SHORT + newName.length > 50 -> EditProfileNameError.TOO_LONG + else -> null + } + reduce { + state.copy( + name = newName, + nameError = nameError, + hasChanges = state.currentProfile.name != newName, + ) + } + } + + fun onPickImage() = intent { + postSideEffect(EditProfileScreenEffect.LaunchImagePicker) + } + + fun onImagePicked(uri: String) = intent { + val base64 = avatarHelper.uriToBase64(uri) + base64?.let { + reduce { + state.copy( + avatar = UserAvatar(base64, predefinedId = null), + hasChanges = state.avatar != state.currentProfile.avatar, + ) + } + } ?: let { + state.copy( + avatarImageLoadingFailed = true, + ) + } + } + + fun onAvatarSelected(id: String) = intent { + val base64 = avatarHelper.getDefaultAvatarBase64(id) + reduce { + state.copy( + avatar = UserAvatar(base64, predefinedId = id), + hasChanges = state.avatar != state.currentProfile.avatar, + ) + } + } + + fun onSave() = intent { + profileRepo.updateProfile(UserProfile(state.name, state.avatar)) + postSideEffect(EditProfileScreenEffect.NavigateBack) + } + + fun clearAvatarLoadingError()= intent { + reduce { + state.copy(avatarImageLoadingFailed = false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt index 00cff29..2231545 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import dev.arkbuilders.drop.app.data.ReceiveFileInfo import dev.arkbuilders.drop.app.data.ReceivingProgress -import dev.arkbuilders.drop.app.ui.profile.AvatarUtils +import dev.arkbuilders.drop.app.ui.components.AvatarImageWithFallback import dev.arkbuilders.drop.app.ui.theme.DesignTokens @Composable @@ -95,8 +95,8 @@ fun ReceiveProgressCard( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) ) { - AvatarUtils.AvatarImageWithFallback( - base64String = progress.senderAvatar, + AvatarImageWithFallback( + avatarB64 = progress.senderAvatar, fallbackText = progress.senderName, size = 36.dp ) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt index 6ea6ba9..07ca3ba 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import dev.arkbuilders.drop.app.ui.DisplayUtils.formatBytes import dev.arkbuilders.drop.app.ui.DisplayUtils.formatDuration -import dev.arkbuilders.drop.app.ui.profile.AvatarUtils +import dev.arkbuilders.drop.app.ui.components.AvatarImageWithFallback import dev.arkbuilders.drop.app.ui.send.SendScreenState import dev.arkbuilders.drop.app.ui.send.components.SendCard import dev.arkbuilders.drop.app.ui.send.components.SendProgressBar @@ -84,8 +84,8 @@ fun TransferringPhase( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - AvatarUtils.AvatarImageWithFallback( - base64String = p.receiverAvatar ?: "", + AvatarImageWithFallback( + avatarB64 = p.receiverAvatar, fallbackText = p.receiverName, size = 32.dp ) From 73d258c1a8dd2166538d04d904de226214b835b3 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Sat, 1 Nov 2025 07:13:56 +0600 Subject: [PATCH 08/33] Split ReceiveScreenState into separate subclasses --- .../dev/arkbuilders/drop/app/MainActivity.kt | 3 +- .../app/ui/profile/EditProfileViewModel.kt | 2 +- .../drop/app/ui/receive/Receive.kt | 82 ++---- .../drop/app/ui/receive/ReceiveViewModel.kt | 274 +++++++----------- 4 files changed, 139 insertions(+), 222 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt index 42106b5..65e11ad 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt @@ -95,8 +95,7 @@ fun DropNavigation( ) ) { Receive( - navController = navController, - transferManager = transferManager + navController = navController ) } composable(DropDestination.History.route) { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt index 4d64998..4c766a3 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt @@ -105,7 +105,7 @@ class EditProfileViewModel @Inject constructor( postSideEffect(EditProfileScreenEffect.NavigateBack) } - fun clearAvatarLoadingError()= intent { + fun clearAvatarLoadingError() = intent { reduce { state.copy(avatarImageLoadingFailed = false) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt index b4f234f..9287b6b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt @@ -45,7 +45,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -54,7 +53,6 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -63,10 +61,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState import dev.arkbuilders.drop.app.R -import dev.arkbuilders.drop.app.TransferManager import dev.arkbuilders.drop.app.ui.receive.components.ReceiveCompleteCard import dev.arkbuilders.drop.app.ui.receive.components.ReceiveErrorCard import dev.arkbuilders.drop.app.ui.receive.components.ReceiveLoadingCard @@ -110,30 +105,14 @@ sealed class ReceiveError(val message: String, val isRecoverable: Boolean = true object UnknownError : ReceiveError("An unexpected error occurred. Please try again.", true) } -sealed class ReceiveWorkflowState { - object Initial : ReceiveWorkflowState() - object RequestingPermission : ReceiveWorkflowState() - object Scanning : ReceiveWorkflowState() - object ManualInput : ReceiveWorkflowState() - object QRCodeScanned : ReceiveWorkflowState() - object Connecting : ReceiveWorkflowState() - object Receiving : ReceiveWorkflowState() - object Success : ReceiveWorkflowState() - data class Error(val error: ReceiveError) : ReceiveWorkflowState() -} - @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @Composable fun Receive( navController: NavController, - transferManager: TransferManager, ) { val viewModel: ReceiveViewModel = hiltViewModel() - val context = LocalContext.current - val scope = rememberCoroutineScope() val clipboardManager = LocalClipboardManager.current val keyboardController = LocalSoftwareKeyboardController.current - val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA) val requestPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() @@ -147,12 +126,15 @@ fun Receive( ReceiveScreenEffect.HideKeyboard -> { keyboardController?.hide() } + ReceiveScreenEffect.NavigateBack -> { navController.navigateUp() } + ReceiveScreenEffect.RequestCameraPermission -> { requestPermissionLauncher.launch(Manifest.permission.CAMERA) } + ReceiveScreenEffect.ShowSuccessAnimation -> { } @@ -305,7 +287,7 @@ fun Receive( } AnimatedContent( - targetState = uiState.workflowState, + targetState = uiState, transitionSpec = { slideInVertically( initialOffsetY = { it / 3 }, @@ -319,8 +301,13 @@ fun Receive( label = "workflowStateTransition" ) { state -> when (state) { - is ReceiveWorkflowState.Initial -> { - if (!cameraPermissionState.status.isGranted) { + is ReceiveScreenState.Initial -> { + if (state.cameraPermissionGranted) { + ReceiveReadyToScanCard( + onStartScanning = { viewModel.onStartScanning() }, + onEnterManually = { viewModel.onEnterManually() } + ) + } else { ReceivePermissionRequestCard( onRequestPermission = { viewModel.onRequestCameraPermission() @@ -329,19 +316,14 @@ fun Receive( viewModel.onEnterManually() } ) - } else { - ReceiveReadyToScanCard( - onStartScanning = { viewModel.onStartScanning() }, - onEnterManually = { viewModel.onEnterManually() } - ) } } - is ReceiveWorkflowState.RequestingPermission -> { + ReceiveScreenState.RequestingPermission -> { ReceiveLoadingCard(message = "Requesting camera permission...") } - is ReceiveWorkflowState.Scanning -> { + ReceiveScreenState.Scanning -> { ReceiveScanningCard( onQRCodeScanned = { ticket, confirmation -> viewModel.onQrCodeScanned(ticket, confirmation) @@ -354,13 +336,13 @@ fun Receive( ) } - is ReceiveWorkflowState.ManualInput -> { + is ReceiveScreenState.ManualInput -> { ReceiveManualInputCard( - inputText = uiState.manualInputText, + inputText = state.inputText, onInputChange = { viewModel.onManualInputChanged(it) }, - inputError = uiState.manualInputError, + inputError = state.inputError, onPasteFromClipboard = { viewModel.onPasteFromClipboard(clipboardManager.getText()?.text) }, onSubmit = { viewModel.handleManualInputSubmit() }, onCancel = { @@ -369,7 +351,7 @@ fun Receive( ) } - is ReceiveWorkflowState.QRCodeScanned -> { + is ReceiveScreenState.QRCodeScanned -> { ReceiveQRCodeScannedCard( onAccept = { viewModel.onAccept() @@ -380,25 +362,23 @@ fun Receive( ) } - is ReceiveWorkflowState.Connecting -> { + ReceiveScreenState.Connecting -> { ReceiveLoadingCard(message = "Connecting to sender...") } - is ReceiveWorkflowState.Receiving -> { - uiState.receiveProgress?.let { progress -> - ReceiveProgressCard( - progress = progress, - onCancel = { - viewModel.onCancelReceiving() - } - ) - } + is ReceiveScreenState.Receiving -> { + ReceiveProgressCard( + progress = state.progress, + onCancel = { + viewModel.onCancelReceiving() + } + ) } - is ReceiveWorkflowState.Success -> { + is ReceiveScreenState.Success -> { if (!showSuccessAnimation) { ReceiveCompleteCard( - receivedFiles = uiState.receivedFiles, + receivedFiles = state.receivedFiles, onReceiveMore = { viewModel.onReceiveMore() }, @@ -409,7 +389,7 @@ fun Receive( } } - is ReceiveWorkflowState.Error -> { + is ReceiveScreenState.Error -> { ReceiveErrorCard( error = state.error, onRetry = { @@ -423,8 +403,10 @@ fun Receive( } } - if (uiState.workflowState !is ReceiveWorkflowState.Success - && uiState.workflowState !is ReceiveWorkflowState.Error) { + if (uiState !is ReceiveScreenState.Success + && uiState !is ReceiveScreenState.Error + ) { + Spacer(modifier = Modifier.weight(1f)) Card( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt index 0ea9777..2ba62cb 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt @@ -1,14 +1,11 @@ package dev.arkbuilders.drop.app.ui.receive -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.accompanist.permissions.isGranted import dagger.hilt.android.lifecycle.HiltViewModel import dev.arkbuilders.drop.app.TransferManager import dev.arkbuilders.drop.app.data.ReceivingProgress import dev.arkbuilders.drop.app.domain.PermissionsHelper -import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -17,28 +14,16 @@ import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject -data class ReceiveScreenState( - val workflowState: ReceiveWorkflowState, - val scannedTicket: String?, - val scannedConfirmation: UByte?, - val manualInputText: String, - val manualInputError: String?, - val receivedFiles: List, - val isCameraPermissionGranted: Boolean, - val receiveProgress: ReceivingProgress?, -) { - companion object { - fun initial() = ReceiveScreenState( - workflowState = ReceiveWorkflowState.Initial, - scannedTicket = null, - scannedConfirmation = null, - manualInputText = "", - manualInputError = null, - receivedFiles = emptyList(), - isCameraPermissionGranted = false, - receiveProgress = null, - ) - } +sealed class ReceiveScreenState { + data class Initial(val cameraPermissionGranted: Boolean) : ReceiveScreenState() + data object RequestingPermission : ReceiveScreenState() + data object Scanning : ReceiveScreenState() + data class ManualInput(val inputText: String, val inputError: String?) : ReceiveScreenState() + data class QRCodeScanned(val ticket: String, val confirmation: UByte) : ReceiveScreenState() + data object Connecting : ReceiveScreenState() + data class Receiving(val progress: ReceivingProgress) : ReceiveScreenState() + data class Success(val receivedFiles: List) : ReceiveScreenState() + data class Error(val error: ReceiveError) : ReceiveScreenState() } sealed class ReceiveScreenEffect { @@ -54,154 +39,122 @@ class ReceiveViewModel @Inject constructor( private val permissionsHelper: PermissionsHelper, ) : ViewModel(), ContainerHost { override val container: Container = - container(ReceiveScreenState.initial()) + container(ReceiveScreenState.Initial(false)) init { intent { val receiveProgress = transferManager.receiveProgress?.value - val workflowState = if (receiveProgress != null && receiveProgress.isConnected) { + val state = if (receiveProgress != null && receiveProgress.isConnected) { listenToProgress() - ReceiveWorkflowState.Receiving + ReceiveScreenState.Receiving(receiveProgress) } else { - ReceiveWorkflowState.Initial + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } reduce { - state.copy( - workflowState = workflowState, - receiveProgress = receiveProgress, - ) + state } } } fun onRequestCameraPermission() = intent { reduce { - state.copy( - workflowState = ReceiveWorkflowState.RequestingPermission - ) + ReceiveScreenState.RequestingPermission } postSideEffect(ReceiveScreenEffect.RequestCameraPermission) } fun onEnterManually() = intent { reduce { - state.copy( - workflowState = ReceiveWorkflowState.ManualInput - ) + ReceiveScreenState.ManualInput(inputText = "", inputError = null) } } fun onStartScanning() = intent { reduce { - state.copy( - workflowState = ReceiveWorkflowState.Scanning - ) + ReceiveScreenState.Scanning } } fun onStopScanning() = intent { reduce { - state.copy( - workflowState = ReceiveWorkflowState.Initial - ) + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } } fun onError(error: ReceiveError) = intent { reduce { - state.copy( - workflowState = ReceiveWorkflowState.Error(error) - ) + ReceiveScreenState.Error(error) } } fun onAccept() = intent { try { + val s = state + if (s !is ReceiveScreenState.QRCodeScanned) { + return@intent + } + val ticket = s.ticket + val confirmation = s.confirmation + reduce { - state.copy( - workflowState = ReceiveWorkflowState.Connecting - ) + ReceiveScreenState.Connecting } val bubble = - transferManager.receiveFiles(state.scannedTicket!!, state.scannedConfirmation!!) + transferManager.receiveFiles(ticket, confirmation) if (bubble != null) { reduce { - state.copy( - receiveProgress = transferManager.receiveProgress!!.value, - workflowState = ReceiveWorkflowState.Receiving - ) + ReceiveScreenState.Receiving(transferManager.receiveProgress!!.value) } listenToProgress() } else { reduce { - state.copy( - workflowState = ReceiveWorkflowState.Error(ReceiveError.ConnectionFailed) - ) + ReceiveScreenState.Error(ReceiveError.ConnectionFailed) } } } catch (e: Exception) { - val workflowState = ReceiveWorkflowState.Error( - when { - e.message?.contains( - "network", - ignoreCase = true - ) == true -> ReceiveError.NetworkError - - else -> ReceiveError.ConnectionFailed - } - ) + val error = when { + e.message?.contains( + "network", + ignoreCase = true + ) == true -> ReceiveError.NetworkError + + else -> ReceiveError.ConnectionFailed + } + reduce { - state.copy( - workflowState = workflowState - ) + ReceiveScreenState.Error(error) } } } fun onCameraPermissionGranted(isGranted: Boolean) = intent { - val workflowState = if (isGranted) { - ReceiveWorkflowState.Scanning + val state = if (isGranted) { + ReceiveScreenState.Scanning } else { - ReceiveWorkflowState.Error(ReceiveError.CameraPermissionDenied) + ReceiveScreenState.Error(ReceiveError.CameraPermissionDenied) } reduce { - state.copy( - workflowState = workflowState, - isCameraPermissionGranted = isGranted, - ) + state } } fun onScanAgain() = intent { - val workflowState = if (permissionsHelper.isCameraGranted()) { - ReceiveWorkflowState.Scanning + val state = if (permissionsHelper.isCameraGranted()) { + ReceiveScreenState.Scanning } else { - ReceiveWorkflowState.ManualInput + ReceiveScreenState.ManualInput(inputText = "", inputError = null) } reduce { - state.copy( - workflowState = workflowState, - scannedTicket = null, - scannedConfirmation = null, - manualInputText = "", - manualInputError = null, - ) + state } } fun onReceiveMore() = intent { + transferManager.cancelReceive() reduce { - state.copy( - workflowState = ReceiveWorkflowState.Initial, - receivedFiles = emptyList(), - scannedTicket = null, - scannedConfirmation = null, - manualInputText = "", - manualInputError = null, - ) + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } - transferManager.cancelReceive() } fun onDone() = intent { @@ -210,11 +163,15 @@ class ReceiveViewModel @Inject constructor( } fun onPasteFromClipboard(clipText: String?) = intent { + val s = state + if (s !is ReceiveScreenState.ManualInput) + return@intent + if (!clipText.isNullOrEmpty()) { reduce { - state.copy( - manualInputText = clipText, - manualInputError = null, + s.copy( + inputText = clipText, + inputError = null, ) } } @@ -222,13 +179,7 @@ class ReceiveViewModel @Inject constructor( fun onErrorRetry() = intent { reduce { - state.copy( - workflowState = ReceiveWorkflowState.Initial, - scannedTicket = null, - scannedConfirmation = null, - manualInputText = "", - manualInputError = null, - ) + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } } @@ -239,19 +190,19 @@ class ReceiveViewModel @Inject constructor( fun onQrCodeScanned(ticket: String, confirmation: UByte) = intent { reduce { - state.copy( - scannedTicket = ticket, - scannedConfirmation = confirmation, - workflowState = ReceiveWorkflowState.QRCodeScanned, - ) + ReceiveScreenState.QRCodeScanned(ticket, confirmation) } } fun onManualInputChanged(input: String) = blockingIntent { + val s = state + if (s !is ReceiveScreenState.ManualInput) + return@blockingIntent + reduce { - state.copy( - manualInputText = input, - manualInputError = null, + s.copy( + inputText = input, + inputError = null, ) } } @@ -259,43 +210,35 @@ class ReceiveViewModel @Inject constructor( fun onCancelReceiving() = intent { transferManager.cancelReceive() reduce { - state.copy( - workflowState = ReceiveWorkflowState.Initial, - scannedTicket = null, - scannedConfirmation = null, - manualInputText = "", - manualInputError = null, - ) + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } } fun onCancelManualInput() = intent { reduce { - state.copy( - workflowState = ReceiveWorkflowState.Initial, - manualInputText = "", - manualInputError = null, - ) + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } postSideEffect(ReceiveScreenEffect.HideKeyboard) } fun handleManualInputSubmit() = intent { - val parsed = parseManualInput(state.manualInputText) + val s = state + if (s !is ReceiveScreenState.ManualInput) + return@intent + + val parsed = parseManualInput(s.inputText) if (parsed != null) { reduce { - state.copy( - scannedTicket = parsed.first, - scannedConfirmation = parsed.second, - workflowState = ReceiveWorkflowState.QRCodeScanned, - manualInputError = null, + ReceiveScreenState.QRCodeScanned( + ticket = parsed.first, + confirmation = parsed.second ) } postSideEffect(ReceiveScreenEffect.HideKeyboard) } else { reduce { - state.copy( - manualInputError = "Invalid format. Please enter: ticket confirmation" + s.copy( + inputError = "Invalid format. Please enter: ticket confirmation", ) } } @@ -304,47 +247,44 @@ class ReceiveViewModel @Inject constructor( private fun listenToProgress() { transferManager.receiveProgress!!.onEach { progress -> intent { + val s = state + if (s !is ReceiveScreenState.Receiving) + return@intent + reduce { - state.copy( - receiveProgress = progress + s.copy( + progress = progress ) } - } - if (progress.isConnected && progress.files.isNotEmpty()) { - // Check if all files are complete - val allFilesComplete = progress.files.all { file -> - val fileProgress = progress.fileProgress[file.id] - fileProgress?.isComplete == true - } + if (progress.isConnected && progress.files.isNotEmpty()) { + // Check if all files are complete + val allFilesComplete = progress.files.all { file -> + val fileProgress = progress.fileProgress[file.id] + fileProgress?.isComplete == true + } - if (allFilesComplete) { - // Small delay to ensure UI updates are visible - delay(1000) - try { - val savedFiles = transferManager.saveReceivedFiles() - if (savedFiles.isNotEmpty()) { - intent { + if (allFilesComplete) { + // Small delay to ensure UI updates are visible + delay(1000) + try { + val savedFiles = transferManager.saveReceivedFiles() + if (savedFiles.isNotEmpty()) { reduce { - state.copy( - receivedFiles = savedFiles.map { it.name }, - workflowState = ReceiveWorkflowState.Success, + ReceiveScreenState.Success( + receivedFiles = savedFiles.map { it.name } ) } postSideEffect(ReceiveScreenEffect.ShowSuccessAnimation) - } - } else { - intent { + } else { reduce { - state.copy( - workflowState = ReceiveWorkflowState.Error(ReceiveError.NoFilesReceived) + ReceiveScreenState.Error( + ReceiveError.NoFilesReceived ) } } - } - } catch (e: Exception) { - val workflowState = ReceiveWorkflowState.Error( - when { + } catch (e: Exception) { + val error = when { e.message?.contains("storage", ignoreCase = true) == true -> ReceiveError.StorageError @@ -353,12 +293,8 @@ class ReceiveViewModel @Inject constructor( else -> ReceiveError.UnknownError } - ) - intent { reduce { - state.copy( - workflowState = workflowState - ) + ReceiveScreenState.Error(error) } } } From 71f74036acd1e754abf151c0662ed316609d53cf Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Sun, 2 Nov 2025 00:43:20 +0600 Subject: [PATCH 09/33] Remove RandomNameGenerator --- .../drop/app/utils/RandomNameGenerator.kt | 181 ------------------ .../drop/app/utils/RandomNameGeneratorTest.kt | 129 ------------- 2 files changed, 310 deletions(-) delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/utils/RandomNameGenerator.kt delete mode 100644 app/src/test/java/dev/arkbuilders/drop/app/utils/RandomNameGeneratorTest.kt diff --git a/app/src/main/java/dev/arkbuilders/drop/app/utils/RandomNameGenerator.kt b/app/src/main/java/dev/arkbuilders/drop/app/utils/RandomNameGenerator.kt deleted file mode 100644 index 70d6b8b..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/utils/RandomNameGenerator.kt +++ /dev/null @@ -1,181 +0,0 @@ -package dev.arkbuilders.drop.app.utils - -import kotlin.random.Random - -object RandomNameGenerator { - - private val adjectives = listOf( - // Positive traits - "clever", "bright", "swift", "gentle", "brave", "wise", "kind", "bold", - "calm", "cool", "warm", "fierce", "quick", "smart", "sharp", "keen", - "pure", "free", "wild", "true", "fresh", "alive", "awake", "aware", - - // Nature-inspired - "misty", "sunny", "cloudy", "stormy", "windy", "frosty", "snowy", "rainy", - "golden", "silver", "crystal", "pearl", "amber", "jade", "ruby", "emerald", - - // Movement/Energy - "dancing", "flying", "soaring", "flowing", "gliding", "drifting", "rushing", - "floating", "spinning", "jumping", "leaping", "racing", "wandering", - - // Emotions/Feelings - "happy", "joyful", "cheerful", "peaceful", "serene", "content", "blissful", - "hopeful", "dreamy", "curious", "playful", "merry", "lively", "vibrant", - - // Colors - "crimson", "azure", "violet", "indigo", "scarlet", "turquoise", "lavender", - "magenta", "coral", "teal", "lime", "maroon", "navy", "olive", "pink", - - // Size/Intensity - "tiny", "small", "little", "mini", "micro", "giant", "huge", "mega", - "super", "ultra", "grand", "mighty", "vast", "epic", "colossal", - - // Technology/Modern - "digital", "cyber", "virtual", "quantum", "nano", "tech", "smart", "auto", - "electric", "magnetic", "sonic", "laser", "plasma", "neon", "pixel", - - // Mystical/Fantasy - "mystic", "magic", "cosmic", "stellar", "lunar", "solar", "astral", - "ethereal", "divine", "enchanted", "mysterious", "legendary", "mythical" - ) - - private val nouns = listOf( - // Animals - "fox", "wolf", "bear", "lion", "tiger", "eagle", "hawk", "owl", "raven", - "dove", "swan", "crane", "heron", "falcon", "shark", "whale", "dolphin", - "turtle", "rabbit", "deer", "horse", "unicorn", "dragon", "phoenix", - "butterfly", "bee", "ant", "spider", "cat", "dog", "mouse", "elephant", - - // Nature Elements - "river", "ocean", "mountain", "forest", "desert", "valley", "cliff", - "waterfall", "lake", "pond", "stream", "meadow", "grove", "hill", - "stone", "rock", "crystal", "gem", "pearl", "shell", "leaf", "flower", - "tree", "branch", "root", "seed", "flame", "spark", "ember", "ash", - - // Celestial Bodies - "star", "moon", "sun", "comet", "meteor", "galaxy", "nebula", "planet", - "cosmos", "orbit", "asteroid", "constellation", "aurora", "eclipse", - - // Weather/Elements - "cloud", "rain", "snow", "mist", "fog", "wind", "storm", "thunder", - "lightning", "rainbow", "frost", "ice", "fire", "earth", "water", "air", - - // Objects/Tools - "arrow", "blade", "shield", "hammer", "key", "lock", "bridge", "tower", - "castle", "gate", "door", "window", "mirror", "lamp", "candle", "torch", - "compass", "map", "book", "scroll", "pen", "brush", "canvas", "lens", - - // Abstract Concepts - "dream", "hope", "wish", "joy", "peace", "love", "trust", "faith", - "courage", "wisdom", "truth", "freedom", "unity", "harmony", "balance", - "energy", "power", "force", "spirit", "soul", "heart", "mind", "will", - - // Professions/Characters - "builder", "maker", "creator", "artist", "writer", "poet", "singer", - "dancer", "runner", "climber", "sailor", "pilot", "explorer", "seeker", - "hunter", "guardian", "keeper", "watcher", "guide", "teacher", "student", - - // Technology/Future - "robot", "droid", "cyber", "pixel", "byte", "code", "data", "signal", - "wave", "pulse", "beam", "ray", "core", "chip", "circuit", "matrix", - "nexus", "node", "hub", "link", "network", "system", "protocol", "cipher" - ) - - /** - * Generates a random name in the format "adjective_noun" - * Similar to Docker's naming convention - */ - fun generateName(): String { - val adjective = adjectives.random() - val noun = nouns.random() - return "${adjective}_${noun}" - } - - /** - * Generates a random name with a specific format - */ - fun generateName(separator: String = "_", capitalize: Boolean = false): String { - val adjective = adjectives.random() - val noun = nouns.random() - - return if (capitalize) { - "${adjective.replaceFirstChar { it.uppercaseChar() }}${separator}${noun.replaceFirstChar { it.uppercaseChar() }}" - } else { - "$adjective$separator$noun" - } - } - - /** - * Generates a random name with additional entropy (number suffix) - */ - fun generateNameWithNumber(maxNumber: Int = 999): String { - val baseName = generateName() - val number = Random.Default.nextInt(0, maxNumber + 1) - return "${baseName}_$number" - } - - /** - * Generates multiple unique names - */ - fun generateUniqueNames(count: Int): List { - val names = mutableSetOf() - var attempts = 0 - val maxAttempts = count * 10 // Avoid infinite loops - - while (names.size < count && attempts < maxAttempts) { - names.add(generateName()) - attempts++ - } - - // If we couldn't generate enough unique names, add numbers - while (names.size < count) { - names.add(generateNameWithNumber()) - } - - return names.toList() - } - - /** - * Get a random adjective - */ - fun getRandomAdjective(): String = adjectives.random() - - /** - * Get a random noun - */ - fun getRandomNoun(): String = nouns.random() - - /** - * Check if a name follows the expected format - */ - fun isValidGeneratedName(name: String): Boolean { - val parts = name.split("_") - if (parts.size < 2) return false - - val adjective = parts[0] - val noun = parts[1] - - return adjectives.contains(adjective) && nouns.contains(noun) - } - - /** - * Generate a name that's guaranteed to be different from the provided name - */ - fun generateDifferentName(excludeName: String): String { - var newName: String - var attempts = 0 - val maxAttempts = 50 - - do { - newName = generateName() - attempts++ - } while (newName == excludeName && attempts < maxAttempts) - - // If we still got the same name after many attempts, add a number - if (newName == excludeName) { - newName = generateNameWithNumber() - } - - return newName - } -} \ No newline at end of file diff --git a/app/src/test/java/dev/arkbuilders/drop/app/utils/RandomNameGeneratorTest.kt b/app/src/test/java/dev/arkbuilders/drop/app/utils/RandomNameGeneratorTest.kt deleted file mode 100644 index f80132a..0000000 --- a/app/src/test/java/dev/arkbuilders/drop/app/utils/RandomNameGeneratorTest.kt +++ /dev/null @@ -1,129 +0,0 @@ -package dev.arkbuilders.drop.app.utils - -import org.junit.Test -import org.junit.Assert.* - -class RandomNameGeneratorTest { - - @Test - fun testGenerateName() { - val name = RandomNameGenerator.generateName() - - // Check format (should contain underscore) - assertTrue("Name should contain underscore", name.contains("_")) - - // Check parts - val parts = name.split("_") - assertEquals("Name should have exactly 2 parts", 2, parts.size) - - // Check if it's a valid generated name - assertTrue("Should be a valid generated name", - RandomNameGenerator.isValidGeneratedName(name)) - - println("Generated name: $name") - } - - @Test - fun testGenerateNameWithCustomSeparator() { - val name = RandomNameGenerator.generateName("-", true) - - assertTrue("Name should contain hyphen", name.contains("-")) - - val parts = name.split("-") - assertEquals("Name should have exactly 2 parts", 2, parts.size) - - // Check capitalization - assertTrue("First part should be capitalized", - parts[0].first().isUpperCase()) - assertTrue("Second part should be capitalized", - parts[1].first().isUpperCase()) - - println("Generated name with hyphen and capitalization: $name") - } - - @Test - fun testGenerateNameWithNumber() { - val name = RandomNameGenerator.generateNameWithNumber(100) - - val parts = name.split("_") - assertEquals("Name should have exactly 3 parts", 3, parts.size) - - // Last part should be a number - val number = parts[2].toIntOrNull() - assertNotNull("Last part should be a number", number) - assertTrue("Number should be between 0 and 100", - number!! in 0..100) - - println("Generated name with number: $name") - } - - @Test - fun testGenerateUniqueNames() { - val names = RandomNameGenerator.generateUniqueNames(10) - - assertEquals("Should generate 10 names", 10, names.size) - - // Check uniqueness - val uniqueNames = names.toSet() - assertEquals("All names should be unique", names.size, uniqueNames.size) - - println("Generated unique names:") - names.forEach { println(" $it") } - } - - @Test - fun testGenerateDifferentName() { - val originalName = "clever_fox" - val newName = RandomNameGenerator.generateDifferentName(originalName) - - assertNotEquals("New name should be different from original", - originalName, newName) - - println("Original: $originalName") - println("New: $newName") - } - - @Test - fun testRandomComponents() { - val adjective = RandomNameGenerator.getRandomAdjective() - val noun = RandomNameGenerator.getRandomNoun() - - assertNotNull("Adjective should not be null", adjective) - assertNotNull("Noun should not be null", noun) - assertTrue("Adjective should not be empty", adjective.isNotEmpty()) - assertTrue("Noun should not be empty", noun.isNotEmpty()) - - println("Random adjective: $adjective") - println("Random noun: $noun") - } - - @Test - fun testMultipleGenerations() { - println("Sample generated names:") - repeat(20) { - val name = RandomNameGenerator.generateName() - println(" $name") - } - } - - @Test - fun testVariousFormats() { - println("Different formats:") - - // Standard format - println("Standard: ${RandomNameGenerator.generateName()}") - - // With hyphen - println("Hyphenated: ${RandomNameGenerator.generateName("-")}") - - // Capitalized with space - println("Capitalized: ${RandomNameGenerator.generateName(" ", true)}") - - // With number - println("With number: ${RandomNameGenerator.generateNameWithNumber()}") - - // CamelCase style - val camelCase = RandomNameGenerator.generateName("", true) - println("CamelCase: $camelCase") - } -} \ No newline at end of file From 1275819fa308905cdb59ede298e9eea7f9de10e5 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Sun, 2 Nov 2025 02:19:41 +0600 Subject: [PATCH 10/33] Fix packages, add UserAvatarDto --- app/src/main/AndroidManifest.xml | 4 +- .../data/datasource/ProfileLocalDataSource.kt | 15 ++++- .../drop/app/data/model/UserAvatarDto.kt | 9 +++ .../drop/app/data/model/UserProfileDto.kt | 3 +- .../{ => data/repository}/TransferManager.kt | 13 +++-- .../dev/arkbuilders/drop/app/di/AppModule.kt | 2 +- .../drop/app/domain/model/UserAvatar.kt | 3 - .../app/{ui => presentation}/DisplayUtils.kt | 2 +- .../app/{ => presentation}/DropApplication.kt | 6 +- .../app/{ui => presentation}/Home/Home.kt | 44 +++++++------- .../Home/HomeViewModel.kt | 2 +- .../app/{ => presentation}/MainActivity.kt | 17 +++--- .../components/AvatarImage.kt | 5 +- .../components/DropButton.kt | 6 +- .../components/DropCard.kt | 9 +-- .../components/ErrorStates.kt | 10 ++-- .../components/LoadingStates.kt | 5 +- .../{ui => presentation}/history/History.kt | 4 +- .../history/HistoryViewModel.kt | 2 +- .../navigation/DropDestination.kt | 2 +- .../profile/EditProfileScreen.kt | 57 +++++++++---------- .../profile/EditProfileViewModel.kt | 2 +- .../{ui => presentation}/receive/Receive.kt | 42 +++++++------- .../receive/ReceiveViewModel.kt | 4 +- .../receive/components/ReceiveCompleteCard.kt | 20 +++---- .../receive/components/ReceiveErrorCard.kt | 14 ++--- .../receive/components/ReceiveLoadingCard.kt | 6 +- .../components/ReceiveManualInputCard.kt | 14 ++--- .../ReceivePermissionRequestCard.kt | 18 +++--- .../receive/components/ReceiveProgressCard.kt | 20 +++---- .../components/ReceiveQRCodeScannedCard.kt | 12 ++-- .../components/ReceiveReadyToScanCard.kt | 20 +++---- .../receive/components/ReceiveScanningCard.kt | 14 ++--- .../app/{ui => presentation}/send/Send.kt | 22 +++---- .../send/SendViewModel.kt | 10 ++-- .../send/components/SendButton.kt | 2 +- .../send/components/SendCard.kt | 2 +- .../send/components/SendEmptyState.kt | 2 +- .../send/components/SendErrorOverlay.kt | 4 +- .../send/components/SendFileItem.kt | 5 +- .../send/components/SendInstructionsCard.kt | 2 +- .../send/components/SendLoadingIndicator.kt | 2 +- .../send/components/SendProgressBar.kt | 2 +- .../send/components/phase/ErrorPhase.kt | 12 ++-- .../components/phase/FileSelectionPhase.kt | 18 +++--- .../components/phase/GeneratingQRPhase.kt | 12 ++-- .../components/phase/TransferCompletePhase.kt | 10 ++-- .../components/phase/TransferringPhase.kt | 14 ++--- .../phase/WaitingForReceiverPhase.kt | 12 ++-- .../app/{ui => presentation}/theme/Color.kt | 2 +- .../theme/DesignTokens.kt | 2 +- .../app/{ui => presentation}/theme/Theme.kt | 2 +- .../app/{ui => presentation}/theme/TopBar.kt | 2 +- .../app/{ui => presentation}/theme/Type.kt | 2 +- 54 files changed, 280 insertions(+), 267 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt rename app/src/main/java/dev/arkbuilders/drop/app/{ => data/repository}/TransferManager.kt (97%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/DisplayUtils.kt (93%) rename app/src/main/java/dev/arkbuilders/drop/app/{ => presentation}/DropApplication.kt (69%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/Home/Home.kt (88%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/Home/HomeViewModel.kt (97%) rename app/src/main/java/dev/arkbuilders/drop/app/{ => presentation}/MainActivity.kt (86%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/components/AvatarImage.kt (96%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/components/DropButton.kt (97%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/components/DropCard.kt (95%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/components/ErrorStates.kt (94%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/components/LoadingStates.kt (97%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/history/History.kt (99%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/history/HistoryViewModel.kt (97%) rename app/src/main/java/dev/arkbuilders/drop/app/{ => presentation}/navigation/DropDestination.kt (91%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/profile/EditProfileScreen.kt (92%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/profile/EditProfileViewModel.kt (98%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/Receive.kt (90%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/ReceiveViewModel.kt (98%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/components/ReceiveCompleteCard.kt (88%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/components/ReceiveErrorCard.kt (89%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/components/ReceiveLoadingCard.kt (90%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/components/ReceiveManualInputCard.kt (91%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/components/ReceivePermissionRequestCard.kt (87%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/components/ReceiveProgressCard.kt (92%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/components/ReceiveQRCodeScannedCard.kt (90%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/components/ReceiveReadyToScanCard.kt (86%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/receive/components/ReceiveScanningCard.kt (94%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/Send.kt (95%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/SendViewModel.kt (97%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/SendButton.kt (98%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/SendCard.kt (93%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/SendEmptyState.kt (97%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/SendErrorOverlay.kt (96%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/SendFileItem.kt (96%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/SendInstructionsCard.kt (97%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/SendLoadingIndicator.kt (95%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/SendProgressBar.kt (92%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/phase/ErrorPhase.kt (89%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/phase/FileSelectionPhase.kt (89%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/phase/GeneratingQRPhase.kt (73%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/phase/TransferCompletePhase.kt (94%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/phase/TransferringPhase.kt (93%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/send/components/phase/WaitingForReceiverPhase.kt (88%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/theme/Color.kt (95%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/theme/DesignTokens.kt (97%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/theme/Theme.kt (98%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/theme/TopBar.kt (97%) rename app/src/main/java/dev/arkbuilders/drop/app/{ui => presentation}/theme/Type.kt (98%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 15f87b0..ebfa873 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt index 0d276fd..c33b2c4 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt @@ -7,6 +7,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import javax.inject.Inject import androidx.core.content.edit +import dev.arkbuilders.drop.app.data.model.UserAvatarDto import dev.arkbuilders.drop.app.data.model.UserProfileDto import dev.arkbuilders.drop.app.domain.AvatarHelper import dev.arkbuilders.drop.app.domain.model.UserAvatar @@ -64,10 +65,20 @@ class ProfileLocalDataSource @Inject constructor( private fun UserProfileDto.toDomain() = UserProfile( name = name, - avatar = avatar + avatar = avatar.toDomain(), ) private fun UserProfile.toDto() = UserProfileDto( name = name, - avatar = avatar + avatar = avatar.toDto(), +) + +private fun UserAvatar.toDto() = UserAvatarDto( + base64 = base64, + predefinedId = predefinedId, +) + +private fun UserAvatarDto.toDomain() = UserAvatar( + base64 = base64, + predefinedId = predefinedId, ) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt new file mode 100644 index 0000000..1a6fab2 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt @@ -0,0 +1,9 @@ +package dev.arkbuilders.drop.app.data.model + +import kotlinx.serialization.Serializable + +@Serializable +data class UserAvatarDto( + val base64: String, + val predefinedId: String? +) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt index c071148..567e2f3 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt @@ -1,10 +1,9 @@ package dev.arkbuilders.drop.app.data.model -import dev.arkbuilders.drop.app.domain.model.UserAvatar import kotlinx.serialization.Serializable @Serializable data class UserProfileDto( val name: String, - val avatar: UserAvatar, + val avatar: UserAvatarDto, ) \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt similarity index 97% rename from app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt rename to app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt index e35ad36..f7c4e3b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/TransferManager.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app +package dev.arkbuilders.drop.app.data.repository import android.content.ContentValues import android.content.Context @@ -9,9 +9,12 @@ import android.provider.OpenableColumns import android.util.Log import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.qualifiers.ApplicationContext -import dev.arkbuilders.drop.* +import dev.arkbuilders.drop.ReceiveFilesBubble +import dev.arkbuilders.drop.SendFilesBubble import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl +import dev.arkbuilders.drop.app.data.ReceivingProgress import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl +import dev.arkbuilders.drop.app.data.SendingProgress import dev.arkbuilders.drop.app.di.TmpEntryPoint import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.repository.ProfileRepo @@ -39,10 +42,10 @@ class TransferManager @Inject constructor( private var sendSubscriber: SendFilesSubscriberImpl? = null private var receiveSubscriber: ReceiveFilesSubscriberImpl? = null - val sendProgress: StateFlow? + val sendProgress: StateFlow? get() = sendSubscriber?.progress - val receiveProgress: StateFlow? + val receiveProgress: StateFlow? get() = receiveSubscriber?.progress suspend fun sendFiles(fileUris: List): SendFilesBubble? = withContext(Dispatchers.IO) { @@ -420,4 +423,4 @@ class TransferManager @Inject constructor( null } } -} +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index fd99fbc..67baada 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -6,7 +6,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import dev.arkbuilders.drop.app.TransferManager +import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.data.helper.ResourcesHelperImpl import dev.arkbuilders.drop.app.data.db.Database import dev.arkbuilders.drop.app.data.helper.AvatarHelperImpl diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt index cb558cd..a842449 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt @@ -1,8 +1,5 @@ package dev.arkbuilders.drop.app.domain.model -import kotlinx.serialization.Serializable - -@Serializable data class UserAvatar( val base64: String, val predefinedId: String? diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/DisplayUtils.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DisplayUtils.kt similarity index 93% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/DisplayUtils.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/DisplayUtils.kt index a42b9f9..7f29786 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/DisplayUtils.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DisplayUtils.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui +package dev.arkbuilders.drop.app.presentation object DisplayUtils { fun formatBytes(bytes: Long): String { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/DropApplication.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt similarity index 69% rename from app/src/main/java/dev/arkbuilders/drop/app/DropApplication.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt index 4344b72..f5eceb3 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/DropApplication.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app +package dev.arkbuilders.drop.app.presentation import android.app.Application import dagger.hilt.android.HiltAndroidApp @@ -8,6 +8,6 @@ import timber.log.Timber class DropApplication : Application() { override fun onCreate() { super.onCreate() - Timber.plant(Timber.DebugTree()) + Timber.Forest.plant(Timber.DebugTree()) } -} +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/Home.kt similarity index 88% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/Home.kt index a338755..664bae6 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/Home.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/Home.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.Home +package dev.arkbuilders.drop.app.presentation.Home import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState @@ -49,19 +49,19 @@ import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.model.UserProfile import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository -import dev.arkbuilders.drop.app.navigation.DropDestination -import dev.arkbuilders.drop.app.ui.components.AvatarImage -import dev.arkbuilders.drop.app.ui.components.AvatarImageWithFallback -import dev.arkbuilders.drop.app.ui.components.DropButton -import dev.arkbuilders.drop.app.ui.components.DropButtonSize -import dev.arkbuilders.drop.app.ui.components.DropButtonVariant -import dev.arkbuilders.drop.app.ui.components.DropCard -import dev.arkbuilders.drop.app.ui.components.DropCardContent -import dev.arkbuilders.drop.app.ui.components.DropCardSize -import dev.arkbuilders.drop.app.ui.components.DropCardVariant -import dev.arkbuilders.drop.app.ui.components.DropOutlinedButton -import dev.arkbuilders.drop.app.ui.components.EmptyState -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.navigation.DropDestination +import dev.arkbuilders.drop.app.presentation.components.AvatarImage +import dev.arkbuilders.drop.app.presentation.components.AvatarImageWithFallback +import dev.arkbuilders.drop.app.presentation.components.DropButton +import dev.arkbuilders.drop.app.presentation.components.DropButtonSize +import dev.arkbuilders.drop.app.presentation.components.DropButtonVariant +import dev.arkbuilders.drop.app.presentation.components.DropCard +import dev.arkbuilders.drop.app.presentation.components.DropCardContent +import dev.arkbuilders.drop.app.presentation.components.DropCardSize +import dev.arkbuilders.drop.app.presentation.components.DropCardVariant +import dev.arkbuilders.drop.app.presentation.components.DropOutlinedButton +import dev.arkbuilders.drop.app.presentation.components.EmptyState +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens import kotlinx.coroutines.delay import org.orbitmvi.orbit.compose.collectAsState import java.time.Duration @@ -208,7 +208,7 @@ private fun QuickActionsSection( modifier = Modifier.fillMaxWidth() ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) // Send button DropButton( @@ -223,7 +223,7 @@ private fun QuickActionsSection( contentDescription = null, modifier = Modifier.size(24.dp) ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) Text( "Send Files", style = MaterialTheme.typography.titleMedium, @@ -231,7 +231,7 @@ private fun QuickActionsSection( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) // Receive button DropOutlinedButton( @@ -245,7 +245,7 @@ private fun QuickActionsSection( contentDescription = null, modifier = Modifier.size(24.dp) ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) Text( "Receive Files", style = MaterialTheme.typography.titleMedium, @@ -286,13 +286,13 @@ private fun RecentTransfersSection( contentDescription = null, modifier = Modifier.size(16.dp) ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.xs)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.xs)) Text("View All") } } } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Column( verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) @@ -341,7 +341,7 @@ private fun EnhancedTransferHistoryCard(item: TransferHistoryItem) { MaterialTheme.colorScheme.secondary ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.lg)) Column(modifier = Modifier.weight(1f)) { Text( @@ -353,7 +353,7 @@ private fun EnhancedTransferHistoryCard(item: TransferHistoryItem) { fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xs)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xs)) Text( text = "${item.fileCount} file${if (item.fileCount != 1) "s" else ""} • ${formatTimestamp(item.timestamp)}", style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/HomeViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/HomeViewModel.kt similarity index 97% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/Home/HomeViewModel.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/HomeViewModel.kt index e98829a..0180b6a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/Home/HomeViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/HomeViewModel.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.Home +package dev.arkbuilders.drop.app.presentation.Home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt similarity index 86% rename from app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt index 65e11ad..99d47f5 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app +package dev.arkbuilders.drop.app.presentation import android.os.Bundle import androidx.activity.ComponentActivity @@ -15,15 +15,16 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink import dagger.hilt.android.AndroidEntryPoint +import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository -import dev.arkbuilders.drop.app.navigation.DropDestination -import dev.arkbuilders.drop.app.ui.history.History -import dev.arkbuilders.drop.app.ui.Home.Home -import dev.arkbuilders.drop.app.ui.profile.EditProfileEnhanced -import dev.arkbuilders.drop.app.ui.receive.Receive -import dev.arkbuilders.drop.app.ui.send.Send -import dev.arkbuilders.drop.app.ui.theme.DropTheme +import dev.arkbuilders.drop.app.presentation.navigation.DropDestination +import dev.arkbuilders.drop.app.presentation.history.History +import dev.arkbuilders.drop.app.presentation.Home.Home +import dev.arkbuilders.drop.app.presentation.profile.EditProfileEnhanced +import dev.arkbuilders.drop.app.presentation.receive.Receive +import dev.arkbuilders.drop.app.presentation.send.Send +import dev.arkbuilders.drop.app.presentation.theme.DropTheme import javax.inject.Inject @AndroidEntryPoint diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/AvatarImage.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/AvatarImage.kt similarity index 96% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/components/AvatarImage.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/components/AvatarImage.kt index 86f62b1..2f22f81 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/AvatarImage.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/AvatarImage.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.components +package dev.arkbuilders.drop.app.presentation.components import android.graphics.BitmapFactory import android.util.Base64 @@ -12,6 +12,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -88,7 +89,7 @@ private fun AvatarFallback( contentAlignment = Alignment.Center ) { if (fallbackText.isNotEmpty()) { - androidx.compose.material3.Text( + Text( text = fallbackText.take(2).uppercase(), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/DropButton.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropButton.kt similarity index 97% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/components/DropButton.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropButton.kt index 8854788..ec286dd 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/DropButton.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropButton.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.components +package dev.arkbuilders.drop.app.presentation.components import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.interaction.MutableInteractionSource @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button -import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme @@ -19,7 +18,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.semantics.Role @@ -27,7 +25,7 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens enum class DropButtonSize { Small, Medium, Large diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/DropCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropCard.kt similarity index 95% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/components/DropCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropCard.kt index a903d2b..44708a3 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/DropCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropCard.kt @@ -1,5 +1,6 @@ -package dev.arkbuilders.drop.app.ui.components +package dev.arkbuilders.drop.app.presentation.components +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -15,7 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens enum class DropCardVariant { Filled, Elevated, Outlined @@ -121,8 +122,8 @@ fun DropCardContent( DropCardSize.Medium -> DesignTokens.Spacing.lg DropCardSize.Large -> DesignTokens.Spacing.xl } - - androidx.compose.foundation.layout.Column( + + Column( modifier = modifier.padding(padding) ) { content() diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/ErrorStates.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/ErrorStates.kt similarity index 94% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/components/ErrorStates.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/components/ErrorStates.kt index 8287c39..a512de5 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/ErrorStates.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/ErrorStates.kt @@ -1,21 +1,19 @@ -package dev.arkbuilders.drop.app.ui.components +package dev.arkbuilders.drop.app.presentation.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -24,7 +22,7 @@ import compose.icons.tablericons.AlertCircle import compose.icons.tablericons.CloudOff import compose.icons.tablericons.FileX import compose.icons.tablericons.WifiOff -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens enum class ErrorType { Network, FileTransfer, Permission, Generic, Offline @@ -62,7 +60,7 @@ fun ErrorStateDisplay( modifier = modifier, variant = DropCardVariant.Outlined, size = DropCardSize.Large, - colors = androidx.compose.material3.CardDefaults.outlinedCardColors( + colors = CardDefaults.outlinedCardColors( containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.1f), contentColor = MaterialTheme.colorScheme.onErrorContainer ) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/LoadingStates.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/LoadingStates.kt similarity index 97% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/components/LoadingStates.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/components/LoadingStates.kt index 883bcbd..9c72743 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/components/LoadingStates.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/LoadingStates.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.components +package dev.arkbuilders.drop.app.presentation.components import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode @@ -30,11 +30,10 @@ import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun LoadingIndicator( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt similarity index 99% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt index ed1e3d1..6f578e5 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/History.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.history +package dev.arkbuilders.drop.app.presentation.history import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -47,7 +47,7 @@ import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository -import dev.arkbuilders.drop.app.ui.components.AvatarImageWithFallback +import dev.arkbuilders.drop.app.presentation.components.AvatarImageWithFallback import org.orbitmvi.orbit.compose.collectAsState import java.time.Duration import java.time.OffsetDateTime diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/HistoryViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt similarity index 97% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/history/HistoryViewModel.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt index 6f29d0b..31e4665 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/history/HistoryViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.history +package dev.arkbuilders.drop.app.presentation.history import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/dev/arkbuilders/drop/app/navigation/DropDestination.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt similarity index 91% rename from app/src/main/java/dev/arkbuilders/drop/app/navigation/DropDestination.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt index 86f6c82..a028bad 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/navigation/DropDestination.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.navigation +package dev.arkbuilders.drop.app.presentation.navigation sealed class DropDestination(val route: String) { object Home : DropDestination("home") diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileScreen.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt similarity index 92% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileScreen.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt index 8ba8b20..4d1a68c 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileScreen.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.profile +package dev.arkbuilders.drop.app.presentation.profile import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult @@ -16,7 +16,6 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -29,10 +28,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -68,6 +63,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -86,17 +82,18 @@ import compose.icons.tablericons.Camera import dagger.hilt.android.EntryPointAccessors import dev.arkbuilders.drop.app.di.TmpEntryPoint import dev.arkbuilders.drop.app.domain.model.UserAvatar -import dev.arkbuilders.drop.app.ui.components.AvatarImage -import dev.arkbuilders.drop.app.ui.components.DropButton -import dev.arkbuilders.drop.app.ui.components.DropButtonSize -import dev.arkbuilders.drop.app.ui.components.DropButtonVariant -import dev.arkbuilders.drop.app.ui.components.DropCard -import dev.arkbuilders.drop.app.ui.components.DropCardContent -import dev.arkbuilders.drop.app.ui.components.DropCardSize -import dev.arkbuilders.drop.app.ui.components.DropCardVariant -import dev.arkbuilders.drop.app.ui.components.ErrorStateDisplay -import dev.arkbuilders.drop.app.ui.components.ErrorType -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.components.AvatarImage +import dev.arkbuilders.drop.app.presentation.components.DropButton +import dev.arkbuilders.drop.app.presentation.components.DropButtonSize +import dev.arkbuilders.drop.app.presentation.components.DropButtonVariant +import dev.arkbuilders.drop.app.presentation.components.DropCard +import dev.arkbuilders.drop.app.presentation.components.DropCardContent +import dev.arkbuilders.drop.app.presentation.components.DropCardSize +import dev.arkbuilders.drop.app.presentation.components.DropCardVariant +import dev.arkbuilders.drop.app.presentation.components.ErrorState +import dev.arkbuilders.drop.app.presentation.components.ErrorStateDisplay +import dev.arkbuilders.drop.app.presentation.components.ErrorType +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens import kotlinx.coroutines.delay import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -203,7 +200,7 @@ fun EditProfileEnhanced( ) + fadeOut() ) { ErrorStateDisplay( - errorState = dev.arkbuilders.drop.app.ui.components.ErrorState( + errorState = ErrorState( type = ErrorType.Generic, title = "Profile Update Failed", message = "Failed to load image. Please check your storage permissions and try again.", @@ -212,7 +209,7 @@ fun EditProfileEnhanced( viewModel.clearAvatarLoadingError() } ), - modifier = Modifier.padding(DesignTokens.Spacing.lg) + modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg) ) } @@ -249,7 +246,7 @@ fun EditProfileEnhanced( PrivacyNoticeSection() - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xxxl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xxxl)) } } } @@ -323,7 +320,7 @@ private fun ProfilePreviewSection( } } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) // Enhanced Name Input OutlinedTextField( @@ -432,7 +429,7 @@ private fun CustomAvatarSection( fontWeight = FontWeight.SemiBold, color = colorScheme.onSurface ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xs)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xs)) Text( text = "Upload your own profile picture", style = MaterialTheme.typography.bodyMedium, @@ -440,7 +437,7 @@ private fun CustomAvatarSection( ) } - Spacer(modifier = Modifier.width(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.lg)) DropButton( onClick = onUploadClick, @@ -453,7 +450,7 @@ private fun CustomAvatarSection( contentDescription = null, modifier = Modifier.size(20.dp) ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Upload", style = MaterialTheme.typography.labelLarge, @@ -484,7 +481,7 @@ private fun AvatarSelectionSection( } ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Column( verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.lg) @@ -564,12 +561,12 @@ private fun EnhancedAvatarOption( border = if (isSelected) { CardDefaults.outlinedCardBorder().copy( width = 3.dp, - brush = androidx.compose.ui.graphics.SolidColor(colorScheme.primary) + brush = SolidColor(colorScheme.primary) ) } else { CardDefaults.outlinedCardBorder().copy( width = 1.dp, - brush = androidx.compose.ui.graphics.SolidColor(colorScheme.outline) + brush = SolidColor(colorScheme.outline) ) }, elevation = CardDefaults.cardElevation( @@ -595,7 +592,7 @@ private fun EnhancedAvatarOption( modifier = Modifier.align(Alignment.TopEnd) ) { Surface( - modifier = Modifier + modifier = Modifier.Companion .padding(DesignTokens.Spacing.sm) .size(20.dp), shape = CircleShape, @@ -648,7 +645,7 @@ private fun PrivacyNoticeSection() { tint = colorScheme.primary ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) Column { Text( @@ -658,7 +655,7 @@ private fun PrivacyNoticeSection() { color = colorScheme.onSurface ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xs)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xs)) Text( text = "Your profile information is only shared during file transfers and is stored locally on your device. Custom avatars are processed and stored securely without being uploaded to any server.", diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt similarity index 98% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt index 4c766a3..601f6af 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/profile/EditProfileViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.profile +package dev.arkbuilders.drop.app.presentation.profile import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt similarity index 90% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt index 9287b6b..a118cae 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.receive +package dev.arkbuilders.drop.app.presentation.receive import android.Manifest import androidx.activity.compose.rememberLauncherForActivityResult @@ -62,16 +62,16 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import dev.arkbuilders.drop.app.R -import dev.arkbuilders.drop.app.ui.receive.components.ReceiveCompleteCard -import dev.arkbuilders.drop.app.ui.receive.components.ReceiveErrorCard -import dev.arkbuilders.drop.app.ui.receive.components.ReceiveLoadingCard -import dev.arkbuilders.drop.app.ui.receive.components.ReceiveManualInputCard -import dev.arkbuilders.drop.app.ui.receive.components.ReceivePermissionRequestCard -import dev.arkbuilders.drop.app.ui.receive.components.ReceiveProgressCard -import dev.arkbuilders.drop.app.ui.receive.components.ReceiveQRCodeScannedCard -import dev.arkbuilders.drop.app.ui.receive.components.ReceiveReadyToScanCard -import dev.arkbuilders.drop.app.ui.receive.components.ReceiveScanningCard -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveCompleteCard +import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveErrorCard +import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveLoadingCard +import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveManualInputCard +import dev.arkbuilders.drop.app.presentation.receive.components.ReceivePermissionRequestCard +import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveProgressCard +import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveQRCodeScannedCard +import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveReadyToScanCard +import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveScanningCard +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens import kotlinx.coroutines.delay import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -172,7 +172,7 @@ fun Receive( onClick = { navController.navigateUp() }, shape = CircleShape, color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f), - modifier = Modifier.size(DesignTokens.TouchTarget.minimum) + modifier = Modifier.Companion.size(DesignTokens.TouchTarget.minimum) ) { Box( contentAlignment = Alignment.Center, @@ -187,7 +187,7 @@ fun Receive( } } - Spacer(modifier = Modifier.width(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) Icon( modifier = Modifier.size(32.dp), @@ -196,7 +196,7 @@ fun Receive( tint = Color.Unspecified, ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) Text( text = "Receive Files", @@ -207,7 +207,7 @@ fun Receive( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) AnimatedVisibility( visible = showSuccessAnimation, @@ -237,7 +237,7 @@ fun Receive( elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.xl) ) { Column( - modifier = Modifier.padding(DesignTokens.Spacing.xxl), + modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), horizontalAlignment = Alignment.CenterHorizontally ) { Box( @@ -263,7 +263,7 @@ fun Receive( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Text( text = "Files Received!", @@ -272,7 +272,7 @@ fun Receive( textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) Text( text = "All files have been successfully saved to your Downloads folder.", @@ -417,14 +417,14 @@ fun Receive( shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) ) { Column( - modifier = Modifier.padding(DesignTokens.Spacing.lg) + modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg) ) { Text( text = "How to receive files:", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) val steps = listOf( "Ask the sender to start a transfer", @@ -444,7 +444,7 @@ fun Receive( fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( text = step, style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt similarity index 98% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt index 2ba62cb..beea36e 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/ReceiveViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt @@ -1,9 +1,9 @@ -package dev.arkbuilders.drop.app.ui.receive +package dev.arkbuilders.drop.app.presentation.receive import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dev.arkbuilders.drop.app.TransferManager +import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.data.ReceivingProgress import dev.arkbuilders.drop.app.domain.PermissionsHelper import kotlinx.coroutines.delay diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveCompleteCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt similarity index 88% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveCompleteCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt index 85c98f0..1c70d24 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveCompleteCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.receive.components +package dev.arkbuilders.drop.app.presentation.receive.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -31,7 +31,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveCompleteCard( @@ -49,7 +49,7 @@ fun ReceiveCompleteCard( elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) ) { Column( - modifier = Modifier.padding(DesignTokens.Spacing.xl), + modifier = Modifier.Companion.padding(DesignTokens.Spacing.xl), horizontalAlignment = Alignment.CenterHorizontally ) { Box( @@ -69,7 +69,7 @@ fun ReceiveCompleteCard( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Text( text = "Files Received Successfully!", @@ -78,7 +78,7 @@ fun ReceiveCompleteCard( textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) Text( text = "${receivedFiles.size} file${if (receivedFiles.size != 1) "s" else ""} saved to Downloads", @@ -88,7 +88,7 @@ fun ReceiveCompleteCard( ) if (receivedFiles.isNotEmpty()) { - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Card( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface, @@ -97,7 +97,7 @@ fun ReceiveCompleteCard( shape = RoundedCornerShape(DesignTokens.CornerRadius.md) ) { Column( - modifier = Modifier.padding(DesignTokens.Spacing.lg) + modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg) ) { Text( text = "Received Files:", @@ -105,7 +105,7 @@ fun ReceiveCompleteCard( fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) // Show first 3 files, then "... and X more" if needed receivedFiles.take(3).forEach { fileName -> @@ -129,7 +129,7 @@ fun ReceiveCompleteCard( } } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Row( modifier = Modifier.fillMaxWidth(), @@ -147,7 +147,7 @@ fun ReceiveCompleteCard( contentDescription = null, modifier = Modifier.size(18.dp) ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text("Receive More", fontWeight = FontWeight.Medium) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveErrorCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt similarity index 89% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveErrorCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt index 8a025f2..bc56278 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveErrorCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.receive.components +package dev.arkbuilders.drop.app.presentation.receive.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -30,8 +30,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import compose.icons.TablerIcons import compose.icons.tablericons.AlertCircle -import dev.arkbuilders.drop.app.ui.receive.ReceiveError -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.receive.ReceiveError +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveErrorCard( @@ -49,7 +49,7 @@ fun ReceiveErrorCard( elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) ) { Column( - modifier = Modifier.padding(DesignTokens.Spacing.xl), + modifier = Modifier.Companion.padding(DesignTokens.Spacing.xl), horizontalAlignment = Alignment.CenterHorizontally ) { Box( @@ -69,7 +69,7 @@ fun ReceiveErrorCard( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Text( text = if (error.isRecoverable) "Something went wrong" else "Error occurred", @@ -78,7 +78,7 @@ fun ReceiveErrorCard( textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) Text( text = error.message, @@ -88,7 +88,7 @@ fun ReceiveErrorCard( lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2 ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Row( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveLoadingCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt similarity index 90% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveLoadingCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt index 1324b0f..d745a10 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveLoadingCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.receive.components +package dev.arkbuilders.drop.app.presentation.receive.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -18,7 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveLoadingCard( @@ -45,7 +45,7 @@ fun ReceiveLoadingCard( strokeWidth = 3.dp, color = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.lg)) Text( text = message, style = MaterialTheme.typography.bodyLarge, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveManualInputCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt similarity index 91% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveManualInputCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt index 9f597ae..bd1203b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveManualInputCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.receive.components +package dev.arkbuilders.drop.app.presentation.receive.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -33,7 +33,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import compose.icons.TablerIcons import compose.icons.tablericons.ArrowForward -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveManualInputCard( @@ -54,7 +54,7 @@ fun ReceiveManualInputCard( elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) ) { Column( - modifier = Modifier.padding(DesignTokens.Spacing.xxl), + modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), horizontalAlignment = Alignment.CenterHorizontally ) { Box( @@ -74,7 +74,7 @@ fun ReceiveManualInputCard( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Text( text = "Enter Transfer Code", @@ -83,7 +83,7 @@ fun ReceiveManualInputCard( textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) Text( text = "Paste or type the transfer code from the sender in the format: ticket confirmation", @@ -93,7 +93,7 @@ fun ReceiveManualInputCard( lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) OutlinedTextField( value = inputText, @@ -123,7 +123,7 @@ fun ReceiveManualInputCard( shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Row( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceivePermissionRequestCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt similarity index 87% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceivePermissionRequestCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt index 24a0e19..5aed5bb 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceivePermissionRequestCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.receive.components +package dev.arkbuilders.drop.app.presentation.receive.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -28,7 +28,7 @@ import androidx.compose.ui.unit.dp import compose.icons.TablerIcons import compose.icons.tablericons.ArrowForward import compose.icons.tablericons.Camera -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceivePermissionRequestCard( @@ -45,7 +45,7 @@ fun ReceivePermissionRequestCard( elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) ) { Column( - modifier = Modifier.padding(DesignTokens.Spacing.xxl), + modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), horizontalAlignment = Alignment.CenterHorizontally ) { Box( @@ -65,7 +65,7 @@ fun ReceivePermissionRequestCard( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Text( text = "Camera Permission Required", @@ -74,7 +74,7 @@ fun ReceivePermissionRequestCard( textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) Text( text = "We need camera access to scan QR codes for receiving files. Your privacy is protected - we only use the camera for QR code scanning.", @@ -84,7 +84,7 @@ fun ReceivePermissionRequestCard( lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Button( onClick = onRequestPermission, @@ -103,7 +103,7 @@ fun ReceivePermissionRequestCard( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) Text( text = "Or", @@ -112,7 +112,7 @@ fun ReceivePermissionRequestCard( textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) OutlinedButton( onClick = onEnterManually, @@ -126,7 +126,7 @@ fun ReceivePermissionRequestCard( contentDescription = null, modifier = Modifier.size(18.dp) ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Enter Code Manually", fontWeight = FontWeight.Medium diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt similarity index 92% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt index 2231545..1d7b657 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveProgressCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.receive.components +package dev.arkbuilders.drop.app.presentation.receive.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -37,8 +37,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import dev.arkbuilders.drop.app.data.ReceiveFileInfo import dev.arkbuilders.drop.app.data.ReceivingProgress -import dev.arkbuilders.drop.app.ui.components.AvatarImageWithFallback -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.components.AvatarImageWithFallback +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveProgressCard( @@ -55,7 +55,7 @@ fun ReceiveProgressCard( elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) ) { Column( - modifier = Modifier.padding(DesignTokens.Spacing.lg) + modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -89,7 +89,7 @@ fun ReceiveProgressCard( } if (progress.isConnected) { - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) Row( verticalAlignment = Alignment.CenterVertically, @@ -116,7 +116,7 @@ fun ReceiveProgressCard( } } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Text( text = "Files (${progress.files.size}):", @@ -124,7 +124,7 @@ fun ReceiveProgressCard( fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) LazyColumn( modifier = Modifier.heightIn(max = 280.dp), @@ -143,7 +143,7 @@ fun ReceiveProgressCard( } } } else { - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Box( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center @@ -192,7 +192,7 @@ private fun ReceivingFileItem( overflow = TextOverflow.Ellipsis ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) LinearProgressIndicator( progress = { progress }, @@ -214,7 +214,7 @@ private fun ReceivingFileItem( } if (isComplete) { - Spacer(modifier = Modifier.width(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) Box( modifier = Modifier .size(32.dp) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveQRCodeScannedCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt similarity index 90% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveQRCodeScannedCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt index 426e324..8667534 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveQRCodeScannedCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.receive.components +package dev.arkbuilders.drop.app.presentation.receive.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -28,7 +28,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveQRCodeScannedCard( @@ -45,7 +45,7 @@ fun ReceiveQRCodeScannedCard( elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) ) { Column( - modifier = Modifier.padding(DesignTokens.Spacing.xxl), + modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), horizontalAlignment = Alignment.CenterHorizontally ) { Box( @@ -65,7 +65,7 @@ fun ReceiveQRCodeScannedCard( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Text( text = "Code Received!", @@ -74,7 +74,7 @@ fun ReceiveQRCodeScannedCard( color = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) Text( text = "Ready to receive files from sender. Tap Accept to start the transfer.", @@ -84,7 +84,7 @@ fun ReceiveQRCodeScannedCard( lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2 ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Row( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveReadyToScanCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt similarity index 86% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveReadyToScanCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt index 68dd319..46b9c2d 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveReadyToScanCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.receive.components +package dev.arkbuilders.drop.app.presentation.receive.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -30,7 +30,7 @@ import compose.icons.TablerIcons import compose.icons.tablericons.ArrowForward import compose.icons.tablericons.Camera import compose.icons.tablericons.Qrcode -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveReadyToScanCard( @@ -47,7 +47,7 @@ fun ReceiveReadyToScanCard( elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) ) { Column( - modifier = Modifier.padding(DesignTokens.Spacing.xxl), + modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), horizontalAlignment = Alignment.CenterHorizontally ) { Box( @@ -72,7 +72,7 @@ fun ReceiveReadyToScanCard( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Text( text = "Ready to Receive", @@ -80,7 +80,7 @@ fun ReceiveReadyToScanCard( fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) Text( text = "Scan the QR code from the sender's device or enter the transfer code manually to start receiving files securely.", @@ -90,7 +90,7 @@ fun ReceiveReadyToScanCard( lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Button( onClick = onStartScanning, @@ -107,7 +107,7 @@ fun ReceiveReadyToScanCard( contentDescription = null, modifier = Modifier.size(20.dp) ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Start Scanning", style = MaterialTheme.typography.titleMedium, @@ -115,7 +115,7 @@ fun ReceiveReadyToScanCard( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) Text( text = "Or", @@ -124,7 +124,7 @@ fun ReceiveReadyToScanCard( textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.md)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) OutlinedButton( onClick = onEnterManually, @@ -138,7 +138,7 @@ fun ReceiveReadyToScanCard( contentDescription = null, modifier = Modifier.size(18.dp) ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Enter Code Manually", fontWeight = FontWeight.Medium diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveScanningCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt similarity index 94% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveScanningCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt index 54f44ba..9f4a25e 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/receive/components/ReceiveScanningCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.receive.components +package dev.arkbuilders.drop.app.presentation.receive.components import androidx.annotation.OptIn import androidx.camera.core.CameraSelector @@ -44,8 +44,8 @@ import com.google.mlkit.vision.common.InputImage import compose.icons.TablerIcons import compose.icons.tablericons.ArrowForward import compose.icons.tablericons.CameraOff -import dev.arkbuilders.drop.app.ui.receive.ReceiveError -import dev.arkbuilders.drop.app.ui.theme.DesignTokens +import dev.arkbuilders.drop.app.presentation.receive.ReceiveError +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import kotlin.text.toUByte @@ -71,7 +71,7 @@ fun ReceiveScanningCard( ) } - Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Text( text = "Point your camera at the QR code", @@ -82,7 +82,7 @@ fun ReceiveScanningCard( color = MaterialTheme.colorScheme.onSurface ) - Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Row( modifier = Modifier.fillMaxWidth(), @@ -100,7 +100,7 @@ fun ReceiveScanningCard( contentDescription = null, modifier = Modifier.size(18.dp) ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Stop Scanning", fontWeight = FontWeight.Medium @@ -119,7 +119,7 @@ fun ReceiveScanningCard( contentDescription = null, modifier = Modifier.size(18.dp) ) - Spacer(modifier = Modifier.width(DesignTokens.Spacing.sm)) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Enter Code", fontWeight = FontWeight.Medium diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt similarity index 95% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/Send.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt index b9be659..0100a9b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send +package dev.arkbuilders.drop.app.presentation.send import android.content.ClipData import android.content.ClipboardManager @@ -80,16 +80,16 @@ import compose.icons.tablericons.CloudUpload import compose.icons.tablericons.Copy import compose.icons.tablericons.Qrcode import dev.arkbuilders.drop.app.R -import dev.arkbuilders.drop.app.TransferManager -import dev.arkbuilders.drop.app.ui.send.components.ButtonSize -import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant -import dev.arkbuilders.drop.app.ui.send.components.phase.FileSelectionPhase -import dev.arkbuilders.drop.app.ui.send.components.phase.GeneratingQRPhase -import dev.arkbuilders.drop.app.ui.send.components.SendButton -import dev.arkbuilders.drop.app.ui.send.components.SendLoadingIndicator -import dev.arkbuilders.drop.app.ui.send.components.phase.TransferCompletePhase -import dev.arkbuilders.drop.app.ui.send.components.phase.TransferringPhase -import dev.arkbuilders.drop.app.ui.send.components.phase.WaitingForReceiverPhase +import dev.arkbuilders.drop.app.data.repository.TransferManager +import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize +import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant +import dev.arkbuilders.drop.app.presentation.send.components.SendButton +import dev.arkbuilders.drop.app.presentation.send.components.SendLoadingIndicator +import dev.arkbuilders.drop.app.presentation.send.components.phase.FileSelectionPhase +import dev.arkbuilders.drop.app.presentation.send.components.phase.GeneratingQRPhase +import dev.arkbuilders.drop.app.presentation.send.components.phase.TransferCompletePhase +import dev.arkbuilders.drop.app.presentation.send.components.phase.TransferringPhase +import dev.arkbuilders.drop.app.presentation.send.components.phase.WaitingForReceiverPhase import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.orbitmvi.orbit.compose.collectAsState diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/SendViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt similarity index 97% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/SendViewModel.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt index 780ebee..7780c87 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/SendViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt @@ -1,6 +1,7 @@ -package dev.arkbuilders.drop.app.ui.send +package dev.arkbuilders.drop.app.presentation.send import android.graphics.Bitmap +import android.graphics.Color import androidx.core.graphics.createBitmap import androidx.core.graphics.set import androidx.core.net.toUri @@ -11,7 +12,7 @@ import com.google.zxing.WriterException import com.google.zxing.common.BitMatrix import com.google.zxing.qrcode.QRCodeWriter import dagger.hilt.android.lifecycle.HiltViewModel -import dev.arkbuilders.drop.app.TransferManager +import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.domain.repository.NetworkStatus import kotlinx.coroutines.delay @@ -22,7 +23,6 @@ import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container -import timber.log.Timber import javax.inject.Inject sealed class SendScreenState { @@ -243,9 +243,9 @@ class SendViewModel @Inject constructor( for (x in 0 until width) { for (y in 0 until height) { bitmap[x, y] = if (bitMatrix[x, y]) { - android.graphics.Color.BLACK + Color.BLACK } else { - android.graphics.Color.WHITE + Color.WHITE } } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendButton.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendButton.kt similarity index 98% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendButton.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendButton.kt index 1ca727f..6a1be99 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendButton.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendButton.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components +package dev.arkbuilders.drop.app.presentation.send.components import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendCard.kt similarity index 93% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendCard.kt index d6a59e4..263ab09 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components +package dev.arkbuilders.drop.app.presentation.send.components import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendEmptyState.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendEmptyState.kt similarity index 97% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendEmptyState.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendEmptyState.kt index adec2e1..9033162 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendEmptyState.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendEmptyState.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components +package dev.arkbuilders.drop.app.presentation.send.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendErrorOverlay.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt similarity index 96% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendErrorOverlay.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt index cdb57a3..7f01d0f 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendErrorOverlay.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components +package dev.arkbuilders.drop.app.presentation.send.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -23,7 +23,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.send.SendException +import dev.arkbuilders.drop.app.presentation.send.SendException @Composable fun SendErrorOverlay( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendFileItem.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt similarity index 96% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendFileItem.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt index 6dc1779..1939f66 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendFileItem.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components +package dev.arkbuilders.drop.app.presentation.send.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -32,12 +32,11 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import compose.icons.TablerIcons import compose.icons.tablericons.FileText import dagger.hilt.android.EntryPointAccessors import dev.arkbuilders.drop.app.di.TmpEntryPoint -import dev.arkbuilders.drop.app.ui.DisplayUtils.formatBytes +import dev.arkbuilders.drop.app.presentation.DisplayUtils.formatBytes @Composable fun SendFileItem( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendInstructionsCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt similarity index 97% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendInstructionsCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt index 5b32241..280f6db 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendInstructionsCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components +package dev.arkbuilders.drop.app.presentation.send.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendLoadingIndicator.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendLoadingIndicator.kt similarity index 95% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendLoadingIndicator.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendLoadingIndicator.kt index 3cf4e18..2c62546 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendLoadingIndicator.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendLoadingIndicator.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components +package dev.arkbuilders.drop.app.presentation.send.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendProgressBar.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendProgressBar.kt similarity index 92% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendProgressBar.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendProgressBar.kt index 70d6017..f78995a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/SendProgressBar.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendProgressBar.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components +package dev.arkbuilders.drop.app.presentation.send.components import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/ErrorPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt similarity index 89% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/ErrorPhase.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt index 1582361..af2f056 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/ErrorPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components.phase +package dev.arkbuilders.drop.app.presentation.send.components.phase import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -18,11 +18,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.send.SendException -import dev.arkbuilders.drop.app.ui.send.components.ButtonSize -import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant -import dev.arkbuilders.drop.app.ui.send.components.SendButton -import dev.arkbuilders.drop.app.ui.send.components.SendCard +import dev.arkbuilders.drop.app.presentation.send.SendException +import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize +import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant +import dev.arkbuilders.drop.app.presentation.send.components.SendButton +import dev.arkbuilders.drop.app.presentation.send.components.SendCard @Composable private fun ErrorPhase( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/FileSelectionPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt similarity index 89% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/FileSelectionPhase.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt index 902d755..7a70505 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/FileSelectionPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components.phase +package dev.arkbuilders.drop.app.presentation.send.components.phase import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -26,14 +26,14 @@ import androidx.compose.ui.unit.dp import compose.icons.TablerIcons import compose.icons.tablericons.FileText import compose.icons.tablericons.Plus -import dev.arkbuilders.drop.app.ui.DisplayUtils.formatBytes -import dev.arkbuilders.drop.app.ui.send.components.ButtonSize -import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant -import dev.arkbuilders.drop.app.ui.send.components.SendButton -import dev.arkbuilders.drop.app.ui.send.components.SendCard -import dev.arkbuilders.drop.app.ui.send.components.SendEmptyState -import dev.arkbuilders.drop.app.ui.send.components.SendFileItem -import dev.arkbuilders.drop.app.ui.send.components.SendInstructionsCard +import dev.arkbuilders.drop.app.presentation.DisplayUtils.formatBytes +import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize +import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant +import dev.arkbuilders.drop.app.presentation.send.components.SendButton +import dev.arkbuilders.drop.app.presentation.send.components.SendCard +import dev.arkbuilders.drop.app.presentation.send.components.SendEmptyState +import dev.arkbuilders.drop.app.presentation.send.components.SendFileItem +import dev.arkbuilders.drop.app.presentation.send.components.SendInstructionsCard @Composable fun FileSelectionPhase( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/GeneratingQRPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/GeneratingQRPhase.kt similarity index 73% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/GeneratingQRPhase.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/GeneratingQRPhase.kt index ecb32ce..5abab93 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/GeneratingQRPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/GeneratingQRPhase.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components.phase +package dev.arkbuilders.drop.app.presentation.send.components.phase import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,11 +11,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.send.components.ButtonSize -import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant -import dev.arkbuilders.drop.app.ui.send.components.SendButton -import dev.arkbuilders.drop.app.ui.send.components.SendCard -import dev.arkbuilders.drop.app.ui.send.components.SendLoadingIndicator +import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize +import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant +import dev.arkbuilders.drop.app.presentation.send.components.SendButton +import dev.arkbuilders.drop.app.presentation.send.components.SendCard +import dev.arkbuilders.drop.app.presentation.send.components.SendLoadingIndicator @Composable fun GeneratingQRPhase( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferCompletePhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt similarity index 94% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferCompletePhase.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt index bcf55f8..352164b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferCompletePhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components.phase +package dev.arkbuilders.drop.app.presentation.send.components.phase import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -24,10 +24,10 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.send.components.ButtonSize -import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant -import dev.arkbuilders.drop.app.ui.send.components.SendButton -import dev.arkbuilders.drop.app.ui.send.components.SendCard +import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize +import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant +import dev.arkbuilders.drop.app.presentation.send.components.SendButton +import dev.arkbuilders.drop.app.presentation.send.components.SendCard @Composable fun TransferCompletePhase( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt similarity index 93% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt index 07ca3ba..3556811 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/TransferringPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components.phase +package dev.arkbuilders.drop.app.presentation.send.components.phase import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -25,12 +25,12 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.DisplayUtils.formatBytes -import dev.arkbuilders.drop.app.ui.DisplayUtils.formatDuration -import dev.arkbuilders.drop.app.ui.components.AvatarImageWithFallback -import dev.arkbuilders.drop.app.ui.send.SendScreenState -import dev.arkbuilders.drop.app.ui.send.components.SendCard -import dev.arkbuilders.drop.app.ui.send.components.SendProgressBar +import dev.arkbuilders.drop.app.presentation.DisplayUtils.formatBytes +import dev.arkbuilders.drop.app.presentation.DisplayUtils.formatDuration +import dev.arkbuilders.drop.app.presentation.components.AvatarImageWithFallback +import dev.arkbuilders.drop.app.presentation.send.SendScreenState +import dev.arkbuilders.drop.app.presentation.send.components.SendCard +import dev.arkbuilders.drop.app.presentation.send.components.SendProgressBar @Composable fun TransferringPhase( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/WaitingForReceiverPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt similarity index 88% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/WaitingForReceiverPhase.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt index 110db29..0e8f288 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/send/components/phase/WaitingForReceiverPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.send.components.phase +package dev.arkbuilders.drop.app.presentation.send.components.phase import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,11 +18,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.ui.send.components.ButtonSize -import dev.arkbuilders.drop.app.ui.send.components.ButtonVariant -import dev.arkbuilders.drop.app.ui.send.components.SendButton -import dev.arkbuilders.drop.app.ui.send.components.SendCard -import dev.arkbuilders.drop.app.ui.send.components.SendLoadingIndicator +import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize +import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant +import dev.arkbuilders.drop.app.presentation.send.components.SendButton +import dev.arkbuilders.drop.app.presentation.send.components.SendCard +import dev.arkbuilders.drop.app.presentation.send.components.SendLoadingIndicator @Composable fun WaitingForReceiverPhase( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/theme/Color.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Color.kt similarity index 95% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/theme/Color.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Color.kt index 09f14d8..c1c6bc5 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/theme/Color.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Color.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.theme +package dev.arkbuilders.drop.app.presentation.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/theme/DesignTokens.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/DesignTokens.kt similarity index 97% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/theme/DesignTokens.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/DesignTokens.kt index 0d48435..a0dbd4b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/theme/DesignTokens.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/DesignTokens.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.theme +package dev.arkbuilders.drop.app.presentation.theme import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/theme/Theme.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt similarity index 98% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/theme/Theme.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt index d66aa91..c9cecde 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/theme/Theme.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.theme +package dev.arkbuilders.drop.app.presentation.theme import android.app.Activity import android.os.Build diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/theme/TopBar.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/TopBar.kt similarity index 97% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/theme/TopBar.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/TopBar.kt index 0690a17..468f4d9 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/theme/TopBar.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/TopBar.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.theme +package dev.arkbuilders.drop.app.presentation.theme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/dev/arkbuilders/drop/app/ui/theme/Type.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Type.kt similarity index 98% rename from app/src/main/java/dev/arkbuilders/drop/app/ui/theme/Type.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Type.kt index 8256ae0..9789499 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/ui/theme/Type.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Type.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.ui.theme +package dev.arkbuilders.drop.app.presentation.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle From 7bf79ff29ba0aa9772eb355febba6a6e5da592cf Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Sun, 2 Nov 2025 02:34:28 +0600 Subject: [PATCH 11/33] Remove AnimatedContent in Send/Receive to avoid reanimation --- .../drop/app/presentation/receive/Receive.kt | 192 ++++++++---------- .../drop/app/presentation/send/Send.kt | 126 +++++------- 2 files changed, 142 insertions(+), 176 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt index a118cae..a009837 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt @@ -3,7 +3,6 @@ package dev.arkbuilders.drop.app.presentation.receive import android.Manifest import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState @@ -13,9 +12,6 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -120,7 +116,7 @@ fun Receive( viewModel.onCameraPermissionGranted(isGranted) } - val uiState by viewModel.collectAsState() + val state by viewModel.collectAsState() viewModel.collectSideEffect { effect -> when (effect) { ReceiveScreenEffect.HideKeyboard -> { @@ -286,125 +282,111 @@ fun Receive( } } - AnimatedContent( - targetState = uiState, - transitionSpec = { - slideInVertically( - initialOffsetY = { it / 3 }, - animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) - ) + fadeIn() togetherWith - slideOutVertically( - targetOffsetY = { -it / 3 }, - animationSpec = tween(DesignTokens.Animation.fast) - ) + fadeOut() - }, - label = "workflowStateTransition" - ) { state -> - when (state) { - is ReceiveScreenState.Initial -> { - if (state.cameraPermissionGranted) { - ReceiveReadyToScanCard( - onStartScanning = { viewModel.onStartScanning() }, - onEnterManually = { viewModel.onEnterManually() } - ) - } else { - ReceivePermissionRequestCard( - onRequestPermission = { - viewModel.onRequestCameraPermission() - }, - onEnterManually = { - viewModel.onEnterManually() - } - ) - } - } - - ReceiveScreenState.RequestingPermission -> { - ReceiveLoadingCard(message = "Requesting camera permission...") - } - - ReceiveScreenState.Scanning -> { - ReceiveScanningCard( - onQRCodeScanned = { ticket, confirmation -> - viewModel.onQrCodeScanned(ticket, confirmation) - }, - onError = { error -> - viewModel.onError(error) - }, - onStopScanning = { viewModel.onStopScanning() }, + val _state = state + when (_state) { + is ReceiveScreenState.Initial -> { + if (_state.cameraPermissionGranted) { + ReceiveReadyToScanCard( + onStartScanning = { viewModel.onStartScanning() }, onEnterManually = { viewModel.onEnterManually() } ) - } - - is ReceiveScreenState.ManualInput -> { - ReceiveManualInputCard( - inputText = state.inputText, - onInputChange = { - viewModel.onManualInputChanged(it) + } else { + ReceivePermissionRequestCard( + onRequestPermission = { + viewModel.onRequestCameraPermission() }, - inputError = state.inputError, - onPasteFromClipboard = { viewModel.onPasteFromClipboard(clipboardManager.getText()?.text) }, - onSubmit = { viewModel.handleManualInputSubmit() }, - onCancel = { - viewModel.onCancelManualInput() + onEnterManually = { + viewModel.onEnterManually() } ) } + } - is ReceiveScreenState.QRCodeScanned -> { - ReceiveQRCodeScannedCard( - onAccept = { - viewModel.onAccept() - }, - onScanAgain = { - viewModel.onScanAgain() - } - ) - } + ReceiveScreenState.RequestingPermission -> { + ReceiveLoadingCard(message = "Requesting camera permission...") + } - ReceiveScreenState.Connecting -> { - ReceiveLoadingCard(message = "Connecting to sender...") - } + ReceiveScreenState.Scanning -> { + ReceiveScanningCard( + onQRCodeScanned = { ticket, confirmation -> + viewModel.onQrCodeScanned(ticket, confirmation) + }, + onError = { error -> + viewModel.onError(error) + }, + onStopScanning = { viewModel.onStopScanning() }, + onEnterManually = { viewModel.onEnterManually() } + ) + } - is ReceiveScreenState.Receiving -> { - ReceiveProgressCard( - progress = state.progress, - onCancel = { - viewModel.onCancelReceiving() - } - ) - } + is ReceiveScreenState.ManualInput -> { + ReceiveManualInputCard( + inputText = _state.inputText, + onInputChange = { + viewModel.onManualInputChanged(it) + }, + inputError = _state.inputError, + onPasteFromClipboard = { viewModel.onPasteFromClipboard(clipboardManager.getText()?.text) }, + onSubmit = { viewModel.handleManualInputSubmit() }, + onCancel = { + viewModel.onCancelManualInput() + } + ) + } - is ReceiveScreenState.Success -> { - if (!showSuccessAnimation) { - ReceiveCompleteCard( - receivedFiles = state.receivedFiles, - onReceiveMore = { - viewModel.onReceiveMore() - }, - onDone = { - viewModel.onDone() - } - ) + is ReceiveScreenState.QRCodeScanned -> { + ReceiveQRCodeScannedCard( + onAccept = { + viewModel.onAccept() + }, + onScanAgain = { + viewModel.onScanAgain() } - } + ) + } - is ReceiveScreenState.Error -> { - ReceiveErrorCard( - error = state.error, - onRetry = { - viewModel.onErrorRetry() + ReceiveScreenState.Connecting -> { + ReceiveLoadingCard(message = "Connecting to sender...") + } + + is ReceiveScreenState.Receiving -> { + ReceiveProgressCard( + progress = _state.progress, + onCancel = { + viewModel.onCancelReceiving() + } + ) + } + + is ReceiveScreenState.Success -> { + if (!showSuccessAnimation) { + ReceiveCompleteCard( + receivedFiles = _state.receivedFiles, + onReceiveMore = { + viewModel.onReceiveMore() }, - onDismiss = { - viewModel.onErrorDismiss() + onDone = { + viewModel.onDone() } ) } } + + is ReceiveScreenState.Error -> { + ReceiveErrorCard( + error = _state.error, + onRetry = { + viewModel.onErrorRetry() + }, + onDismiss = { + viewModel.onErrorDismiss() + } + ) + } } - if (uiState !is ReceiveScreenState.Success - && uiState !is ReceiveScreenState.Error + if (state !is ReceiveScreenState.Success + && state !is ReceiveScreenState.Error ) { Spacer(modifier = Modifier.weight(1f)) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt index 0100a9b..564e512 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt @@ -228,81 +228,65 @@ fun Send( .fillMaxSize() .padding(paddingValues) ) { - // Main content with phase-based transitions - AnimatedContent( - targetState = state, transitionSpec = { - slideInVertically( - initialOffsetY = { it / 3 }, animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessMedium - ) - ) + fadeIn(animationSpec = tween(300)) togetherWith slideOutVertically( - targetOffsetY = { -it / 3 }, animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessMedium - ) - ) + fadeOut(animationSpec = tween(200)) - }, label = "phaseTransition" - ) { phase -> - when (phase) { - is SendScreenState.FileSelection -> { - FileSelectionPhase( - selectedFiles = phase.files, - totalFileSize = phase.size, - onAddFiles = { - viewModel.onAddFiles() - }, - onRemoveFile = { uri -> - viewModel.onFileRemove(uri) - }, - onStartTransfer = { - viewModel.onStartTransfer() - }, - canStartTransfer = phase.canStartTransfer, - listState = listState - ) - } + val _state = state + when (_state) { + is SendScreenState.FileSelection -> { + FileSelectionPhase( + selectedFiles = _state.files, + totalFileSize = _state.size, + onAddFiles = { + viewModel.onAddFiles() + }, + onRemoveFile = { uri -> + viewModel.onFileRemove(uri) + }, + onStartTransfer = { + viewModel.onStartTransfer() + }, + canStartTransfer = _state.canStartTransfer, + listState = listState + ) + } - is SendScreenState.GeneratingQR -> { - GeneratingQRPhase(onCancel = { viewModel.onCancelQrGeneration() }) - } + is SendScreenState.GeneratingQR -> { + GeneratingQRPhase(onCancel = { viewModel.onCancelQrGeneration() }) + } - is SendScreenState.WaitingForReceiver -> { - WaitingForReceiverPhase( - fileCount = phase.files.size, - onCancel = { viewModel.onCancelTransfer() } - ) - } + is SendScreenState.WaitingForReceiver -> { + WaitingForReceiverPhase( + fileCount = _state.files.size, + onCancel = { viewModel.onCancelTransfer() } + ) + } - is SendScreenState.Transfer -> { - TransferringPhase( - progress = phase, - onCancel = { viewModel.onCancelTransfer() } - ) - } + is SendScreenState.Transfer -> { + TransferringPhase( + progress = _state, + onCancel = { viewModel.onCancelTransfer() } + ) + } - is SendScreenState.Complete -> { - TransferCompletePhase( - fileCount = phase.files.size, - onSendMore = { - viewModel.onSendMore() - }, - onDone = { - viewModel.onDone() - }, - ) - } + is SendScreenState.Complete -> { + TransferCompletePhase( + fileCount = _state.files.size, + onSendMore = { + viewModel.onSendMore() + }, + onDone = { + viewModel.onDone() + }, + ) + } - is SendScreenState.Error -> { -// ErrorPhase( -// error = sendState.error, -// onRetry = { handleError("Retry") }, -// onCancel = { -// transferManager.cancelSend() -// navController.navigateUp() -// } -// ) - } + is SendScreenState.Error -> { +// ErrorPhase( +// error = sendState.error, +// onRetry = { handleError("Retry") }, +// onCancel = { +// transferManager.cancelSend() +// navController.navigateUp() +// } +// ) } } @@ -321,7 +305,7 @@ fun Send( qrBitmap = s.qrBitmap, fileCount = s.files.size, copyString = s.copyString, - onDismiss = { }, + onDismiss = { }, onCancel = {} ) } From 63e89eba56f68a31d7e9ed45308772434ef9499d Mon Sep 17 00:00:00 2001 From: Shubert Munthali Date: Sun, 9 Nov 2025 00:24:48 +0200 Subject: [PATCH 12/33] Clean Gradle: Migrate plugins and dependencies to version catalog (#13) * Migrate plugins and dependencies to version catalog * Remove kapt and migrate jvmTarget to jvmToolchain --- app/build.gradle.kts | 45 ++++++++++++++++++--------------------- build.gradle.kts | 7 +++--- gradle/libs.versions.toml | 32 ++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 540cdf3..ca838bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,14 +1,18 @@ plugins { - kotlin("kapt") version "2.2.0" - kotlin("plugin.serialization") version "1.9.23" alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) - id("com.google.dagger.hilt.android") version "2.57.1" - id("com.github.triplet.play") version "3.10.1" + alias(libs.plugins.dagger.hilt) + alias(libs.plugins.triplet.play) alias(libs.plugins.ksp) } +kotlin { + compilerOptions { + jvmToolchain(11) + } +} + android { namespace = "dev.arkbuilders.drop.app" compileSdk = 36 @@ -32,9 +36,10 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" setProperty("archivesBaseName", "ark-drop") - ksp { - arg("room.schemaLocation", "$projectDir/schemas") - } + } + + ksp { + arg("room.schemaLocation", "$projectDir/schemas") } buildTypes { @@ -66,10 +71,6 @@ android { targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" - } - buildFeatures { compose = true buildConfig = true @@ -154,14 +155,14 @@ dependencies { implementation(libs.orbit.viewmodel) // DAGGER SETUP - implementation("com.google.dagger:hilt-android:2.57.1") - implementation("androidx.hilt:hilt-navigation-compose:1.3.0") - ksp("com.google.dagger:hilt-compiler:2.57.1") + implementation(libs.dagger.hilt.android) + implementation(libs.androidx.hilt.nav.compose) + ksp(libs.dagger.hilt.compiler) // EXTRA ICONS - implementation("br.com.devsrsouza.compose.icons:simple-icons:1.1.0") - implementation("br.com.devsrsouza.compose.icons:font-awesome:1.1.0") - implementation("br.com.devsrsouza.compose.icons:tabler-icons:1.1.0") + implementation(libs.simple.icons) + implementation(libs.font.awesome) + implementation(libs.tabler.icons) // DEVELOPMENT SETUP testImplementation(libs.junit) @@ -173,13 +174,9 @@ dependencies { debugImplementation(libs.androidx.ui.test.manifest) // File-system profile manager - implementation("io.coil-kt:coil-compose:2.5.0") - implementation("androidx.compose.foundation:foundation:1.4.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") -} - -kapt { - correctErrorTypes = true + implementation(libs.io.coil) + implementation(libs.androidx.compose.foundation) + implementation(libs.kotlinx.serialization) } tasks.named("clean") { diff --git a/build.gradle.kts b/build.gradle.kts index fc1978c..1bf2a29 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,9 @@ plugins { - kotlin("kapt") version "2.2.0" apply false - kotlin("plugin.serialization") version "1.9.23" apply false + alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false - id("com.google.dagger.hilt.android") version "2.57.1" apply false - id("com.github.triplet.play") version "3.10.1" apply false alias(libs.plugins.ksp) + alias(libs.plugins.dagger.hilt) apply false + alias(libs.plugins.triplet.play) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cce9c10..41d40e8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,13 @@ accompanistPermissions = "0.36.0" timber = "5.0.1" room = "2.8.1" orbitMVI = "9.0.0" +hilt = "2.57.1" +hiltNavCompose = "1.3.0" +kotlinSerialization = "1.9.0" +tripletPlay = "3.10.1" +drop = "17348879247" +devsrsouzaIcons = "1.1.0" +coil = "2.5.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -30,6 +37,7 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } + androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } @@ -38,26 +46,50 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } + androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } google-zxing-core = { group = "com.google.zxing", name = "core", version.ref = "googleZxingCore" } github-yuriy-budiyev-code-scanner = { group = "com.github.yuriy-budiyev", name = "code-scanner", version.ref = "githubYuriyBudiyevCodeScanner" } + androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraCore" } androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraCamera2" } androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraLifecycle" } androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraView" } + mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkitBarcodeScanning" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistPermissions" } + room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-testing = { module = "androidx.room:room-testing", version.ref = "room" } + orbit-compose = { module = "org.orbit-mvi:orbit-compose", version.ref = "orbitMVI" } orbit-viewmodel = { module = "org.orbit-mvi:orbit-viewmodel", version.ref = "orbitMVI" } +arkbuilders-drop = { module = "dev.arkbuilders:drop", version.ref = "drop" } + +dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +androidx-hilt-nav-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavCompose" } +dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } + +simple-icons = { module = "br.com.devsrsouza.compose.icons:simple-icons", version.ref = "devsrsouzaIcons" } +font-awesome = { module = "br.com.devsrsouza.compose.icons:font-awesome", version.ref = "devsrsouzaIcons" } +tabler-icons = { module = "br.com.devsrsouza.compose.icons:tabler-icons", version.ref = "devsrsouzaIcons" } + +io-coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } + + + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"} +dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref="hilt" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +triplet-play = { id = "com.github.triplet.play", version.ref = "tripletPlay" } From 54db1f97678cb002416b99522c017de790c9e4f4 Mon Sep 17 00:00:00 2001 From: Shubert Munthali Date: Wed, 12 Nov 2025 23:30:35 +0200 Subject: [PATCH 13/33] Migrate dependency injection from Hilt to Koin (#14) * Configure koin dependency injection * Remove hilt * Fix koin --- app/build.gradle.kts | 14 +++++-- .../data/datasource/ProfileLocalDataSource.kt | 9 ++--- .../TransferHistoryItemLocalDataSource.kt | 2 +- .../drop/app/data/helper/AvatarHelperImpl.kt | 5 +-- .../app/data/helper/PermissionsHelperImpl.kt | 6 +-- .../app/data/helper/ResourcesHelperImpl.kt | 6 +-- .../app/data/repository/NetworkStatusImpl.kt | 7 +--- .../app/data/repository/ProfileRepoImpl.kt | 8 +--- .../TransferHistoryItemRepositoryImpl.kt | 5 +-- .../app/data/repository/TransferManager.kt | 29 +++++--------- .../dev/arkbuilders/drop/app/di/AppModule.kt | 39 ++++++++++++++----- .../arkbuilders/drop/app/di/TmpEntryPoint.kt | 18 --------- .../drop/app/di/ViewModelsModule.kt | 17 ++++++++ .../app/domain/usecase/ReceiveFilesUseCase.kt | 6 +-- .../app/domain/usecase/SendFilesUseCase.kt | 6 +-- .../drop/app/presentation/DropApplication.kt | 11 +++++- .../drop/app/presentation/MainActivity.kt | 17 +++----- .../drop/app/presentation/history/History.kt | 6 ++- .../presentation/history/HistoryViewModel.kt | 5 +-- .../app/presentation/{Home => home}/Home.kt | 9 +++-- .../{Home => home}/HomeViewModel.kt | 7 +--- .../presentation/profile/EditProfileScreen.kt | 16 +++----- .../profile/EditProfileViewModel.kt | 5 +-- .../drop/app/presentation/receive/Receive.kt | 5 ++- .../presentation/receive/ReceiveViewModel.kt | 7 +--- .../drop/app/presentation/send/Send.kt | 15 ++----- .../app/presentation/send/SendViewModel.kt | 5 +-- .../send/components/SendFileItem.kt | 11 +++--- build.gradle.kts | 2 +- gradle/libs.versions.toml | 6 ++- 30 files changed, 137 insertions(+), 167 deletions(-) delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt rename app/src/main/java/dev/arkbuilders/drop/app/presentation/{Home => home}/Home.kt (98%) rename app/src/main/java/dev/arkbuilders/drop/app/presentation/{Home => home}/HomeViewModel.kt (89%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ca838bc..c7f16e3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,7 +2,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) - alias(libs.plugins.dagger.hilt) + // alias(libs.plugins.dagger.hilt) alias(libs.plugins.triplet.play) alias(libs.plugins.ksp) } @@ -155,9 +155,9 @@ dependencies { implementation(libs.orbit.viewmodel) // DAGGER SETUP - implementation(libs.dagger.hilt.android) - implementation(libs.androidx.hilt.nav.compose) - ksp(libs.dagger.hilt.compiler) + //implementation(libs.dagger.hilt.android) + //implementation(libs.androidx.hilt.nav.compose) + //ksp(libs.dagger.hilt.compiler) // EXTRA ICONS implementation(libs.simple.icons) @@ -177,6 +177,12 @@ dependencies { implementation(libs.io.coil) implementation(libs.androidx.compose.foundation) implementation(libs.kotlinx.serialization) + + // Koin Dependency Injection + implementation(libs.io.koin.core) + implementation(libs.io.koin.android) + implementation(libs.io.koin.compose) + implementation(libs.io.koin.test) } tasks.named("clean") { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt index c33b2c4..2138d23 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt @@ -2,19 +2,16 @@ package dev.arkbuilders.drop.app.data.datasource import android.content.Context import android.content.SharedPreferences -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import javax.inject.Inject import androidx.core.content.edit import dev.arkbuilders.drop.app.data.model.UserAvatarDto import dev.arkbuilders.drop.app.data.model.UserProfileDto import dev.arkbuilders.drop.app.domain.AvatarHelper import dev.arkbuilders.drop.app.domain.model.UserAvatar import dev.arkbuilders.drop.app.domain.model.UserProfile +import kotlinx.serialization.json.Json -class ProfileLocalDataSource @Inject constructor( - @ApplicationContext private val context: Context, +class ProfileLocalDataSource( + private val context: Context, private val avatarHelper: AvatarHelper, ) { private val prefs: SharedPreferences = diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt index bd27f23..94c743c 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject -class TransferHistoryItemLocalDataSource @Inject constructor( +class TransferHistoryItemLocalDataSource( private val dao: TransferHistoryItemDao ) { fun flow(): Flow> = diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt index 6a48ba1..6961beb 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt @@ -9,17 +9,14 @@ import android.net.Uri import android.util.Base64 import androidx.core.graphics.scale import androidx.core.net.toUri -import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.app.domain.AvatarHelper import dev.arkbuilders.drop.app.domain.AvatarHelper.Companion.JPEG_QUALITY import dev.arkbuilders.drop.app.domain.AvatarHelper.Companion.MAX_FILE_SIZE import dev.arkbuilders.drop.app.domain.AvatarHelper.Companion.MAX_IMAGE_SIZE import java.io.ByteArrayOutputStream import java.io.IOException -import javax.inject.Inject -class AvatarHelperImpl @Inject constructor( - @ApplicationContext +class AvatarHelperImpl( private val context: Context, ) : AvatarHelper { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt index ccb739d..5bf4a60 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt @@ -4,12 +4,10 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import androidx.core.content.ContextCompat -import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.app.domain.PermissionsHelper -import javax.inject.Inject -class PermissionsHelperImpl @Inject constructor( - @ApplicationContext private val ctx: Context, +class PermissionsHelperImpl( + private val ctx: Context, ): PermissionsHelper { override fun isCameraGranted(): Boolean { return ContextCompat.checkSelfPermission( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt index 7fd137d..779f1dc 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt @@ -3,13 +3,11 @@ package dev.arkbuilders.drop.app.data.helper import android.content.Context import android.provider.OpenableColumns import androidx.core.net.toUri -import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.app.domain.ResourcesHelper import timber.log.Timber -import javax.inject.Inject -class ResourcesHelperImpl @Inject constructor( - @ApplicationContext private val context: Context, +class ResourcesHelperImpl( + private val context: Context, ) : ResourcesHelper { override fun getFileName(uri: String): String? { return try { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt index cf75c58..ed6c9f8 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt @@ -1,20 +1,17 @@ package dev.arkbuilders.drop.app.data.repository -import android.annotation.SuppressLint import android.content.Context import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.os.Build -import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.app.domain.repository.NetworkStatus import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject -class NetworkStatusImpl @Inject constructor( - @ApplicationContext private val context: Context, +class NetworkStatusImpl( + private val context: Context, ) : NetworkStatus { private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt index 1284115..ce45bc8 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt @@ -1,7 +1,6 @@ package dev.arkbuilders.drop.app.data.repository import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.app.data.datasource.ProfileLocalDataSource import dev.arkbuilders.drop.app.domain.model.UserAvatar import dev.arkbuilders.drop.app.domain.model.UserProfile @@ -9,13 +8,10 @@ import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class ProfileRepoImpl @Inject constructor( +class ProfileRepoImpl( private val localDataSource: ProfileLocalDataSource, - @ApplicationContext private val context: Context, + private val context: Context, ) : ProfileRepo { private val _profile = MutableStateFlow(localDataSource.loadProfile()) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt index abc4d35..3aa0f96 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt @@ -7,11 +7,8 @@ import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import kotlinx.coroutines.flow.Flow import java.time.OffsetDateTime -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class TransferHistoryItemRepositoryImpl @Inject constructor( +class TransferHistoryItemRepositoryImpl( private val localSource: TransferHistoryItemLocalDataSource ): TransferHistoryItemRepository { override val historyItems: Flow> = localSource.flow() diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt index f7c4e3b..da10374 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt @@ -7,32 +7,30 @@ import android.os.Environment import android.provider.MediaStore import android.provider.OpenableColumns import android.util.Log -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.ReceiveFilesBubble import dev.arkbuilders.drop.SendFilesBubble import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl import dev.arkbuilders.drop.app.data.ReceivingProgress import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl import dev.arkbuilders.drop.app.data.SendingProgress -import dev.arkbuilders.drop.app.di.TmpEntryPoint import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase +import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.get import java.io.File import java.io.FileOutputStream -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class TransferManager @Inject constructor( - @ApplicationContext private val context: Context, +class TransferManager( + private val context: Context, private val profileRepo: ProfileRepo, private val transferHistoryRepository: TransferHistoryItemRepository, -) { +): KoinComponent { companion object { private const val TAG = "TransferManager" } @@ -49,11 +47,8 @@ class TransferManager @Inject constructor( get() = receiveSubscriber?.progress suspend fun sendFiles(fileUris: List): SendFilesBubble? = withContext(Dispatchers.IO) { - val entryPoint = EntryPointAccessors.fromApplication( - context.applicationContext, - TmpEntryPoint::class.java - ) - val sendUseCase = entryPoint.sendFilesUseCase() + val sendUseCase: SendFilesUseCase = get() + sendUseCase.invoke(fileUris).fold( onSuccess = { bubble -> currentSendBubble = bubble @@ -70,11 +65,7 @@ class TransferManager @Inject constructor( suspend fun receiveFiles(ticket: String, confirmation: UByte): ReceiveFilesBubble? = withContext(Dispatchers.IO) { - val entryPoint = EntryPointAccessors.fromApplication( - context.applicationContext, - TmpEntryPoint::class.java - ) - val receiveFilesUseCase = entryPoint.receiveFilesUseCase() + val receiveFilesUseCase: ReceiveFilesUseCase = get() receiveFilesUseCase.invoke(ticket, confirmation).fold( onSuccess = { bubble -> diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index 67baada..4d622e6 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -1,28 +1,46 @@ package dev.arkbuilders.drop.app.di -import android.content.Context -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import dev.arkbuilders.drop.app.data.repository.TransferManager -import dev.arkbuilders.drop.app.data.helper.ResourcesHelperImpl +import dev.arkbuilders.drop.app.data.datasource.ProfileLocalDataSource +import dev.arkbuilders.drop.app.data.datasource.TransferHistoryItemLocalDataSource import dev.arkbuilders.drop.app.data.db.Database +import dev.arkbuilders.drop.app.data.db.dao.TransferHistoryItemDao import dev.arkbuilders.drop.app.data.helper.AvatarHelperImpl import dev.arkbuilders.drop.app.data.helper.PermissionsHelperImpl +import dev.arkbuilders.drop.app.data.helper.ResourcesHelperImpl import dev.arkbuilders.drop.app.data.repository.NetworkStatusImpl import dev.arkbuilders.drop.app.data.repository.ProfileRepoImpl import dev.arkbuilders.drop.app.data.repository.TransferHistoryItemRepositoryImpl +import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.domain.AvatarHelper import dev.arkbuilders.drop.app.domain.PermissionsHelper import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.domain.repository.NetworkStatus import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository -import javax.inject.Singleton +import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase +import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase +import org.koin.dsl.module + +val appModule = module { + single { ProfileRepoImpl(get(), get()) } + single { TransferManager(get(), get(), get()) } + single { ResourcesHelperImpl(get()) } + single { Database.build(get()) } + single { TransferHistoryItemRepositoryImpl(get()) } + single { PermissionsHelperImpl(get()) } + single { NetworkStatusImpl(get()) } + single { AvatarHelperImpl(get()) } + single{ ProfileLocalDataSource(get(), get()) } + single{ TransferHistoryItemLocalDataSource(get()) } + factory { + val db: Database = get() + db.transferHistoryDao() + } + factory { SendFilesUseCase(get(), get(), get()) } + factory { ReceiveFilesUseCase(get(), get(), get()) } +} -@Module +/*@Module @InstallIn(SingletonComponent::class) object AppModule { @@ -81,3 +99,4 @@ object AppModule { impl: AvatarHelperImpl ): AvatarHelper = impl } +*/ \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt deleted file mode 100644 index 28ac3ee..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/TmpEntryPoint.kt +++ /dev/null @@ -1,18 +0,0 @@ -package dev.arkbuilders.drop.app.di - -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dev.arkbuilders.drop.app.domain.AvatarHelper -import dev.arkbuilders.drop.app.domain.ResourcesHelper -import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase -import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase - -@EntryPoint -@InstallIn(SingletonComponent::class) -interface TmpEntryPoint { - fun avatarHelper(): AvatarHelper - fun resourcesHelper(): ResourcesHelper - fun sendFilesUseCase(): SendFilesUseCase - fun receiveFilesUseCase(): ReceiveFilesUseCase -} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt new file mode 100644 index 0000000..4d2cc38 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt @@ -0,0 +1,17 @@ +package dev.arkbuilders.drop.app.di + +import dev.arkbuilders.drop.app.presentation.home.HomeViewModel +import dev.arkbuilders.drop.app.presentation.history.HistoryViewModel +import dev.arkbuilders.drop.app.presentation.profile.EditProfileViewModel +import dev.arkbuilders.drop.app.presentation.receive.ReceiveViewModel +import dev.arkbuilders.drop.app.presentation.send.SendViewModel +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +val viewModelsModule = module { + viewModel { HistoryViewModel(get()) } + viewModel { HomeViewModel(get(), get()) } + viewModel { EditProfileViewModel(get(), get()) } + viewModel { ReceiveViewModel(get(), get()) } + viewModel { SendViewModel(get(), get(), get()) } +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt index bf6bcd2..c214f06 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt @@ -1,7 +1,6 @@ package dev.arkbuilders.drop.app.domain.usecase import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.ReceiveFilesBubble import dev.arkbuilders.drop.ReceiveFilesRequest import dev.arkbuilders.drop.ReceiverConfig @@ -12,10 +11,9 @@ import dev.arkbuilders.drop.receiveFiles import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber -import javax.inject.Inject -class ReceiveFilesUseCase @Inject constructor( - @ApplicationContext private val context: Context, +class ReceiveFilesUseCase( + private val context: Context, private val profileRepo: ProfileRepo, private val resourcesHelper: ResourcesHelper, ) { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt index a468a91..ee1edfc 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt @@ -2,7 +2,6 @@ package dev.arkbuilders.drop.app.domain.usecase import android.content.Context import android.net.Uri -import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.drop.SendFilesBubble import dev.arkbuilders.drop.SendFilesRequest import dev.arkbuilders.drop.SenderConfig @@ -15,10 +14,9 @@ import dev.arkbuilders.drop.sendFiles import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber -import javax.inject.Inject -class SendFilesUseCase @Inject constructor( - @ApplicationContext private val context: Context, +class SendFilesUseCase( + private val context: Context, private val profileRepo: ProfileRepo, private val resourcesHelper: ResourcesHelper, ) { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt index f5eceb3..59c4266 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt @@ -1,13 +1,20 @@ package dev.arkbuilders.drop.app.presentation import android.app.Application -import dagger.hilt.android.HiltAndroidApp +import dev.arkbuilders.drop.app.di.appModule +import dev.arkbuilders.drop.app.di.viewModelsModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin import timber.log.Timber -@HiltAndroidApp class DropApplication : Application() { override fun onCreate() { super.onCreate() Timber.Forest.plant(Timber.DebugTree()) + + startKoin { + androidContext(this@DropApplication) + modules(appModule, viewModelsModule) + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt index 99d47f5..bbf8de2 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt @@ -14,30 +14,25 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink -import dagger.hilt.android.AndroidEntryPoint import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository -import dev.arkbuilders.drop.app.presentation.navigation.DropDestination +import dev.arkbuilders.drop.app.presentation.home.Home import dev.arkbuilders.drop.app.presentation.history.History -import dev.arkbuilders.drop.app.presentation.Home.Home +import dev.arkbuilders.drop.app.presentation.navigation.DropDestination import dev.arkbuilders.drop.app.presentation.profile.EditProfileEnhanced import dev.arkbuilders.drop.app.presentation.receive.Receive import dev.arkbuilders.drop.app.presentation.send.Send import dev.arkbuilders.drop.app.presentation.theme.DropTheme -import javax.inject.Inject +import org.koin.android.ext.android.get -@AndroidEntryPoint class MainActivity : ComponentActivity() { - @Inject - lateinit var transferManager: TransferManager + private val transferManager: TransferManager = get() - @Inject - lateinit var profileRepo: ProfileRepo + private val profileRepo: ProfileRepo = get() - @Inject - lateinit var transferHistoryItemRepository: TransferHistoryItemRepository + private val transferHistoryItemRepository: TransferHistoryItemRepository = get() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt index 6f578e5..4d444a3 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.ClearAll @@ -48,6 +48,7 @@ import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import dev.arkbuilders.drop.app.presentation.components.AvatarImageWithFallback +import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState import java.time.Duration import java.time.OffsetDateTime @@ -60,7 +61,8 @@ fun History( navController: NavController, transferHistoryItemRepository: TransferHistoryItemRepository ) { - val viewModel: HistoryViewModel = hiltViewModel() + val viewModel: HistoryViewModel = koinInject() + val scope = rememberCoroutineScope() val state by viewModel.collectAsState() diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt index 31e4665..d441959 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt @@ -2,7 +2,6 @@ package dev.arkbuilders.drop.app.presentation.history import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import kotlinx.coroutines.flow.launchIn @@ -10,7 +9,6 @@ import kotlinx.coroutines.flow.onEach import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject data class HistoryScreenState( val historyItems: List, @@ -20,8 +18,7 @@ data class HistoryScreenState( sealed class HistoryScreenEffect -@HiltViewModel -class HistoryViewModel @Inject constructor( +class HistoryViewModel( private val historyItemRepository: TransferHistoryItemRepository, ) : ViewModel(), ContainerHost { override val container: Container = container( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/Home.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt similarity index 98% rename from app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/Home.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt index 664bae6..0046bc6 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/Home.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.presentation.Home +package dev.arkbuilders.drop.app.presentation.home import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState @@ -35,7 +35,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.ArrowDownCircle @@ -49,7 +49,6 @@ import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.model.UserProfile import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository -import dev.arkbuilders.drop.app.presentation.navigation.DropDestination import dev.arkbuilders.drop.app.presentation.components.AvatarImage import dev.arkbuilders.drop.app.presentation.components.AvatarImageWithFallback import dev.arkbuilders.drop.app.presentation.components.DropButton @@ -61,8 +60,10 @@ import dev.arkbuilders.drop.app.presentation.components.DropCardSize import dev.arkbuilders.drop.app.presentation.components.DropCardVariant import dev.arkbuilders.drop.app.presentation.components.DropOutlinedButton import dev.arkbuilders.drop.app.presentation.components.EmptyState +import dev.arkbuilders.drop.app.presentation.navigation.DropDestination import dev.arkbuilders.drop.app.presentation.theme.DesignTokens import kotlinx.coroutines.delay +import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState import java.time.Duration import java.time.OffsetDateTime @@ -75,7 +76,7 @@ fun Home( profileRepo: ProfileRepo, transferHistoryItemRepository: TransferHistoryItemRepository, ) { - val viewModel: HomeViewModel = hiltViewModel() + val viewModel: HomeViewModel = koinInject() val state by viewModel.collectAsState() var logoScale by remember { mutableStateOf(0f) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/HomeViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt similarity index 89% rename from app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/HomeViewModel.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt index 0180b6a..362ee20 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/Home/HomeViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt @@ -1,8 +1,7 @@ -package dev.arkbuilders.drop.app.presentation.Home +package dev.arkbuilders.drop.app.presentation.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem import dev.arkbuilders.drop.app.domain.model.UserProfile import dev.arkbuilders.drop.app.domain.repository.ProfileRepo @@ -13,7 +12,6 @@ import kotlinx.coroutines.flow.onEach import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject data class HomeScreenState( val historyItems: List, @@ -22,8 +20,7 @@ data class HomeScreenState( sealed class HomeScreenEffect -@HiltViewModel -class HomeViewModel @Inject constructor( +class HomeViewModel( private val historyItemRepository: TransferHistoryItemRepository, private val profileRepo: ProfileRepo, ): ViewModel(), ContainerHost{ diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt index 4d1a68c..4a728fd 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt @@ -75,12 +75,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.Camera -import dagger.hilt.android.EntryPointAccessors -import dev.arkbuilders.drop.app.di.TmpEntryPoint +import dev.arkbuilders.drop.app.domain.AvatarHelper import dev.arkbuilders.drop.app.domain.model.UserAvatar import dev.arkbuilders.drop.app.presentation.components.AvatarImage import dev.arkbuilders.drop.app.presentation.components.DropButton @@ -95,6 +94,7 @@ import dev.arkbuilders.drop.app.presentation.components.ErrorStateDisplay import dev.arkbuilders.drop.app.presentation.components.ErrorType import dev.arkbuilders.drop.app.presentation.theme.DesignTokens import kotlinx.coroutines.delay +import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -103,7 +103,7 @@ import org.orbitmvi.orbit.compose.collectSideEffect fun EditProfileEnhanced( navController: NavController, ) { - val viewModel: EditProfileViewModel = hiltViewModel() + val viewModel: EditProfileViewModel = koinInject() val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current val nameFocusRequester = remember { FocusRequester() } @@ -524,14 +524,8 @@ private fun EnhancedAvatarOption( isSelected: Boolean, onClick: () -> Unit ) { - val context = LocalContext.current val haptic = LocalHapticFeedback.current - val avatarHelper = remember { - EntryPointAccessors.fromApplication( - context.applicationContext, - TmpEntryPoint::class.java - ).avatarHelper() - } + val avatarHelper: AvatarHelper = koinInject() var scale by remember { mutableStateOf(1f) } val animatedScale by animateFloatAsState( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt index 601f6af..a79622f 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt @@ -1,7 +1,6 @@ package dev.arkbuilders.drop.app.presentation.profile import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel import dev.arkbuilders.drop.app.domain.AvatarHelper import dev.arkbuilders.drop.app.domain.model.UserAvatar import dev.arkbuilders.drop.app.domain.model.UserProfile @@ -9,7 +8,6 @@ import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject data class EditProfileScreenState( val currentProfile: UserProfile, @@ -29,8 +27,7 @@ sealed class EditProfileScreenEffect { data object NavigateBack : EditProfileScreenEffect() } -@HiltViewModel -class EditProfileViewModel @Inject constructor( +class EditProfileViewModel( private val profileRepo: ProfileRepo, private val avatarHelper: AvatarHelper, ) : ViewModel(), ContainerHost { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt index a009837..e266fcc 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt @@ -54,7 +54,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import dev.arkbuilders.drop.app.R @@ -69,6 +69,7 @@ import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveReadyToSc import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveScanningCard import dev.arkbuilders.drop.app.presentation.theme.DesignTokens import kotlinx.coroutines.delay +import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -106,7 +107,7 @@ sealed class ReceiveError(val message: String, val isRecoverable: Boolean = true fun Receive( navController: NavController, ) { - val viewModel: ReceiveViewModel = hiltViewModel() + val viewModel: ReceiveViewModel = koinInject() val clipboardManager = LocalClipboardManager.current val keyboardController = LocalSoftwareKeyboardController.current diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt index beea36e..7f71f33 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt @@ -2,9 +2,8 @@ package dev.arkbuilders.drop.app.presentation.receive import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.data.ReceivingProgress +import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.domain.PermissionsHelper import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn @@ -12,7 +11,6 @@ import kotlinx.coroutines.flow.onEach import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject sealed class ReceiveScreenState { data class Initial(val cameraPermissionGranted: Boolean) : ReceiveScreenState() @@ -33,8 +31,7 @@ sealed class ReceiveScreenEffect { data object RequestCameraPermission : ReceiveScreenEffect() } -@HiltViewModel -class ReceiveViewModel @Inject constructor( +class ReceiveViewModel( private val transferManager: TransferManager, private val permissionsHelper: PermissionsHelper, ) : ViewModel(), ContainerHost { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt index 564e512..a06b04a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt @@ -6,16 +6,8 @@ import android.content.Context import android.graphics.Bitmap import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -72,7 +64,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.AlertCircle @@ -92,6 +84,7 @@ import dev.arkbuilders.drop.app.presentation.send.components.phase.TransferringP import dev.arkbuilders.drop.app.presentation.send.components.phase.WaitingForReceiverPhase import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -179,11 +172,9 @@ fun Send( navController: NavController, transferManager: TransferManager ) { - val context = LocalContext.current - val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current val listState = rememberLazyListState() - val viewModel: SendViewModel = hiltViewModel() + val viewModel: SendViewModel = koinInject() val state by viewModel.collectAsState() diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt index 7780c87..df34188 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt @@ -11,7 +11,6 @@ import com.google.zxing.BarcodeFormat import com.google.zxing.WriterException import com.google.zxing.common.BitMatrix import com.google.zxing.qrcode.QRCodeWriter -import dagger.hilt.android.lifecycle.HiltViewModel import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.domain.repository.NetworkStatus @@ -23,7 +22,6 @@ import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject sealed class SendScreenState { data class FileSelection( @@ -66,8 +64,7 @@ sealed class SendScreenEffect { data object NavigateBack : SendScreenEffect() } -@HiltViewModel -class SendViewModel @Inject constructor( +class SendViewModel( private val resourcesHelper: ResourcesHelper, private val networkStatus: NetworkStatus, private val transferManager: TransferManager, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt index 1939f66..1f8318f 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt @@ -34,9 +34,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import compose.icons.TablerIcons import compose.icons.tablericons.FileText -import dagger.hilt.android.EntryPointAccessors -import dev.arkbuilders.drop.app.di.TmpEntryPoint +import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.presentation.DisplayUtils.formatBytes +import org.koin.java.KoinJavaComponent.inject @Composable fun SendFileItem( @@ -49,10 +49,9 @@ fun SendFileItem( LaunchedEffect(uri) { try { - val resourcesHelper = EntryPointAccessors.fromApplication( - context.applicationContext, - TmpEntryPoint::class.java - ).resourcesHelper() + val resourcesHelper: ResourcesHelper = inject( + ResourcesHelper::class.java + ).value fileName = resourcesHelper.getFileName(uri) ?: "Unknown file" fileSize = resourcesHelper.getFileSize(uri) diff --git a/build.gradle.kts b/build.gradle.kts index 1bf2a29..a9531fd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,6 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.ksp) - alias(libs.plugins.dagger.hilt) apply false + // alias(libs.plugins.dagger.hilt) apply false alias(libs.plugins.triplet.play) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41d40e8..023f93a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,7 @@ tripletPlay = "3.10.1" drop = "17348879247" devsrsouzaIcons = "1.1.0" coil = "2.5.0" +koin = "4.1.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -83,7 +84,10 @@ tabler-icons = { module = "br.com.devsrsouza.compose.icons:tabler-icons", versio io-coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } - +io-koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +io-koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +io-koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } +io-koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 7b2579b40a70354bc52eb2a4bdded5c389a9b6ff Mon Sep 17 00:00:00 2001 From: Shubert Munthali Date: Thu, 13 Nov 2025 17:34:20 +0200 Subject: [PATCH 14/33] Add ktlint code styling (#15) * Add ktlint code styling * Add ktlint to build workflow * Update jobs dependencies in build.yml * Migrate dependency injection from Hilt to Koin (#14) * Configure koin dependency injection * Remove hilt * Fix koin * Add ktlint code styling * Add ktlint to build workflow * Update jobs dependencies in build.yml * Update from refactor-data-layer * Remove hilt dependencies * Use Drop dependency from version catalog --- .editorconfig | 34 ++ .github/workflows/build.yml | 167 ++++---- app/build.gradle.kts | 41 +- .../drop/app/ExampleInstrumentedTest.kt | 2 +- .../app/data/ReceiveFilesSubscriberImpl.kt | 83 ++-- .../drop/app/data/SendFilesSubscriberImpl.kt | 42 +- .../drop/app/data/SenderFileDataImpl.kt | 4 +- .../data/datasource/ProfileLocalDataSource.kt | 50 ++- .../TransferHistoryItemLocalDataSource.kt | 57 ++- .../arkbuilders/drop/app/data/db/Database.kt | 5 +- .../app/data/db/dao/TransferHistoryItemDao.kt | 3 +- .../db/entity/TransferHistoryItemEntity.kt | 2 +- .../OffsetDateTimeTypeConverter.kt | 2 +- .../drop/app/data/helper/AvatarHelperImpl.kt | 25 +- .../app/data/helper/PermissionsHelperImpl.kt | 6 +- .../app/data/helper/ResourcesHelperImpl.kt | 11 +- .../drop/app/data/model/UserAvatarDto.kt | 4 +- .../drop/app/data/model/UserProfileDto.kt | 2 +- .../app/data/repository/NetworkStatusImpl.kt | 8 +- .../app/data/repository/ProfileRepoImpl.kt | 3 +- .../TransferHistoryItemRepositoryImpl.kt | 48 ++- .../app/data/repository/TransferManager.kt | 222 +++++----- .../dev/arkbuilders/drop/app/di/AppModule.kt | 37 +- .../drop/app/di/ViewModelsModule.kt | 17 +- .../drop/app/domain/AvatarHelper.kt | 6 +- .../drop/app/domain/PermissionsHelper.kt | 2 +- .../drop/app/domain/ResourcesHelper.kt | 6 +- .../app/domain/model/TransferHistoryItem.kt | 2 +- .../drop/app/domain/model/TransferStatus.kt | 9 +- .../drop/app/domain/model/UserAvatar.kt | 13 +- .../drop/app/domain/model/UserProfile.kt | 2 +- .../app/domain/repository/NetworkStatus.kt | 2 +- .../drop/app/domain/repository/ProfileRepo.kt | 6 +- .../TransferHistoryItemRepository.kt | 6 +- .../app/domain/usecase/ReceiveFilesUseCase.kt | 54 +-- .../app/domain/usecase/SendFilesUseCase.kt | 91 ++-- .../drop/app/presentation/DisplayUtils.kt | 2 +- .../drop/app/presentation/DropApplication.kt | 2 +- .../drop/app/presentation/MainActivity.kt | 36 +- .../presentation/components/AvatarImage.kt | 44 +- .../app/presentation/components/DropButton.kt | 204 +++++---- .../app/presentation/components/DropCard.kt | 122 +++--- .../presentation/components/ErrorStates.kt | 166 ++++---- .../presentation/components/LoadingStates.kt | 121 +++--- .../drop/app/presentation/history/History.kt | 170 ++++---- .../presentation/history/HistoryViewModel.kt | 73 ++-- .../drop/app/presentation/home/Home.kt | 139 +++--- .../app/presentation/home/HomeViewModel.kt | 5 +- .../navigation/DropDestination.kt | 11 +- .../presentation/profile/EditProfileScreen.kt | 398 ++++++++++-------- .../profile/EditProfileViewModel.kt | 101 +++-- .../drop/app/presentation/receive/Receive.kt | 213 +++++----- .../presentation/receive/ReceiveViewModel.kt | 353 +++++++++------- .../receive/components/ReceiveCompleteCard.kt | 86 ++-- .../receive/components/ReceiveErrorCard.kt | 66 +-- .../receive/components/ReceiveLoadingCard.kt | 33 +- .../components/ReceiveManualInputCard.kt | 101 +++-- .../ReceivePermissionRequestCard.kt | 76 ++-- .../receive/components/ReceiveProgressCard.kt | 117 ++--- .../components/ReceiveQRCodeScannedCard.kt | 70 +-- .../components/ReceiveReadyToScanCard.kt | 88 ++-- .../receive/components/ReceiveScanningCard.kt | 82 ++-- .../drop/app/presentation/send/Send.kt | 214 +++++----- .../app/presentation/send/SendViewModel.kt | 246 ++++++----- .../send/components/SendButton.kt | 55 ++- .../presentation/send/components/SendCard.kt | 20 +- .../send/components/SendEmptyState.kt | 25 +- .../send/components/SendErrorOverlay.kt | 62 +-- .../send/components/SendFileItem.kt | 46 +- .../send/components/SendInstructionsCard.kt | 43 +- .../send/components/SendLoadingIndicator.kt | 20 +- .../send/components/SendProgressBar.kt | 14 +- .../send/components/phase/ErrorPhase.kt | 53 ++- .../components/phase/FileSelectionPhase.kt | 72 ++-- .../components/phase/GeneratingQRPhase.kt | 17 +- .../components/phase/TransferCompletePhase.kt | 63 +-- .../components/phase/TransferringPhase.kt | 94 +++-- .../phase/WaitingForReceiverPhase.kt | 46 +- .../app/presentation/theme/DesignTokens.kt | 65 ++- .../drop/app/presentation/theme/Theme.kt | 182 ++++---- .../drop/app/presentation/theme/TopBar.kt | 18 +- .../drop/app/presentation/theme/Type.kt | 242 ++++++----- .../arkbuilders/drop/app/ExampleUnitTest.kt | 2 +- build.gradle.kts | 2 +- gradle/libs.versions.toml | 6 +- 85 files changed, 3100 insertions(+), 2460 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..015d3bc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,34 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +tab_width = 4 +no-unused-imports = true +no-wildcard-import = true +max_line_length = 100 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{kt,kts}] +ktlint_code_style = ktlint_official +ktlint_function_naming_ignore_when_annotated_with=Composable +ktlint_standard_annotation = disabled +ktlint_ignore_back_ticked_identifier = true +ktlint_standard = enabled +ktlint_standard_multiline-if-else = disabled + +# Don't allow any wildcard imports +ij_kotlin_packages_to_use_import_on_demand = unset + +# Prevent wildcard imports +ij_kotlin_name_count_to_use_star_import = 99 +ij_kotlin_name_count_to_use_star_import_for_members = 99 + +[*.md] +trim_trailing_whitespace = false + +[**/test/**.kt] +max_line_length=off diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6da73a..e6c0d64 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,83 +1,100 @@ name: Build the app on: - push: - branches: [ main ] - pull_request: - branches: [ main ] + push: + branches: [ main ] + pull_request: + branches: [ main ] env: - BRANCH_NAME: ${{ github.ref_name }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH_NAME: ${{ github.ref_name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: - check: - if: ${{ startsWith(github.actor, 'dependabot') }} - environment: Development - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'adopt' - cache: gradle - - - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v3 - - - name: Build debug APK - run: ./gradlew assembleDebug - - build: - environment: Development - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'adopt' - cache: gradle - - - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v3 - - - name: Decrypt the keystore for signing - run: | - echo "${{ secrets.KEYSTORE_ENCRYPTED }}" > keystore.asc - gpg -d --passphrase "${{ secrets.KEYSTORE_PASSWORD }}" --batch keystore.asc > keystore.jks - - - name: Build release APK - run: ./gradlew assembleRelease - - - name: Upload APK - uses: actions/upload-artifact@v4 - with: - name: ark-drop-release - path: ./app/build/outputs/apk/release/ark-drop-release.apk - lint: - needs: build - environment: Development - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'adopt' - - - name: Run linter - run: ./gradlew lint - - - uses: actions/upload-artifact@v4 - with: - name: lint-results - path: ./app/build/reports/*.html + check: + needs: [lint, ktlint] + if: ${{ startsWith(github.actor, 'dependabot') }} + environment: Development + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'adopt' + cache: gradle + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v3 + + - name: Build debug APK + run: ./gradlew assembleDebug + + build: + needs: [check, lint, ktlint] + environment: Development + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'adopt' + cache: gradle + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v3 + + - name: Decrypt the keystore for signing + run: | + echo "${{ secrets.KEYSTORE_ENCRYPTED }}" > keystore.asc + gpg -d --passphrase "${{ secrets.KEYSTORE_PASSWORD }}" --batch keystore.asc > keystore.jks + + - name: Build release APK + run: ./gradlew assembleRelease + + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: ark-drop-release + path: ./app/build/outputs/apk/release/ark-drop-release.apk + + lint: + environment: Development + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'adopt' + + - name: Run linter + run: ./gradlew lint + + - uses: actions/upload-artifact@v4 + with: + name: lint-results + path: ./app/build/reports/*.html + + ktlint: + environment: Development + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'adopt' + + - name: Run KtLint + run: ./gradlew ktlintCheck diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c7f16e3..03d64ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,10 +1,12 @@ +import com.android.build.gradle.internal.tasks.factory.dependsOn + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) - // alias(libs.plugins.dagger.hilt) alias(libs.plugins.triplet.play) alias(libs.plugins.ksp) + alias(libs.plugins.ktlint.gradle) } kotlin { @@ -56,7 +58,7 @@ android { signingConfig = signingConfigs.getByName("testRelease") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) // Enable R8 full mode @@ -79,17 +81,19 @@ android { packaging { jniLibs.excludes.add("META-INF/AL2.0") jniLibs.excludes.add("META-INF/LGPL2.1") - resources.excludes.addAll(listOf( - "META-INF/DEPENDENCIES", - "META-INF/LICENSE", - "META-INF/LICENSE.txt", - "META-INF/license.txt", - "META-INF/NOTICE", - "META-INF/NOTICE.txt", - "META-INF/notice.txt", - "META-INF/ASL2.0", - "META-INF/*.kotlin_module" - )) + resources.excludes.addAll( + listOf( + "META-INF/DEPENDENCIES", + "META-INF/LICENSE", + "META-INF/LICENSE.txt", + "META-INF/license.txt", + "META-INF/NOTICE", + "META-INF/NOTICE.txt", + "META-INF/notice.txt", + "META-INF/ASL2.0", + "META-INF/*.kotlin_module", + ), + ) } bundle { @@ -126,7 +130,7 @@ dependencies { } } //noinspection Aligned16KB - implementation("dev.arkbuilders:drop:17348879247") { + implementation(libs.arkbuilders.drop) { artifact { extension = "aar" type = "aar" @@ -154,11 +158,6 @@ dependencies { implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) - // DAGGER SETUP - //implementation(libs.dagger.hilt.android) - //implementation(libs.androidx.hilt.nav.compose) - //ksp(libs.dagger.hilt.compiler) - // EXTRA ICONS implementation(libs.simple.icons) implementation(libs.font.awesome) @@ -185,7 +184,9 @@ dependencies { implementation(libs.io.koin.test) } +tasks.preBuild.dependsOn(tasks.ktlintCheck) +tasks.ktlintCheck.dependsOn(tasks.ktlintFormat) + tasks.named("clean") { delete(fileTree("$projectDir/src/main/jniLibs")) } - diff --git a/app/src/androidTest/java/dev/arkbuilders/drop/app/ExampleInstrumentedTest.kt b/app/src/androidTest/java/dev/arkbuilders/drop/app/ExampleInstrumentedTest.kt index 12b1c32..2d5d626 100644 --- a/app/src/androidTest/java/dev/arkbuilders/drop/app/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/dev/arkbuilders/drop/app/ExampleInstrumentedTest.kt @@ -19,4 +19,4 @@ class ExampleInstrumentedTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext Assert.assertEquals("dev.arkbuilders.drop", appContext.packageName) } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/ReceiveFilesSubscriberImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/ReceiveFilesSubscriberImpl.kt index 3f9a885..a06c50a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/ReceiveFilesSubscriberImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/ReceiveFilesSubscriberImpl.kt @@ -16,22 +16,21 @@ data class ReceivingProgress( val senderName: String = "", val senderAvatar: String? = null, val files: List = emptyList(), - val fileProgress: Map = emptyMap() + val fileProgress: Map = emptyMap(), ) data class ReceiveFileInfo( - val id: String, - val name: String, - val size: ULong + val id: String, + val name: String, + val size: ULong, ) data class FileProgressInfo( val receivedBytes: Long = 0L, - val isComplete: Boolean = false + val isComplete: Boolean = false, ) class ReceiveFilesSubscriberImpl : ReceiveFilesSubscriber { - companion object { private const val TAG = "ReceiveFilesSubscriber" } @@ -40,7 +39,7 @@ class ReceiveFilesSubscriberImpl : ReceiveFilesSubscriber { // Thread-safe storage for received data using ByteArrayOutputStream for efficient appending private val receivedDataStreams = ConcurrentHashMap() - + private val _progress = MutableStateFlow(ReceivingProgress()) val progress: StateFlow = _progress.asStateFlow() @@ -55,32 +54,34 @@ class ReceiveFilesSubscriberImpl : ReceiveFilesSubscriber { // Get or create ByteArrayOutputStream for this file val stream = receivedDataStreams.getOrPut(event.id) { ByteArrayOutputStream() } - + // Efficiently append data to the stream synchronized(stream) { stream.write(event.data) } - + // Find the file info to get expected size val currentProgress = _progress.value val fileInfo = currentProgress.files.find { it.id == event.id } - + if (fileInfo != null) { val receivedBytes = stream.size().toLong() val isComplete = receivedBytes.toULong() >= fileInfo.size - + // Update progress with new file progress info val updatedFileProgress = currentProgress.fileProgress.toMutableMap() - updatedFileProgress[event.id] = FileProgressInfo( - receivedBytes = receivedBytes, - isComplete = isComplete - ) - + updatedFileProgress[event.id] = + FileProgressInfo( + receivedBytes = receivedBytes, + isComplete = isComplete, + ) + // Emit new state - _progress.value = currentProgress.copy( - fileProgress = updatedFileProgress.toMap() - ) - + _progress.value = + currentProgress.copy( + fileProgress = updatedFileProgress.toMap(), + ) + if (isComplete) { Log.d(TAG, "File ${fileInfo.name} completed: $receivedBytes bytes") } @@ -90,18 +91,22 @@ class ReceiveFilesSubscriberImpl : ReceiveFilesSubscriber { override fun notifyConnecting(event: ReceiveFilesConnectingEvent) { Log.d(TAG, "Connected to sender: ${event.sender.name}, files: ${event.files.size}") - val fileInfos = event.files.map { file -> - ReceiveFileInfo( - id = file.id, name = file.name, size = file.len - ) - } + val fileInfos = + event.files.map { file -> + ReceiveFileInfo( + id = file.id, + name = file.name, + size = file.len, + ) + } - _progress.value = _progress.value.copy( - isConnected = true, - senderName = event.sender.name, - senderAvatar = event.sender.avatarB64, - files = fileInfos - ) + _progress.value = + _progress.value.copy( + isConnected = true, + senderName = event.sender.name, + senderAvatar = event.sender.avatarB64, + files = fileInfos, + ) } fun reset() { @@ -132,7 +137,7 @@ class ReceiveFilesSubscriberImpl : ReceiveFilesSubscriber { } } } - + /** * Get progress for a specific file (0.0 to 1.0) */ @@ -140,29 +145,29 @@ class ReceiveFilesSubscriberImpl : ReceiveFilesSubscriber { val currentProgress = _progress.value val fileInfo = currentProgress.files.find { it.id == fileId } val progressInfo = currentProgress.fileProgress[fileId] - + return if (fileInfo != null && progressInfo != null && fileInfo.size > 0UL) { (progressInfo.receivedBytes.toFloat() / fileInfo.size.toFloat()).coerceIn(0f, 1f) } else { 0f } } - + /** * Get received bytes for a specific file */ fun getReceivedBytes(fileId: String): Long { return _progress.value.fileProgress[fileId]?.receivedBytes ?: 0L } - + /** * Check if all files are complete */ public fun areAllFilesComplete(): Boolean { val currentProgress = _progress.value - return currentProgress.files.isNotEmpty() && - currentProgress.files.all { file -> - currentProgress.fileProgress[file.id]?.isComplete == true - } + return currentProgress.files.isNotEmpty() && + currentProgress.files.all { file -> + currentProgress.fileProgress[file.id]?.isComplete == true + } } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/SendFilesSubscriberImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/SendFilesSubscriberImpl.kt index 4705990..12ad1b9 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/SendFilesSubscriberImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/SendFilesSubscriberImpl.kt @@ -15,20 +15,19 @@ data class SendingProgress( val remaining: ULong = 0UL, val isConnected: Boolean = false, val receiverName: String = "", - val receiverAvatar: String? = null + val receiverAvatar: String? = null, ) class SendFilesSubscriberImpl : SendFilesSubscriber { - companion object { private const val TAG = "SendFilesSubscriber" } - + private val id = UUID.randomUUID().toString() - + private val _progress = MutableStateFlow(SendingProgress()) val progress: StateFlow = _progress.asStateFlow() - + override fun getId(): String = id override fun log(message: String) { @@ -36,25 +35,30 @@ class SendFilesSubscriberImpl : SendFilesSubscriber { } override fun notifySending(event: SendFilesSendingEvent) { - Log.d(TAG, "Sending progress: ${event.name} - sent: ${event.sent}, remaining: ${event.remaining}") - - _progress.value = _progress.value.copy( - fileName = event.name, - sent = event.sent, - remaining = event.remaining + Log.d( + TAG, + "Sending progress: ${event.name} - sent: ${event.sent}, remaining: ${event.remaining}", ) + + _progress.value = + _progress.value.copy( + fileName = event.name, + sent = event.sent, + remaining = event.remaining, + ) } - + override fun notifyConnecting(event: SendFilesConnectingEvent) { Log.d(TAG, "Connected to receiver: ${event.receiver.name}") - - _progress.value = _progress.value.copy( - isConnected = true, - receiverName = event.receiver.name, - receiverAvatar = event.receiver.avatarB64 - ) + + _progress.value = + _progress.value.copy( + isConnected = true, + receiverName = event.receiver.name, + receiverAvatar = event.receiver.avatarB64, + ) } - + fun reset() { _progress.value = SendingProgress() } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/SenderFileDataImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/SenderFileDataImpl.kt index 9379dcd..5dffa1b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/SenderFileDataImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/SenderFileDataImpl.kt @@ -7,9 +7,9 @@ import dev.arkbuilders.drop.SenderFileData import java.io.InputStream class SenderFileDataImpl( - private val context: Context, private val uri: Uri + private val context: Context, + private val uri: Uri, ) : SenderFileData { - companion object { private const val TAG = "SenderFileDataImpl" } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt index 2138d23..db56660 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt @@ -36,13 +36,15 @@ class ProfileLocalDataSource( private fun createDefaultProfile(): UserProfile { val defaultAvatarId = "avatar_00" - val default = UserProfile( - name = "Anonymous", - avatar = UserAvatar( - base64 = avatarHelper.getDefaultAvatarBase64(defaultAvatarId), - predefinedId = defaultAvatarId, + val default = + UserProfile( + name = "Anonymous", + avatar = + UserAvatar( + base64 = avatarHelper.getDefaultAvatarBase64(defaultAvatarId), + predefinedId = defaultAvatarId, + ), ) - ) saveProfile(default) return default } @@ -60,22 +62,26 @@ class ProfileLocalDataSource( } } -private fun UserProfileDto.toDomain() = UserProfile( - name = name, - avatar = avatar.toDomain(), -) +private fun UserProfileDto.toDomain() = + UserProfile( + name = name, + avatar = avatar.toDomain(), + ) -private fun UserProfile.toDto() = UserProfileDto( - name = name, - avatar = avatar.toDto(), -) +private fun UserProfile.toDto() = + UserProfileDto( + name = name, + avatar = avatar.toDto(), + ) -private fun UserAvatar.toDto() = UserAvatarDto( - base64 = base64, - predefinedId = predefinedId, -) +private fun UserAvatar.toDto() = + UserAvatarDto( + base64 = base64, + predefinedId = predefinedId, + ) -private fun UserAvatarDto.toDomain() = UserAvatar( - base64 = base64, - predefinedId = predefinedId, -) \ No newline at end of file +private fun UserAvatarDto.toDomain() = + UserAvatar( + base64 = base64, + predefinedId = predefinedId, + ) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt index 94c743c..9e84c22 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt @@ -5,46 +5,45 @@ import dev.arkbuilders.drop.app.data.db.entity.TransferHistoryItemEntity import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject class TransferHistoryItemLocalDataSource( - private val dao: TransferHistoryItemDao + private val dao: TransferHistoryItemDao, ) { fun flow(): Flow> = dao.getAll().map { list -> list.map { it.toDomain() } } - suspend fun add(item: TransferHistoryItem): Long = - dao.insert(item.toEntity()) + suspend fun add(item: TransferHistoryItem): Long = dao.insert(item.toEntity()) suspend fun addAll(items: List) = dao.insertAll(items.map { it.toEntity() }) - suspend fun delete(itemId: Long) = - dao.deleteById(itemId) + suspend fun delete(itemId: Long) = dao.deleteById(itemId) suspend fun clear() = dao.clear() } -fun TransferHistoryItemEntity.toDomain() = TransferHistoryItem( - id = id, - fileName = fileName, - fileSize = fileSize, - type = type, - timestamp = timestamp, - status = status, - peerName = peerName, - peerAvatar = peerAvatar, - fileCount = fileCount -) - -fun TransferHistoryItem.toEntity() = TransferHistoryItemEntity( - id = id, - fileName = fileName, - fileSize = fileSize, - type = type, - timestamp = timestamp, - status = status, - peerName = peerName, - peerAvatar = peerAvatar, - fileCount = fileCount -) \ No newline at end of file +fun TransferHistoryItemEntity.toDomain() = + TransferHistoryItem( + id = id, + fileName = fileName, + fileSize = fileSize, + type = type, + timestamp = timestamp, + status = status, + peerName = peerName, + peerAvatar = peerAvatar, + fileCount = fileCount, + ) + +fun TransferHistoryItem.toEntity() = + TransferHistoryItemEntity( + id = id, + fileName = fileName, + fileSize = fileSize, + type = type, + timestamp = timestamp, + status = status, + peerName = peerName, + peerAvatar = peerAvatar, + fileCount = fileCount, + ) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt index 4e69e8c..e8dc1d1 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt @@ -13,13 +13,12 @@ import dev.arkbuilders.drop.app.data.db.typeconverters.OffsetDateTimeTypeConvert TransferHistoryItemEntity::class, ], version = 1, - exportSchema = true + exportSchema = true, ) @TypeConverters( OffsetDateTimeTypeConverter::class, ) abstract class Database : RoomDatabase() { - abstract fun transferHistoryDao(): TransferHistoryItemDao companion object { @@ -29,4 +28,4 @@ abstract class Database : RoomDatabase() { Room.databaseBuilder(ctx, Database::class.java, DB_NAME) .build() } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt index 3cc14f2..db90a81 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt @@ -4,7 +4,6 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import androidx.room.Upsert import dev.arkbuilders.drop.app.data.db.entity.TransferHistoryItemEntity import kotlinx.coroutines.flow.Flow @@ -24,4 +23,4 @@ interface TransferHistoryItemDao { @Query("DELETE FROM TransferHistoryItemEntity") suspend fun clear() -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt index b84283a..d85214b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt @@ -18,4 +18,4 @@ data class TransferHistoryItemEntity( val peerName: String, val peerAvatar: String?, val fileCount: Int, -) \ No newline at end of file +) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt index f333bdd..b91bab6 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt @@ -13,4 +13,4 @@ object OffsetDateTimeTypeConverter { val offset = OffsetDateTime.now().offset return date.withOffsetSameInstant(offset) } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt index 6961beb..9030ec7 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt @@ -19,7 +19,6 @@ import java.io.IOException class AvatarHelperImpl( private val context: Context, ) : AvatarHelper { - override fun uriToBase64(uri: String): String? { return try { val bitmap = loadBitmapFromUri(uri.toUri()) ?: return null @@ -30,7 +29,6 @@ class AvatarHelperImpl( } } - private fun loadBitmapFromUri(uri: Uri): Bitmap? { return try { val source = ImageDecoder.createSource(context.contentResolver, uri) @@ -47,11 +45,12 @@ class AvatarHelperImpl( val height = bitmap.height // Calculate scaling factor - val scaleFactor = if (width > height) { - MAX_IMAGE_SIZE.toFloat() / width - } else { - MAX_IMAGE_SIZE.toFloat() / height - } + val scaleFactor = + if (width > height) { + MAX_IMAGE_SIZE.toFloat() / width + } else { + MAX_IMAGE_SIZE.toFloat() / height + } return if (scaleFactor < 1f) { val newWidth = (width * scaleFactor).toInt() @@ -79,13 +78,15 @@ class AvatarHelperImpl( } } - @SuppressLint("DiscouragedApi") override fun getDefaultAvatarBase64(avatarId: String): String { return try { - val resourceId = context.resources.getIdentifier( - avatarId, "drawable", context.packageName - ) + val resourceId = + context.resources.getIdentifier( + avatarId, + "drawable", + context.packageName, + ) if (resourceId != 0) { val bitmap = BitmapFactory.decodeResource(context.resources, resourceId) bitmapToBase64(bitmap) ?: "" @@ -96,4 +97,4 @@ class AvatarHelperImpl( "" } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt index 5bf4a60..3b8d37a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt @@ -8,11 +8,11 @@ import dev.arkbuilders.drop.app.domain.PermissionsHelper class PermissionsHelperImpl( private val ctx: Context, -): PermissionsHelper { +) : PermissionsHelper { override fun isCameraGranted(): Boolean { return ContextCompat.checkSelfPermission( ctx, - Manifest.permission.CAMERA + Manifest.permission.CAMERA, ) == PackageManager.PERMISSION_GRANTED } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt index 779f1dc..eef83ed 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt @@ -18,7 +18,9 @@ class ResourcesHelperImpl( if (cursor.moveToFirst()) { val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (nameIndex >= 0) cursor.getString(nameIndex) else null - } else null + } else { + null + } } } catch (e: Exception) { Timber.Forest.e(e, "Error getting filename for URI: $uri") @@ -52,11 +54,12 @@ class ResourcesHelperImpl( if (cursor.moveToFirst()) { val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) if (sizeIndex >= 0) cursor.getLong(sizeIndex) else 0L - } else 0L + } else { + 0L + } } ?: 0L } catch (e: Exception) { 0L } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt index 1a6fab2..cdafc8e 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt @@ -5,5 +5,5 @@ import kotlinx.serialization.Serializable @Serializable data class UserAvatarDto( val base64: String, - val predefinedId: String? -) \ No newline at end of file + val predefinedId: String?, +) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt index 567e2f3..5688139 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt @@ -6,4 +6,4 @@ import kotlinx.serialization.Serializable data class UserProfileDto( val name: String, val avatar: UserAvatarDto, -) \ No newline at end of file +) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt index ed6c9f8..8655878 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt @@ -51,11 +51,11 @@ class NetworkStatusImpl( return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) } else { networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt index ce45bc8..27bc088 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt @@ -13,7 +13,6 @@ class ProfileRepoImpl( private val localDataSource: ProfileLocalDataSource, private val context: Context, ) : ProfileRepo { - private val _profile = MutableStateFlow(localDataSource.loadProfile()) override val profile: StateFlow = _profile.asStateFlow() @@ -29,4 +28,4 @@ class ProfileRepoImpl( override fun updateAvatar(avatar: UserAvatar) { updateProfile(_profile.value.copy(avatar = avatar)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt index 3aa0f96..6284a70 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt @@ -9,8 +9,8 @@ import kotlinx.coroutines.flow.Flow import java.time.OffsetDateTime class TransferHistoryItemRepositoryImpl( - private val localSource: TransferHistoryItemLocalDataSource -): TransferHistoryItemRepository { + private val localSource: TransferHistoryItemLocalDataSource, +) : TransferHistoryItemRepository { override val historyItems: Flow> = localSource.flow() override suspend fun addSentTransfer( @@ -21,16 +21,17 @@ class TransferHistoryItemRepositoryImpl( fileCount: Int, status: TransferStatus, ) { - val newItem = TransferHistoryItem( - fileName = if (fileCount > 1) "$fileName and ${fileCount - 1} more" else fileName, - fileSize = fileSize, - type = TransferType.SENT, - timestamp = OffsetDateTime.now(), - status = status, - peerName = peerName, - peerAvatar = peerAvatar, - fileCount = fileCount - ) + val newItem = + TransferHistoryItem( + fileName = if (fileCount > 1) "$fileName and ${fileCount - 1} more" else fileName, + fileSize = fileSize, + type = TransferType.SENT, + timestamp = OffsetDateTime.now(), + status = status, + peerName = peerName, + peerAvatar = peerAvatar, + fileCount = fileCount, + ) localSource.add(newItem) } @@ -42,16 +43,17 @@ class TransferHistoryItemRepositoryImpl( fileCount: Int, status: TransferStatus, ) { - val newItem = TransferHistoryItem( - fileName = if (fileCount > 1) "$fileName and ${fileCount - 1} more" else fileName, - fileSize = fileSize, - type = TransferType.RECEIVED, - timestamp = OffsetDateTime.now(), - status = status, - peerName = peerName, - peerAvatar = peerAvatar, - fileCount = fileCount - ) + val newItem = + TransferHistoryItem( + fileName = if (fileCount > 1) "$fileName and ${fileCount - 1} more" else fileName, + fileSize = fileSize, + type = TransferType.RECEIVED, + timestamp = OffsetDateTime.now(), + status = status, + peerName = peerName, + peerAvatar = peerAvatar, + fileCount = fileCount, + ) localSource.add(newItem) } @@ -62,4 +64,4 @@ class TransferHistoryItemRepositoryImpl( override suspend fun clearHistory() { localSource.clear() } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt index da10374..f6a780d 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt @@ -30,7 +30,7 @@ class TransferManager( private val context: Context, private val profileRepo: ProfileRepo, private val transferHistoryRepository: TransferHistoryItemRepository, -): KoinComponent { +) : KoinComponent { companion object { private const val TAG = "TransferManager" } @@ -46,33 +46,39 @@ class TransferManager( val receiveProgress: StateFlow? get() = receiveSubscriber?.progress - suspend fun sendFiles(fileUris: List): SendFilesBubble? = withContext(Dispatchers.IO) { - val sendUseCase: SendFilesUseCase = get() + suspend fun sendFiles(fileUris: List): SendFilesBubble? = + withContext(Dispatchers.IO) { + val sendUseCase: SendFilesUseCase = get() - sendUseCase.invoke(fileUris).fold( - onSuccess = { bubble -> - currentSendBubble = bubble - sendSubscriber = SendFilesSubscriberImpl().also { subscriber -> - bubble.subscribe(subscriber) - } - return@withContext bubble - }, - onFailure = { - return@withContext null - }, - ) - } + sendUseCase.invoke(fileUris).fold( + onSuccess = { bubble -> + currentSendBubble = bubble + sendSubscriber = + SendFilesSubscriberImpl().also { subscriber -> + bubble.subscribe(subscriber) + } + return@withContext bubble + }, + onFailure = { + return@withContext null + }, + ) + } - suspend fun receiveFiles(ticket: String, confirmation: UByte): ReceiveFilesBubble? = + suspend fun receiveFiles( + ticket: String, + confirmation: UByte, + ): ReceiveFilesBubble? = withContext(Dispatchers.IO) { val receiveFilesUseCase: ReceiveFilesUseCase = get() receiveFilesUseCase.invoke(ticket, confirmation).fold( onSuccess = { bubble -> currentReceiveBubble = bubble - receiveSubscriber = ReceiveFilesSubscriberImpl().also { subscriber -> - bubble.subscribe(subscriber) - } + receiveSubscriber = + ReceiveFilesSubscriberImpl().also { subscriber -> + bubble.subscribe(subscriber) + } bubble.start() return@withContext bubble }, @@ -82,80 +88,83 @@ class TransferManager( ) } - suspend fun saveReceivedFiles(): List = withContext(Dispatchers.IO) { - val subscriber = receiveSubscriber ?: return@withContext emptyList() - val completeFiles = subscriber.getCompleteFiles() - val savedFiles = mutableListOf() + suspend fun saveReceivedFiles(): List = + withContext(Dispatchers.IO) { + val subscriber = receiveSubscriber ?: return@withContext emptyList() + val completeFiles = subscriber.getCompleteFiles() + val savedFiles = mutableListOf() - try { - completeFiles.forEach { (fileInfo, data) -> - val savedFile = saveFileToDownloads(fileInfo.name, data) - if (savedFile != null) { - savedFiles.add(savedFile) - Log.d(TAG, "Saved file: ${savedFile.absolutePath}") - } else { - Log.e(TAG, "Failed to save file: ${fileInfo.name}") + try { + completeFiles.forEach { (fileInfo, data) -> + val savedFile = saveFileToDownloads(fileInfo.name, data) + if (savedFile != null) { + savedFiles.add(savedFile) + Log.d(TAG, "Saved file: ${savedFile.absolutePath}") + } else { + Log.e(TAG, "Failed to save file: ${fileInfo.name}") + } } - } - // Add to history if files were saved successfully - if (savedFiles.isNotEmpty()) { + // Add to history if files were saved successfully + if (savedFiles.isNotEmpty()) { + val progress = receiveSubscriber?.progress?.value + val senderName = progress?.senderName ?: "Unknown" + val senderAvatar = progress?.senderAvatar + + val totalSize = completeFiles.sumOf { it.second.size.toLong() } + val firstFileName = savedFiles.firstOrNull()?.name ?: "Unknown" + + transferHistoryRepository.addReceivedTransfer( + fileName = firstFileName, + fileSize = totalSize, + peerName = senderName, + peerAvatar = senderAvatar, + fileCount = savedFiles.size, + status = TransferStatus.COMPLETED, + ) + } + } catch (e: Exception) { + Log.e(TAG, "Error saving received files", e) + + // Add failed transfer to history val progress = receiveSubscriber?.progress?.value val senderName = progress?.senderName ?: "Unknown" val senderAvatar = progress?.senderAvatar - val totalSize = completeFiles.sumOf { it.second.size.toLong() } - val firstFileName = savedFiles.firstOrNull()?.name ?: "Unknown" - transferHistoryRepository.addReceivedTransfer( - fileName = firstFileName, - fileSize = totalSize, + fileName = "Transfer failed", + fileSize = 0L, peerName = senderName, peerAvatar = senderAvatar, - fileCount = savedFiles.size, - status = TransferStatus.COMPLETED + fileCount = completeFiles.size, + status = TransferStatus.FAILED, ) } - } catch (e: Exception) { - Log.e(TAG, "Error saving received files", e) - - // Add failed transfer to history - val progress = receiveSubscriber?.progress?.value - val senderName = progress?.senderName ?: "Unknown" - val senderAvatar = progress?.senderAvatar - - transferHistoryRepository.addReceivedTransfer( - fileName = "Transfer failed", - fileSize = 0L, - peerName = senderName, - peerAvatar = senderAvatar, - fileCount = completeFiles.size, - status = TransferStatus.FAILED - ) + savedFiles } - savedFiles - } - suspend fun recordSendCompletion(fileUris: List) { try { val progress = sendSubscriber?.progress?.value val receiverName = progress?.receiverName ?: "Unknown" val receiverAvatar = progress?.receiverAvatar - val totalSize = fileUris.sumOf { uri -> - try { - context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - if (sizeIndex >= 0) cursor.getLong(sizeIndex) else 0L - } else 0L - } ?: 0L - } catch (_: Exception) { - 0L + val totalSize = + fileUris.sumOf { uri -> + try { + context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + if (sizeIndex >= 0) cursor.getLong(sizeIndex) else 0L + } else { + 0L + } + } ?: 0L + } catch (_: Exception) { + 0L + } } - } val firstFileName = getFileName(fileUris.firstOrNull()) ?: "Unknown" @@ -165,14 +174,17 @@ class TransferManager( peerName = receiverName, peerAvatar = receiverAvatar, fileCount = fileUris.size, - status = TransferStatus.COMPLETED + status = TransferStatus.COMPLETED, ) } catch (e: Exception) { Log.e(TAG, "Error recording send completion", e) } } - private suspend fun saveFileToDownloads(fileName: String, data: ByteArray): File? = + private suspend fun saveFileToDownloads( + fileName: String, + data: ByteArray, + ): File? = withContext(Dispatchers.IO) { try { // Use MediaStore for Android 10+ (Scoped Storage) @@ -183,7 +195,10 @@ class TransferManager( } } - private fun saveFileUsingMediaStore(fileName: String, data: ByteArray): File? { + private fun saveFileUsingMediaStore( + fileName: String, + data: ByteArray, + ): File? { try { val resolver = context.contentResolver @@ -191,11 +206,12 @@ class TransferManager( val uniqueFileName = generateUniqueFileName(fileName) // Create content values for the file - val contentValues = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, uniqueFileName) - put(MediaStore.MediaColumns.MIME_TYPE, getMimeType(uniqueFileName)) - put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) - } + val contentValues = + ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, uniqueFileName) + put(MediaStore.MediaColumns.MIME_TYPE, getMimeType(uniqueFileName)) + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } // Insert the file into MediaStore val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) @@ -221,7 +237,10 @@ class TransferManager( } } - private fun saveFileUsingLegacyStorage(fileName: String, data: ByteArray): File? { + private fun saveFileUsingLegacyStorage( + fileName: String, + data: ByteArray, + ): File? { try { val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) @@ -254,7 +273,7 @@ class TransferManager( private fun generateUniqueFileNameForDirectory( directory: File, - originalFileName: String + originalFileName: String, ): String { val nameWithoutExt = originalFileName.substringBeforeLast(".", originalFileName) val extension = originalFileName.substringAfterLast(".", "") @@ -265,11 +284,12 @@ class TransferManager( // Keep incrementing counter until we find a unique filename while (candidateFile.exists() || isFileNameInMediaStore(candidateFileName)) { - candidateFileName = if (extension.isNotEmpty()) { - "${nameWithoutExt}($counter).$extension" - } else { - "${nameWithoutExt}($counter)" - } + candidateFileName = + if (extension.isNotEmpty()) { + "$nameWithoutExt($counter).$extension" + } else { + "$nameWithoutExt($counter)" + } candidateFile = File(directory, candidateFileName) counter++ @@ -277,11 +297,12 @@ class TransferManager( if (counter > 1000) { Log.w(TAG, "Too many duplicate files, using timestamp suffix") val timestamp = System.currentTimeMillis() - candidateFileName = if (extension.isNotEmpty()) { - "${nameWithoutExt}_$timestamp.$extension" - } else { - "${nameWithoutExt}_$timestamp" - } + candidateFileName = + if (extension.isNotEmpty()) { + "${nameWithoutExt}_$timestamp.$extension" + } else { + "${nameWithoutExt}_$timestamp" + } break } } @@ -295,7 +316,9 @@ class TransferManager( val resolver = context.contentResolver val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME) val selection = - "${MediaStore.MediaColumns.DISPLAY_NAME} = ? AND ${MediaStore.MediaColumns.RELATIVE_PATH} = ?" + "${MediaStore.MediaColumns.DISPLAY_NAME} = ? AND ${ + MediaStore.MediaColumns.RELATIVE_PATH + } = ?" val selectionArgs = arrayOf(fileName, "${Environment.DIRECTORY_DOWNLOADS}/") resolver.query( @@ -303,7 +326,7 @@ class TransferManager( projection, selection, selectionArgs, - null + null, )?.use { cursor -> cursor.count > 0 } ?: false @@ -313,7 +336,10 @@ class TransferManager( } } - private fun getFileFromMediaStoreUri(uri: Uri, fileName: String): File { + private fun getFileFromMediaStoreUri( + uri: Uri, + fileName: String, + ): File { // For MediaStore files, we create a reference file object // The actual file is managed by the system val downloadsDir = @@ -407,11 +433,13 @@ class TransferManager( if (cursor.moveToFirst()) { val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (nameIndex >= 0) cursor.getString(nameIndex) else null - } else null + } else { + null + } } } catch (e: Exception) { Log.e(TAG, "Error getting filename for URI: $uri", e) null } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index 4d622e6..312baee 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -21,24 +21,25 @@ import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase import org.koin.dsl.module -val appModule = module { - single { ProfileRepoImpl(get(), get()) } - single { TransferManager(get(), get(), get()) } - single { ResourcesHelperImpl(get()) } - single { Database.build(get()) } - single { TransferHistoryItemRepositoryImpl(get()) } - single { PermissionsHelperImpl(get()) } - single { NetworkStatusImpl(get()) } - single { AvatarHelperImpl(get()) } - single{ ProfileLocalDataSource(get(), get()) } - single{ TransferHistoryItemLocalDataSource(get()) } - factory { - val db: Database = get() - db.transferHistoryDao() +val appModule = + module { + single { ProfileRepoImpl(get(), get()) } + single { TransferManager(get(), get(), get()) } + single { ResourcesHelperImpl(get()) } + single { Database.build(get()) } + single { TransferHistoryItemRepositoryImpl(get()) } + single { PermissionsHelperImpl(get()) } + single { NetworkStatusImpl(get()) } + single { AvatarHelperImpl(get()) } + single { ProfileLocalDataSource(get(), get()) } + single { TransferHistoryItemLocalDataSource(get()) } + factory { + val db: Database = get() + db.transferHistoryDao() + } + factory { SendFilesUseCase(get(), get(), get()) } + factory { ReceiveFilesUseCase(get(), get(), get()) } } - factory { SendFilesUseCase(get(), get(), get()) } - factory { ReceiveFilesUseCase(get(), get(), get()) } -} /*@Module @InstallIn(SingletonComponent::class) @@ -99,4 +100,4 @@ object AppModule { impl: AvatarHelperImpl ): AvatarHelper = impl } -*/ \ No newline at end of file +*/ diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt index 4d2cc38..1d1efc2 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt @@ -1,17 +1,18 @@ package dev.arkbuilders.drop.app.di -import dev.arkbuilders.drop.app.presentation.home.HomeViewModel import dev.arkbuilders.drop.app.presentation.history.HistoryViewModel +import dev.arkbuilders.drop.app.presentation.home.HomeViewModel import dev.arkbuilders.drop.app.presentation.profile.EditProfileViewModel import dev.arkbuilders.drop.app.presentation.receive.ReceiveViewModel import dev.arkbuilders.drop.app.presentation.send.SendViewModel import org.koin.core.module.dsl.viewModel import org.koin.dsl.module -val viewModelsModule = module { - viewModel { HistoryViewModel(get()) } - viewModel { HomeViewModel(get(), get()) } - viewModel { EditProfileViewModel(get(), get()) } - viewModel { ReceiveViewModel(get(), get()) } - viewModel { SendViewModel(get(), get(), get()) } -} \ No newline at end of file +val viewModelsModule = + module { + viewModel { HistoryViewModel(get()) } + viewModel { HomeViewModel(get(), get()) } + viewModel { EditProfileViewModel(get(), get()) } + viewModel { ReceiveViewModel(get(), get()) } + viewModel { SendViewModel(get(), get(), get()) } + } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt index 5a89d67..91ec9f5 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt @@ -1,10 +1,8 @@ package dev.arkbuilders.drop.app.domain -import android.content.Context -import android.net.Uri - interface AvatarHelper { fun uriToBase64(uri: String): String? + fun getDefaultAvatarBase64(avatarId: String): String companion object { @@ -12,4 +10,4 @@ interface AvatarHelper { const val JPEG_QUALITY = 85 // JPEG compression quality const val MAX_FILE_SIZE = 500 * 1024 // 500KB max file size } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt index a9ce310..ec0da00 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt @@ -2,4 +2,4 @@ package dev.arkbuilders.drop.app.domain interface PermissionsHelper { fun isCameraGranted(): Boolean -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt index 47bcd88..69a058c 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt @@ -1,9 +1,9 @@ package dev.arkbuilders.drop.app.domain -import android.net.Uri - interface ResourcesHelper { fun getFileName(uri: String): String? + fun validateUris(uris: List): Pair, Int> + fun getFileSize(uri: String): Long -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt index 9bf0fd4..4c45f41 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt @@ -12,4 +12,4 @@ data class TransferHistoryItem( val peerName: String, val peerAvatar: String?, val fileCount: Int, -) \ No newline at end of file +) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferStatus.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferStatus.kt index 01b8fcc..c2d4762 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferStatus.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferStatus.kt @@ -1,9 +1,12 @@ package dev.arkbuilders.drop.app.domain.model enum class TransferStatus { - COMPLETED, FAILED, CANCELLED + COMPLETED, + FAILED, + CANCELLED, } enum class TransferType { - SENT, RECEIVED -} \ No newline at end of file + SENT, + RECEIVED, +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt index a842449..d131ec2 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserAvatar.kt @@ -2,12 +2,13 @@ package dev.arkbuilders.drop.app.domain.model data class UserAvatar( val base64: String, - val predefinedId: String? + val predefinedId: String?, ) { companion object { - val predefinedIds = listOf( - "avatar_00", "avatar_01", "avatar_02", "avatar_03", - "avatar_04", "avatar_05", "avatar_06", "avatar_07", "avatar_08" - ) + val predefinedIds = + listOf( + "avatar_00", "avatar_01", "avatar_02", "avatar_03", + "avatar_04", "avatar_05", "avatar_06", "avatar_07", "avatar_08", + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt index fbaa45c..f0ed2bb 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/UserProfile.kt @@ -7,4 +7,4 @@ data class UserProfile( companion object { fun empty() = UserProfile("", UserAvatar("", null)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt index 8053cd8..804c89b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt @@ -6,4 +6,4 @@ interface NetworkStatus { fun isOnline() = onlineStatus.value val onlineStatus: StateFlow -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt index a623116..960dddf 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt @@ -6,8 +6,12 @@ import kotlinx.coroutines.flow.StateFlow interface ProfileRepo { val profile: StateFlow + fun getCurrentProfile() = profile.value + fun updateProfile(profile: UserProfile) + fun updateName(name: String) + fun updateAvatar(avatar: UserAvatar) -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt index fb4af6b..f9353d7 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt @@ -13,7 +13,7 @@ interface TransferHistoryItemRepository { peerName: String, peerAvatar: String?, fileCount: Int = 1, - status: TransferStatus = TransferStatus.COMPLETED + status: TransferStatus = TransferStatus.COMPLETED, ) suspend fun addReceivedTransfer( @@ -22,10 +22,10 @@ interface TransferHistoryItemRepository { peerName: String, peerAvatar: String?, fileCount: Int = 1, - status: TransferStatus = TransferStatus.COMPLETED + status: TransferStatus = TransferStatus.COMPLETED, ) suspend fun deleteHistoryItem(itemId: Long) suspend fun clearHistory() -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt index c214f06..036675e 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt @@ -19,33 +19,37 @@ class ReceiveFilesUseCase( ) { suspend operator fun invoke( ticket: String, - confirmation: UByte - ): Result = withContext(Dispatchers.IO) { - runCatching { - Timber.d("Starting file receive with ticket: $ticket") + confirmation: UByte, + ): Result = + withContext(Dispatchers.IO) { + runCatching { + Timber.d("Starting file receive with ticket: $ticket") - val profile = profileRepo.getCurrentProfile() - val receiverProfile = ReceiverProfile( - name = profile.name.ifEmpty { "Anonymous" }, - avatarB64 = profile.avatar.base64.takeIf { it.isNotEmpty() } - ) + val profile = profileRepo.getCurrentProfile() + val receiverProfile = + ReceiverProfile( + name = profile.name.ifEmpty { "Anonymous" }, + avatarB64 = profile.avatar.base64.takeIf { it.isNotEmpty() }, + ) - val request = ReceiveFilesRequest( - ticket = ticket, - confirmation = confirmation, - profile = receiverProfile, - config = ReceiverConfig( - chunkSize = 1024u * 512u, - parallelStreams = 4u, - ) - ) + val request = + ReceiveFilesRequest( + ticket = ticket, + confirmation = confirmation, + profile = receiverProfile, + config = + ReceiverConfig( + chunkSize = 1024u * 512u, + parallelStreams = 4u, + ), + ) - val bubble = receiveFiles(request) + val bubble = receiveFiles(request) - Timber.d("Receive bubble created and started") - bubble - }.onFailure { - Timber.e("Error starting file receive ${it.message}") + Timber.d("Receive bubble created and started") + bubble + }.onFailure { + Timber.e("Error starting file receive ${it.message}") + } } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt index ee1edfc..3cb1638 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt @@ -20,52 +20,59 @@ class SendFilesUseCase( private val profileRepo: ProfileRepo, private val resourcesHelper: ResourcesHelper, ) { - suspend operator fun invoke( - fileUris: List, - ): Result = withContext(Dispatchers.IO) { - runCatching { - Timber.d("Starting file send for ${fileUris.size} files") + suspend operator fun invoke(fileUris: List): Result = + withContext(Dispatchers.IO) { + runCatching { + Timber.d("Starting file send for ${fileUris.size} files") - val profile = profileRepo.getCurrentProfile() - val senderProfile = SenderProfile( - name = profile.name.ifEmpty { "Anonymous" }, - avatarB64 = profile.avatar.base64.takeIf { it.isNotEmpty() } - ) - - val senderFiles = fileUris.mapNotNull { uri -> - val fileName = resourcesHelper.getFileName(uri.toString()) - if (fileName != null) { - val fileData = SenderFileDataImpl(context, uri) - SenderFile( - name = fileName, - data = fileData + val profile = profileRepo.getCurrentProfile() + val senderProfile = + SenderProfile( + name = profile.name.ifEmpty { "Anonymous" }, + avatarB64 = profile.avatar.base64.takeIf { it.isNotEmpty() }, ) - } else { - Timber.w("Could not get filename for URI: $uri") - null - } - } - if (senderFiles.isEmpty()) { - Timber.e("No valid files to send") - error("No valid files to send") - } + val senderFiles = + fileUris.mapNotNull { uri -> + val fileName = resourcesHelper.getFileName(uri.toString()) + if (fileName != null) { + val fileData = SenderFileDataImpl(context, uri) + SenderFile( + name = fileName, + data = fileData, + ) + } else { + Timber.w("Could not get filename for URI: $uri") + null + } + } - val request = SendFilesRequest( - profile = senderProfile, - files = senderFiles, - config = SenderConfig( - chunkSize = 1024u * 512u, - parallelStreams = 4u, - ), - ) + if (senderFiles.isEmpty()) { + Timber.e("No valid files to send") + error("No valid files to send") + } + + val request = + SendFilesRequest( + profile = senderProfile, + files = senderFiles, + config = + SenderConfig( + chunkSize = 1024u * 512u, + parallelStreams = 4u, + ), + ) - val bubble = sendFiles(request) + val bubble = sendFiles(request) - Timber.d("Send bubble created with ticket and confirmation: ${bubble.getTicket()} ${bubble.getConfirmation()}") - bubble - }.onFailure { - Timber.e("Error starting file send ${it.message}") + Timber.d( + "Send bubble created with ticket and confirmation: ${ + bubble.getTicket() + } ${bubble.getConfirmation()}", + ) + bubble + }.onFailure { + Timber.e("Error starting file send ${it.message}") + } } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DisplayUtils.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DisplayUtils.kt index 7f29786..6fef4bf 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DisplayUtils.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DisplayUtils.kt @@ -22,4 +22,4 @@ object DisplayUtils { else -> "${seconds / 3600}h ${(seconds % 3600) / 60}m" } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt index 59c4266..5a8ad48 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt @@ -17,4 +17,4 @@ class DropApplication : Application() { modules(appModule, viewModelsModule) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt index bbf8de2..f92eab7 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt @@ -17,8 +17,8 @@ import androidx.navigation.navDeepLink import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository -import dev.arkbuilders.drop.app.presentation.home.Home import dev.arkbuilders.drop.app.presentation.history.History +import dev.arkbuilders.drop.app.presentation.home.Home import dev.arkbuilders.drop.app.presentation.navigation.DropDestination import dev.arkbuilders.drop.app.presentation.profile.EditProfileEnhanced import dev.arkbuilders.drop.app.presentation.receive.Receive @@ -27,7 +27,6 @@ import dev.arkbuilders.drop.app.presentation.theme.DropTheme import org.koin.android.ext.android.get class MainActivity : ComponentActivity() { - private val transferManager: TransferManager = get() private val profileRepo: ProfileRepo = get() @@ -40,15 +39,17 @@ class MainActivity : ComponentActivity() { setContent { DropTheme { Scaffold( - modifier = Modifier - .fillMaxSize() + modifier = + Modifier + .fillMaxSize(), ) { innerPadding -> DropNavigation( - modifier = Modifier - .padding(innerPadding), + modifier = + Modifier + .padding(innerPadding), transferManager = transferManager, profileRepo = profileRepo, - transferHistoryItemRepository = transferHistoryItemRepository + transferHistoryItemRepository = transferHistoryItemRepository, ) } } @@ -67,37 +68,38 @@ fun DropNavigation( NavHost( navController = navController, startDestination = DropDestination.Home.route, - modifier = modifier + modifier = modifier, ) { composable(DropDestination.Home.route) { Home( navController = navController, profileRepo = profileRepo, - transferHistoryItemRepository = transferHistoryItemRepository + transferHistoryItemRepository = transferHistoryItemRepository, ) } composable(DropDestination.Send.route) { Send( navController = navController, - transferManager = transferManager + transferManager = transferManager, ) } composable( DropDestination.Receive.route, - deepLinks = listOf( - navDeepLink { - uriPattern = DropDestination.Receive.DEEP_LINK_PATTERN - } - ) + deepLinks = + listOf( + navDeepLink { + uriPattern = DropDestination.Receive.DEEP_LINK_PATTERN + }, + ), ) { Receive( - navController = navController + navController = navController, ) } composable(DropDestination.History.route) { History( navController = navController, - transferHistoryItemRepository = transferHistoryItemRepository + transferHistoryItemRepository = transferHistoryItemRepository, ) } composable(DropDestination.EditProfile.route) { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/AvatarImage.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/AvatarImage.kt index 2f22f81..d48820a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/AvatarImage.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/AvatarImage.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.unit.dp fun AvatarImage( avatarB64: String, modifier: Modifier = Modifier, - contentDescription: String? = null + contentDescription: String? = null, ) { val imageBytes = Base64.decode(avatarB64, Base64.DEFAULT) val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) @@ -39,12 +39,13 @@ fun AvatarImage( Image( painter = BitmapPainter(bitmap.asImageBitmap()), contentDescription = contentDescription, - modifier = modifier - .clip(CircleShape) - .semantics { - this.contentDescription = contentDescription ?: "Profile avatar" - }, - contentScale = ContentScale.Crop + modifier = + modifier + .clip(CircleShape) + .semantics { + this.contentDescription = contentDescription ?: "Profile avatar" + }, + contentScale = ContentScale.Crop, ) } else { AvatarFallback(modifier = modifier, contentDescription = contentDescription) @@ -56,19 +57,19 @@ fun AvatarImageWithFallback( avatarB64: String?, fallbackText: String = "", size: Dp = 48.dp, - contentDescription: String? = null + contentDescription: String? = null, ) { if (avatarB64 != null && avatarB64.isNotEmpty()) { AvatarImage( avatarB64 = avatarB64, modifier = Modifier.size(size), - contentDescription = contentDescription + contentDescription = contentDescription, ) } else { AvatarFallback( modifier = Modifier.size(size), fallbackText = fallbackText, - contentDescription = contentDescription + contentDescription = contentDescription, ) } } @@ -77,31 +78,32 @@ fun AvatarImageWithFallback( private fun AvatarFallback( modifier: Modifier = Modifier, fallbackText: String = "", - contentDescription: String? = null + contentDescription: String? = null, ) { Box( - modifier = modifier - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer) - .semantics { - this.contentDescription = contentDescription ?: "Default profile avatar" - }, - contentAlignment = Alignment.Center + modifier = + modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer) + .semantics { + this.contentDescription = contentDescription ?: "Default profile avatar" + }, + contentAlignment = Alignment.Center, ) { if (fallbackText.isNotEmpty()) { Text( text = fallbackText.take(2).uppercase(), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onPrimaryContainer + color = MaterialTheme.colorScheme.onPrimaryContainer, ) } else { Icon( Icons.Default.Person, contentDescription = null, modifier = Modifier.fillMaxSize(0.6f), - tint = MaterialTheme.colorScheme.onPrimaryContainer + tint = MaterialTheme.colorScheme.onPrimaryContainer, ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropButton.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropButton.kt index ec286dd..3b21539 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropButton.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropButton.kt @@ -28,11 +28,16 @@ import androidx.compose.ui.unit.dp import dev.arkbuilders.drop.app.presentation.theme.DesignTokens enum class DropButtonSize { - Small, Medium, Large + Small, + Medium, + Large, } enum class DropButtonVariant { - Primary, Secondary, Tertiary, Destructive + Primary, + Secondary, + Tertiary, + Destructive, } @Composable @@ -45,55 +50,80 @@ fun DropButton( loading: Boolean = false, contentDescription: String? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - content: @Composable RowScope.() -> Unit + content: @Composable RowScope.() -> Unit, ) { val haptic = LocalHapticFeedback.current val isPressed by interactionSource.collectIsPressedAsState() - + val scale by animateFloatAsState( targetValue = if (isPressed) 0.95f else 1f, - label = "buttonScale" + label = "buttonScale", ) - - val buttonHeight = when (size) { - DropButtonSize.Small -> 40.dp - DropButtonSize.Medium -> DesignTokens.TouchTarget.minimum - DropButtonSize.Large -> DesignTokens.TouchTarget.large - } - - val contentPadding = when (size) { - DropButtonSize.Small -> PaddingValues(horizontal = DesignTokens.Spacing.md, vertical = DesignTokens.Spacing.xs) - DropButtonSize.Medium -> PaddingValues(horizontal = DesignTokens.Spacing.lg, vertical = DesignTokens.Spacing.sm) - DropButtonSize.Large -> PaddingValues(horizontal = DesignTokens.Spacing.xl, vertical = DesignTokens.Spacing.md) - } - - val colors = when (variant) { - DropButtonVariant.Primary -> ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), - disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) - DropButtonVariant.Secondary -> ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary, - disabledContainerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f), - disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) - DropButtonVariant.Tertiary -> ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.tertiary, - contentColor = MaterialTheme.colorScheme.onTertiary, - disabledContainerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.12f), - disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) - DropButtonVariant.Destructive -> ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError, - disabledContainerColor = MaterialTheme.colorScheme.error.copy(alpha = 0.12f), - disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) - } - + + val buttonHeight = + when (size) { + DropButtonSize.Small -> 40.dp + DropButtonSize.Medium -> DesignTokens.TouchTarget.minimum + DropButtonSize.Large -> DesignTokens.TouchTarget.large + } + + val contentPadding = + when (size) { + DropButtonSize.Small -> + PaddingValues( + horizontal = DesignTokens.Spacing.md, + vertical = DesignTokens.Spacing.xs, + ) + DropButtonSize.Medium -> + PaddingValues( + horizontal = DesignTokens.Spacing.lg, + vertical = DesignTokens.Spacing.sm, + ) + DropButtonSize.Large -> + PaddingValues( + horizontal = DesignTokens.Spacing.xl, + vertical = DesignTokens.Spacing.md, + ) + } + + val colors = + when (variant) { + DropButtonVariant.Primary -> + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), + disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + ) + DropButtonVariant.Secondary -> + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + disabledContainerColor = + MaterialTheme.colorScheme.secondary.copy( + alpha = 0.12f, + ), + disabledContentColor = + MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.38f, + ), + ) + DropButtonVariant.Tertiary -> + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiary, + contentColor = MaterialTheme.colorScheme.onTertiary, + disabledContainerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.12f), + disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + ) + DropButtonVariant.Destructive -> + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + disabledContainerColor = MaterialTheme.colorScheme.error.copy(alpha = 0.12f), + disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + ) + } + Button( onClick = { if (!loading) { @@ -101,24 +131,25 @@ fun DropButton( onClick() } }, - modifier = modifier - .scale(scale) - .defaultMinSize(minHeight = buttonHeight) - .semantics { - role = Role.Button - contentDescription?.let { this.contentDescription = it } - }, + modifier = + modifier + .scale(scale) + .defaultMinSize(minHeight = buttonHeight) + .semantics { + role = Role.Button + contentDescription?.let { this.contentDescription = it } + }, enabled = enabled && !loading, colors = colors, contentPadding = contentPadding, interactionSource = interactionSource, - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + shape = RoundedCornerShape(DesignTokens.CornerRadius.md), ) { if (loading) { CircularProgressIndicator( modifier = Modifier.size(16.dp), color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp + strokeWidth = 2.dp, ) } else { content() @@ -135,28 +166,42 @@ fun DropOutlinedButton( loading: Boolean = false, contentDescription: String? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - content: @Composable RowScope.() -> Unit + content: @Composable RowScope.() -> Unit, ) { val haptic = LocalHapticFeedback.current val isPressed by interactionSource.collectIsPressedAsState() - + val scale by animateFloatAsState( targetValue = if (isPressed) 0.95f else 1f, - label = "buttonScale" + label = "buttonScale", ) - - val buttonHeight = when (size) { - DropButtonSize.Small -> 40.dp - DropButtonSize.Medium -> DesignTokens.TouchTarget.minimum - DropButtonSize.Large -> DesignTokens.TouchTarget.large - } - - val contentPadding = when (size) { - DropButtonSize.Small -> PaddingValues(horizontal = DesignTokens.Spacing.md, vertical = DesignTokens.Spacing.xs) - DropButtonSize.Medium -> PaddingValues(horizontal = DesignTokens.Spacing.lg, vertical = DesignTokens.Spacing.sm) - DropButtonSize.Large -> PaddingValues(horizontal = DesignTokens.Spacing.xl, vertical = DesignTokens.Spacing.md) - } - + + val buttonHeight = + when (size) { + DropButtonSize.Small -> 40.dp + DropButtonSize.Medium -> DesignTokens.TouchTarget.minimum + DropButtonSize.Large -> DesignTokens.TouchTarget.large + } + + val contentPadding = + when (size) { + DropButtonSize.Small -> + PaddingValues( + horizontal = DesignTokens.Spacing.md, + vertical = DesignTokens.Spacing.xs, + ) + DropButtonSize.Medium -> + PaddingValues( + horizontal = DesignTokens.Spacing.lg, + vertical = DesignTokens.Spacing.sm, + ) + DropButtonSize.Large -> + PaddingValues( + horizontal = DesignTokens.Spacing.xl, + vertical = DesignTokens.Spacing.md, + ) + } + OutlinedButton( onClick = { if (!loading) { @@ -164,23 +209,24 @@ fun DropOutlinedButton( onClick() } }, - modifier = modifier - .scale(scale) - .defaultMinSize(minHeight = buttonHeight) - .semantics { - role = Role.Button - contentDescription?.let { this.contentDescription = it } - }, + modifier = + modifier + .scale(scale) + .defaultMinSize(minHeight = buttonHeight) + .semantics { + role = Role.Button + contentDescription?.let { this.contentDescription = it } + }, enabled = enabled && !loading, contentPadding = contentPadding, interactionSource = interactionSource, - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + shape = RoundedCornerShape(DesignTokens.CornerRadius.md), ) { if (loading) { CircularProgressIndicator( modifier = Modifier.size(16.dp), color = MaterialTheme.colorScheme.primary, - strokeWidth = 2.dp + strokeWidth = 2.dp, ) } else { content() diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropCard.kt index 44708a3..e5fbd12 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropCard.kt @@ -19,11 +19,15 @@ import androidx.compose.ui.semantics.semantics import dev.arkbuilders.drop.app.presentation.theme.DesignTokens enum class DropCardVariant { - Filled, Elevated, Outlined + Filled, + Elevated, + Outlined, } enum class DropCardSize { - Small, Medium, Large + Small, + Medium, + Large, } @Composable @@ -33,47 +37,64 @@ fun DropCard( size: DropCardSize = DropCardSize.Medium, onClick: (() -> Unit)? = null, contentDescription: String? = null, - shape: Shape = RoundedCornerShape( + shape: Shape = + RoundedCornerShape( + when (size) { + DropCardSize.Small -> DesignTokens.CornerRadius.sm + DropCardSize.Medium -> DesignTokens.CornerRadius.md + DropCardSize.Large -> DesignTokens.CornerRadius.lg + }, + ), + colors: CardColors = + when (variant) { + DropCardVariant.Filled -> + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ) + DropCardVariant.Elevated -> + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ) + DropCardVariant.Outlined -> + CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ) + }, + content: @Composable ColumnScope.() -> Unit, +) { + val cardModifier = + modifier + .fillMaxWidth() + .semantics { + contentDescription?.let { this.contentDescription = it } + } + + val cardPadding = when (size) { - DropCardSize.Small -> DesignTokens.CornerRadius.sm - DropCardSize.Medium -> DesignTokens.CornerRadius.md - DropCardSize.Large -> DesignTokens.CornerRadius.lg + DropCardSize.Small -> DesignTokens.Spacing.md + DropCardSize.Medium -> DesignTokens.Spacing.lg + DropCardSize.Large -> DesignTokens.Spacing.xl } - ), - colors: CardColors = when (variant) { - DropCardVariant.Filled -> CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ) - DropCardVariant.Elevated -> CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ) - DropCardVariant.Outlined -> CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ) - }, - content: @Composable ColumnScope.() -> Unit -) { - val cardModifier = modifier - .fillMaxWidth() - .semantics { - contentDescription?.let { this.contentDescription = it } + + val elevation = + when (variant) { + DropCardVariant.Filled -> + CardDefaults.cardElevation( + defaultElevation = DesignTokens.Elevation.none, + ) + DropCardVariant.Elevated -> + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.md, + ) + DropCardVariant.Outlined -> + CardDefaults.outlinedCardElevation( + defaultElevation = DesignTokens.Elevation.none, + ) } - - val cardPadding = when (size) { - DropCardSize.Small -> DesignTokens.Spacing.md - DropCardSize.Medium -> DesignTokens.Spacing.lg - DropCardSize.Large -> DesignTokens.Spacing.xl - } - - val elevation = when (variant) { - DropCardVariant.Filled -> CardDefaults.cardElevation(defaultElevation = DesignTokens.Elevation.none) - DropCardVariant.Elevated -> CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.md) - DropCardVariant.Outlined -> CardDefaults.outlinedCardElevation(defaultElevation = DesignTokens.Elevation.none) - } - + when (variant) { DropCardVariant.Filled -> { Card( @@ -81,7 +102,7 @@ fun DropCard( onClick = onClick ?: { }, shape = shape, colors = colors, - elevation = elevation + elevation = elevation, ) { content() } @@ -92,7 +113,7 @@ fun DropCard( onClick = onClick ?: { }, shape = shape, colors = colors, - elevation = elevation + elevation = elevation, ) { content() } @@ -103,7 +124,7 @@ fun DropCard( onClick = onClick ?: { }, shape = shape, colors = colors, - elevation = elevation + elevation = elevation, ) { content() } @@ -115,16 +136,17 @@ fun DropCard( fun DropCardContent( modifier: Modifier = Modifier, size: DropCardSize = DropCardSize.Medium, - content: @Composable ColumnScope.() -> Unit + content: @Composable ColumnScope.() -> Unit, ) { - val padding = when (size) { - DropCardSize.Small -> DesignTokens.Spacing.md - DropCardSize.Medium -> DesignTokens.Spacing.lg - DropCardSize.Large -> DesignTokens.Spacing.xl - } + val padding = + when (size) { + DropCardSize.Small -> DesignTokens.Spacing.md + DropCardSize.Medium -> DesignTokens.Spacing.lg + DropCardSize.Large -> DesignTokens.Spacing.xl + } Column( - modifier = modifier.padding(padding) + modifier = modifier.padding(padding), ) { content() } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/ErrorStates.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/ErrorStates.kt index a512de5..9126023 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/ErrorStates.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/ErrorStates.kt @@ -25,7 +25,11 @@ import compose.icons.tablericons.WifiOff import dev.arkbuilders.drop.app.presentation.theme.DesignTokens enum class ErrorType { - Network, FileTransfer, Permission, Generic, Offline + Network, + FileTransfer, + Permission, + Generic, + Offline, } data class ErrorState( @@ -33,77 +37,80 @@ data class ErrorState( val title: String, val message: String, val actionLabel: String? = null, - val onAction: (() -> Unit)? = null + val onAction: (() -> Unit)? = null, ) @Composable fun ErrorStateDisplay( errorState: ErrorState, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val icon = when (errorState.type) { - ErrorType.Network -> TablerIcons.WifiOff - ErrorType.FileTransfer -> TablerIcons.FileX - ErrorType.Permission -> TablerIcons.AlertCircle - ErrorType.Offline -> TablerIcons.CloudOff - ErrorType.Generic -> Icons.Default.Warning - } - - val iconColor = when (errorState.type) { - ErrorType.Network, ErrorType.Offline -> MaterialTheme.colorScheme.error - ErrorType.FileTransfer -> MaterialTheme.colorScheme.error - ErrorType.Permission -> MaterialTheme.colorScheme.error - ErrorType.Generic -> MaterialTheme.colorScheme.error - } - + val icon = + when (errorState.type) { + ErrorType.Network -> TablerIcons.WifiOff + ErrorType.FileTransfer -> TablerIcons.FileX + ErrorType.Permission -> TablerIcons.AlertCircle + ErrorType.Offline -> TablerIcons.CloudOff + ErrorType.Generic -> Icons.Default.Warning + } + + val iconColor = + when (errorState.type) { + ErrorType.Network, ErrorType.Offline -> MaterialTheme.colorScheme.error + ErrorType.FileTransfer -> MaterialTheme.colorScheme.error + ErrorType.Permission -> MaterialTheme.colorScheme.error + ErrorType.Generic -> MaterialTheme.colorScheme.error + } + DropCard( modifier = modifier, variant = DropCardVariant.Outlined, size = DropCardSize.Large, - colors = CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.1f), - contentColor = MaterialTheme.colorScheme.onErrorContainer - ) + colors = + CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.1f), + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ), ) { DropCardContent(size = DropCardSize.Large) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { Icon( imageVector = icon, contentDescription = null, modifier = Modifier.size(48.dp), - tint = iconColor + tint = iconColor, ) - + Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) - + Text( text = errorState.title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) - + Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) - + Text( text = errorState.message, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) - + errorState.actionLabel?.let { label -> Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) - + DropButton( onClick = { errorState.onAction?.invoke() }, variant = DropButtonVariant.Primary, size = DropButtonSize.Medium, - contentDescription = "Retry action" + contentDescription = "Retry action", ) { Text(text = label) } @@ -115,43 +122,58 @@ fun ErrorStateDisplay( // Predefined error states for common scenarios object CommonErrors { - fun networkError(onRetry: () -> Unit) = ErrorState( - type = ErrorType.Network, - title = "Connection Problem", - message = "Unable to connect to the network. Please check your internet connection and try again.", - actionLabel = "Retry", - onAction = onRetry - ) - - fun fileTransferError(onRetry: () -> Unit) = ErrorState( - type = ErrorType.FileTransfer, - title = "Transfer Failed", - message = "The file transfer was interrupted. This might be due to network issues or insufficient storage space.", - actionLabel = "Try Again", - onAction = onRetry - ) - - fun permissionError(onRequestPermission: () -> Unit) = ErrorState( - type = ErrorType.Permission, - title = "Permission Required", - message = "This feature requires additional permissions to work properly. Please grant the necessary permissions.", - actionLabel = "Grant Permission", - onAction = onRequestPermission - ) - - fun offlineError() = ErrorState( - type = ErrorType.Offline, - title = "You're Offline", - message = "This feature requires an internet connection. Please check your network settings and try again.", - actionLabel = null, - onAction = null - ) - - fun genericError(onRetry: () -> Unit) = ErrorState( - type = ErrorType.Generic, - title = "Something Went Wrong", - message = "An unexpected error occurred. Please try again or contact support if the problem persists.", - actionLabel = "Retry", - onAction = onRetry - ) + fun networkError(onRetry: () -> Unit) = + ErrorState( + type = ErrorType.Network, + title = "Connection Problem", + message = + "Unable to connect to the network." + + " Please check your internet connection and try again.", + actionLabel = "Retry", + onAction = onRetry, + ) + + fun fileTransferError(onRetry: () -> Unit) = + ErrorState( + type = ErrorType.FileTransfer, + title = "Transfer Failed", + message = + "The file transfer was interrupted." + + " This might be due to network issues or insufficient storage space.", + actionLabel = "Try Again", + onAction = onRetry, + ) + + fun permissionError(onRequestPermission: () -> Unit) = + ErrorState( + type = ErrorType.Permission, + title = "Permission Required", + message = + "This feature requires additional permissions to work properly." + + " Please grant the necessary permissions.", + actionLabel = "Grant Permission", + onAction = onRequestPermission, + ) + + fun offlineError() = + ErrorState( + type = ErrorType.Offline, + title = "You're Offline", + message = + "This feature requires an internet connection." + + " Please check your network settings and try again.", + actionLabel = null, + onAction = null, + ) + + fun genericError(onRetry: () -> Unit) = + ErrorState( + type = ErrorType.Generic, + title = "Something Went Wrong", + message = + "An unexpected error occurred." + + " Please try again or contact support if the problem persists.", + actionLabel = "Retry", + onAction = onRetry, + ) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/LoadingStates.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/LoadingStates.kt index 9c72743..035c750 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/LoadingStates.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/LoadingStates.kt @@ -38,38 +38,36 @@ import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun LoadingIndicator( modifier: Modifier = Modifier, - message: String? = null + message: String? = null, ) { Column( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { CircularProgressIndicator( modifier = Modifier.size(48.dp), color = MaterialTheme.colorScheme.primary, - strokeWidth = 4.dp + strokeWidth = 4.dp, ) - + message?.let { Spacer(modifier = Modifier.height(DesignTokens.Spacing.lg)) Text( text = it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } } @Composable -fun SkeletonLoader( - modifier: Modifier = Modifier -) { +fun SkeletonLoader(modifier: Modifier = Modifier) { Column( modifier = modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md), ) { repeat(3) { SkeletonCard() @@ -81,41 +79,44 @@ fun SkeletonLoader( private fun SkeletonCard() { DropCard( variant = DropCardVariant.Elevated, - size = DropCardSize.Medium + size = DropCardSize.Medium, ) { DropCardContent(size = DropCardSize.Medium) { Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { // Avatar skeleton Box( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .shimmerEffect() + modifier = + Modifier + .size(48.dp) + .clip(CircleShape) + .shimmerEffect(), ) - + Spacer(modifier = Modifier.width(DesignTokens.Spacing.lg)) - + Column(modifier = Modifier.weight(1f)) { // Title skeleton Box( - modifier = Modifier - .fillMaxWidth(0.7f) - .height(16.dp) - .clip(RoundedCornerShape(DesignTokens.CornerRadius.xs)) - .shimmerEffect() + modifier = + Modifier + .fillMaxWidth(0.7f) + .height(16.dp) + .clip(RoundedCornerShape(DesignTokens.CornerRadius.xs)) + .shimmerEffect(), ) - + Spacer(modifier = Modifier.height(DesignTokens.Spacing.xs)) - + // Subtitle skeleton Box( - modifier = Modifier - .fillMaxWidth(0.5f) - .height(12.dp) - .clip(RoundedCornerShape(DesignTokens.CornerRadius.xs)) - .shimmerEffect() + modifier = + Modifier + .fillMaxWidth(0.5f) + .height(12.dp) + .clip(RoundedCornerShape(DesignTokens.CornerRadius.xs)) + .shimmerEffect(), ) } } @@ -123,59 +124,63 @@ private fun SkeletonCard() { } } -fun Modifier.shimmerEffect(): Modifier = composed { - val transition = rememberInfiniteTransition(label = "shimmer") - val alpha by transition.animateFloat( - initialValue = 0.2f, - targetValue = 0.9f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1000, easing = LinearEasing), - repeatMode = RepeatMode.Reverse - ), - label = "shimmerAlpha" - ) - - background( - brush = Brush.linearGradient( - colors = listOf( - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha), - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha * 0.5f) - ), - start = Offset.Zero, - end = Offset.Infinite +fun Modifier.shimmerEffect(): Modifier = + composed { + val transition = rememberInfiniteTransition(label = "shimmer") + val alpha by transition.animateFloat( + initialValue = 0.2f, + targetValue = 0.9f, + animationSpec = + infiniteRepeatable( + animation = tween(durationMillis = 1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "shimmerAlpha", ) - ) -} + + background( + brush = + Brush.linearGradient( + colors = + listOf( + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha), + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha * 0.5f), + ), + start = Offset.Zero, + end = Offset.Infinite, + ), + ) + } @Composable fun EmptyState( title: String, description: String, modifier: Modifier = Modifier, - action: (@Composable () -> Unit)? = null + action: (@Composable () -> Unit)? = null, ) { Column( modifier = modifier.padding(DesignTokens.Spacing.xl), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { Text( text = title, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) - + Spacer(modifier = Modifier.height(DesignTokens.Spacing.sm)) - + Text( text = description, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) - + action?.let { Spacer(modifier = Modifier.height(DesignTokens.Spacing.xl)) it() diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt index 4d444a3..5239f9a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt @@ -59,7 +59,7 @@ import java.util.Locale @Composable fun History( navController: NavController, - transferHistoryItemRepository: TransferHistoryItemRepository + transferHistoryItemRepository: TransferHistoryItemRepository, ) { val viewModel: HistoryViewModel = koinInject() @@ -67,14 +67,15 @@ fun History( val state by viewModel.collectAsState() Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) + modifier = + Modifier + .fillMaxSize() + .padding(16.dp), ) { // Top bar Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { IconButton(onClick = { navController.navigateUp() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") @@ -83,7 +84,7 @@ fun History( text = "Transfer History", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) if (state.historyItems.isNotEmpty()) { @@ -91,7 +92,7 @@ fun History( Icon( TablerIcons.ClearAll, contentDescription = "Clear All", - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -104,39 +105,42 @@ fun History( Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(20.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ) + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), ) { Column( modifier = Modifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( TablerIcons.History, contentDescription = null, modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.height(20.dp)) Text( text = "No Transfer History", style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) Spacer(modifier = Modifier.height(12.dp)) Text( - text = "Your sent and received files will appear here with details about each transfer.", + text = + "Your sent and received files will appear" + + " here with details about each transfer.", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3, ) } } } else { // History list LazyColumn( - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.spacedBy(12.dp), ) { items(state.historyItems) { item -> HistoryItemCard( @@ -146,7 +150,7 @@ fun History( onDismissDeleteDialog = viewModel::onDismissDeleteDialog, onDelete = { viewModel.onDelete(item.id) - } + }, ) } } @@ -161,13 +165,14 @@ fun History( Text( "Clear All History", style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) }, text = { Text( - "Are you sure you want to clear all transfer history? This action cannot be undone.", - style = MaterialTheme.typography.bodyLarge + "Are you sure you want to clear all transfer history?" + + " This action cannot be undone.", + style = MaterialTheme.typography.bodyLarge, ) }, confirmButton = { @@ -175,9 +180,10 @@ fun History( onClick = { viewModel.onClear() }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + ), ) { Text("Clear All", fontWeight = FontWeight.Medium) } @@ -187,7 +193,7 @@ fun History( Text("Cancel", fontWeight = FontWeight.Medium) } }, - shape = RoundedCornerShape(16.dp) + shape = RoundedCornerShape(16.dp), ) } } @@ -198,27 +204,29 @@ private fun HistoryItemCard( item: TransferHistoryItem, onShowDeleteDialog: () -> Unit, onDismissDeleteDialog: () -> Unit, - onDelete: () -> Unit + onDelete: () -> Unit, ) { ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 4.dp) + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = 4.dp), ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp), - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier + .fillMaxWidth() + .padding(20.dp), + verticalAlignment = Alignment.CenterVertically, ) { // Peer avatar AvatarImageWithFallback( avatarB64 = item.peerAvatar, fallbackText = item.peerName, - size = 48.dp + size = 48.dp, ) Spacer(modifier = Modifier.width(16.dp)) @@ -227,31 +235,35 @@ private fun HistoryItemCard( Column(modifier = Modifier.weight(1f)) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { // Transfer type icon Icon( - imageVector = when (item.type) { - TransferType.SENT -> TablerIcons.FileUpload - TransferType.RECEIVED -> TablerIcons.FileDownload - }, + imageVector = + when (item.type) { + TransferType.SENT -> TablerIcons.FileUpload + TransferType.RECEIVED -> TablerIcons.FileDownload + }, contentDescription = null, modifier = Modifier.size(20.dp), - tint = when (item.status) { - TransferStatus.COMPLETED -> MaterialTheme.colorScheme.primary - TransferStatus.FAILED -> MaterialTheme.colorScheme.error - TransferStatus.CANCELLED -> MaterialTheme.colorScheme.onSurfaceVariant - } + tint = + when (item.status) { + TransferStatus.COMPLETED -> MaterialTheme.colorScheme.primary + TransferStatus.FAILED -> MaterialTheme.colorScheme.error + TransferStatus.CANCELLED -> + MaterialTheme.colorScheme.onSurfaceVariant + }, ) Text( - text = when (item.type) { - TransferType.SENT -> "Sent to ${item.peerName}" - TransferType.RECEIVED -> "Received from ${item.peerName}" - }, + text = + when (item.type) { + TransferType.SENT -> "Sent to ${item.peerName}" + TransferType.RECEIVED -> "Received from ${item.peerName}" + }, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) } @@ -263,45 +275,45 @@ private fun HistoryItemCard( fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) Spacer(modifier = Modifier.height(8.dp)) Row( horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = formatFileSize(item.fileSize), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( text = "•", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( text = formatTimestamp(item.timestamp), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) if (item.fileCount > 1) { Text( text = "•", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( text = "${item.fileCount} files", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } } @@ -310,18 +322,20 @@ private fun HistoryItemCard( // Status Text( - text = when (item.status) { - TransferStatus.COMPLETED -> "Completed" - TransferStatus.FAILED -> "Failed" - TransferStatus.CANCELLED -> "Cancelled" - }, + text = + when (item.status) { + TransferStatus.COMPLETED -> "Completed" + TransferStatus.FAILED -> "Failed" + TransferStatus.CANCELLED -> "Cancelled" + }, style = MaterialTheme.typography.bodySmall, - color = when (item.status) { - TransferStatus.COMPLETED -> MaterialTheme.colorScheme.primary - TransferStatus.FAILED -> MaterialTheme.colorScheme.error - TransferStatus.CANCELLED -> MaterialTheme.colorScheme.onSurfaceVariant - }, - fontWeight = FontWeight.Medium + color = + when (item.status) { + TransferStatus.COMPLETED -> MaterialTheme.colorScheme.primary + TransferStatus.FAILED -> MaterialTheme.colorScheme.error + TransferStatus.CANCELLED -> MaterialTheme.colorScheme.onSurfaceVariant + }, + fontWeight = FontWeight.Medium, ) } @@ -330,7 +344,7 @@ private fun HistoryItemCard( Icon( Icons.Default.Delete, contentDescription = "Delete", - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -344,13 +358,13 @@ private fun HistoryItemCard( Text( "Delete History Item", style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) }, text = { Text( "Are you sure you want to delete this transfer from history?", - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyLarge, ) }, confirmButton = { @@ -358,9 +372,10 @@ private fun HistoryItemCard( onClick = { onDelete() }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + ), ) { Text("Delete", fontWeight = FontWeight.Medium) } @@ -370,7 +385,7 @@ private fun HistoryItemCard( Text("Cancel", fontWeight = FontWeight.Medium) } }, - shape = RoundedCornerShape(16.dp) + shape = RoundedCornerShape(16.dp), ) } } @@ -394,8 +409,9 @@ private fun formatTimestamp(timestamp: OffsetDateTime): String { diff < 3600000 -> "${diff / 60000}m ago" diff < 86400000 -> "${diff / 3600000}h ago" diff < 604800000 -> "${diff / 86400000}d ago" - else -> timestamp.format( - DateTimeFormatter.ofPattern("MMM dd", Locale.getDefault()) - ) + else -> + timestamp.format( + DateTimeFormatter.ofPattern("MMM dd", Locale.getDefault()), + ) } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt index d441959..bb20a12 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt @@ -21,13 +21,14 @@ sealed class HistoryScreenEffect class HistoryViewModel( private val historyItemRepository: TransferHistoryItemRepository, ) : ViewModel(), ContainerHost { - override val container: Container = container( - HistoryScreenState( - historyItems = emptyList(), - showClearDialog = false, - showDeleteDialog = false + override val container: Container = + container( + HistoryScreenState( + historyItems = emptyList(), + showClearDialog = false, + showDeleteDialog = false, + ), ) - ) init { historyItemRepository.historyItems.onEach { items -> @@ -39,41 +40,47 @@ class HistoryViewModel( }.launchIn(viewModelScope) } - fun onShowClearDialog() = intent { - reduce { - state.copy(showClearDialog = true) + fun onShowClearDialog() = + intent { + reduce { + state.copy(showClearDialog = true) + } } - } - fun onClear() = intent { - historyItemRepository.clearHistory() - reduce { - state.copy(showClearDialog = false) + fun onClear() = + intent { + historyItemRepository.clearHistory() + reduce { + state.copy(showClearDialog = false) + } } - } - fun onDismissClearDialog() = intent { - reduce { - state.copy(showClearDialog = false) + fun onDismissClearDialog() = + intent { + reduce { + state.copy(showClearDialog = false) + } } - } - fun onShowDeleteDialog() = intent { - reduce { - state.copy(showDeleteDialog = true) + fun onShowDeleteDialog() = + intent { + reduce { + state.copy(showDeleteDialog = true) + } } - } - fun onDelete(id: Long) = intent { - historyItemRepository.deleteHistoryItem(id) - reduce { - state.copy(showDeleteDialog = false) + fun onDelete(id: Long) = + intent { + historyItemRepository.deleteHistoryItem(id) + reduce { + state.copy(showDeleteDialog = false) + } } - } - fun onDismissDeleteDialog() = intent { - reduce { - state.copy(showDeleteDialog = false) + fun onDismissDeleteDialog() = + intent { + reduce { + state.copy(showDeleteDialog = false) + } } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt index 0046bc6..8bb7512 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt @@ -89,25 +89,27 @@ fun Home( val animatedLogoScale by animateFloatAsState( targetValue = logoScale, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow - ), - label = "logoScale" + animationSpec = + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow, + ), + label = "logoScale", ) LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(DesignTokens.Spacing.lg), - verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.xl) + modifier = + Modifier + .fillMaxSize() + .padding(DesignTokens.Spacing.lg), + verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.xl), ) { // Header Section item { HeaderSection( logoScale = animatedLogoScale, onProfileClick = { navController.navigate(DropDestination.EditProfile.route) }, - profile = state.profile + profile = state.profile, ) } @@ -115,7 +117,7 @@ fun Home( item { QuickActionsSection( onSendClick = { navController.navigate(DropDestination.Send.route) }, - onReceiveClick = { navController.navigate(DropDestination.Receive.route) } + onReceiveClick = { navController.navigate(DropDestination.Receive.route) }, ) } @@ -125,7 +127,7 @@ fun Home( RecentTransfersSection( historyItems = state.historyItems.take(5), onViewAllClick = { navController.navigate(DropDestination.History.route) }, - showViewAll = state.historyItems.isNotEmpty() + showViewAll = state.historyItems.isNotEmpty(), ) } else { EmptyTransfersSection() @@ -138,22 +140,23 @@ fun Home( private fun HeaderSection( logoScale: Float, onProfileClick: () -> Unit, - profile: UserProfile + profile: UserProfile, ) { Row( - modifier = Modifier - .fillMaxWidth() - .semantics { contentDescription = "App header with logo and profile access" }, + modifier = + Modifier + .fillMaxWidth() + .semantics { contentDescription = "App header with logo and profile access" }, horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { // App branding Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.lg) + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.lg), ) { Box( - modifier = Modifier.scale(logoScale) + modifier = Modifier.scale(logoScale), ) { Icon( modifier = Modifier.size(56.dp), @@ -168,21 +171,22 @@ private fun HeaderSection( text = "Drop", style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, ) Text( text = "Share files instantly", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } IconButton( onClick = onProfileClick, - modifier = Modifier.semantics { - contentDescription = "Open profile settings" - } + modifier = + Modifier.semantics { + contentDescription = "Open profile settings" + }, ) { AvatarImage(avatarB64 = profile.avatar.base64) } @@ -192,12 +196,12 @@ private fun HeaderSection( @Composable private fun QuickActionsSection( onSendClick: () -> Unit, - onReceiveClick: () -> Unit + onReceiveClick: () -> Unit, ) { DropCard( variant = DropCardVariant.Elevated, size = DropCardSize.Large, - contentDescription = "Quick actions for sending and receiving files" + contentDescription = "Quick actions for sending and receiving files", ) { DropCardContent(size = DropCardSize.Large) { Text( @@ -206,7 +210,7 @@ private fun QuickActionsSection( fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) @@ -217,18 +221,18 @@ private fun QuickActionsSection( variant = DropButtonVariant.Primary, size = DropButtonSize.Large, modifier = Modifier.fillMaxWidth(), - contentDescription = "Send files to another device" + contentDescription = "Send files to another device", ) { Icon( TablerIcons.ArrowUpCircle, contentDescription = null, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(24.dp), ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) Text( "Send Files", style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, ) } @@ -239,18 +243,18 @@ private fun QuickActionsSection( onClick = onReceiveClick, size = DropButtonSize.Large, modifier = Modifier.fillMaxWidth(), - contentDescription = "Receive files from another device" + contentDescription = "Receive files from another device", ) { Icon( TablerIcons.ArrowDownCircle, contentDescription = null, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(24.dp), ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) Text( "Receive Files", style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, ) } } @@ -261,31 +265,31 @@ private fun QuickActionsSection( private fun RecentTransfersSection( historyItems: List, onViewAllClick: () -> Unit, - showViewAll: Boolean + showViewAll: Boolean, ) { Column { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = "Recent Transfers", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) if (showViewAll) { DropOutlinedButton( onClick = onViewAllClick, size = DropButtonSize.Small, - contentDescription = "View all transfer history" + contentDescription = "View all transfer history", ) { Icon( TablerIcons.History, contentDescription = null, - modifier = Modifier.size(16.dp) + modifier = Modifier.size(16.dp), ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.xs)) Text("View All") @@ -296,7 +300,7 @@ private fun RecentTransfersSection( Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Column( - verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md), ) { historyItems.forEach { item -> EnhancedTransferHistoryCard(item = item) @@ -310,11 +314,13 @@ private fun EmptyTransfersSection() { DropCard( variant = DropCardVariant.Outlined, size = DropCardSize.Large, - contentDescription = "No transfers yet - empty state" + contentDescription = "No transfers yet - empty state", ) { EmptyState( title = "No transfers yet", - description = "Start by sending or receiving files to see your transfer history here. Your recent activity will appear in this section." + description = + "Start by sending or receiving files to see your transfer history here." + + " Your recent activity will appear in this section.", ) } } @@ -324,41 +330,55 @@ private fun EnhancedTransferHistoryCard(item: TransferHistoryItem) { DropCard( variant = DropCardVariant.Elevated, size = DropCardSize.Medium, - contentDescription = "Transfer: ${if (item.type == TransferType.SENT) "Sent to" else "Received from"} ${item.peerName}" + contentDescription = "Transfer: ${ + if (item.type == TransferType.SENT) "Sent to" else "Received from" + } ${item.peerName}", ) { DropCardContent(size = DropCardSize.Medium) { Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { // Transfer type icon with semantic color Icon( - imageVector = if (item.type == TransferType.SENT) TablerIcons.CloudUpload else TablerIcons.CloudDownload, + imageVector = + if (item.type == TransferType.SENT) + TablerIcons.CloudUpload + else + TablerIcons.CloudDownload, contentDescription = if (item.type == TransferType.SENT) "Sent" else "Received", modifier = Modifier.size(24.dp), - tint = if (item.type == TransferType.SENT) - MaterialTheme.colorScheme.primary - else - MaterialTheme.colorScheme.secondary + tint = + if (item.type == TransferType.SENT) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.secondary + }, ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.lg)) Column(modifier = Modifier.weight(1f)) { Text( - text = if (item.type == TransferType.SENT) - "Sent to ${item.peerName}" - else - "Received from ${item.peerName}", + text = + if (item.type == TransferType.SENT) { + "Sent to ${item.peerName}" + } else { + "Received from ${item.peerName}" + }, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xs)) Text( - text = "${item.fileCount} file${if (item.fileCount != 1) "s" else ""} • ${formatTimestamp(item.timestamp)}", + text = "${item.fileCount} file${ + if (item.fileCount != 1) "s" else "" + } • ${formatTimestamp( + item.timestamp, + )}", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -382,8 +402,9 @@ private fun formatTimestamp(timestamp: OffsetDateTime): String { diff.toHours() < 1 -> "${diff.toMinutes()}m ago" diff.toDays() < 1 -> "${diff.toHours()}h ago" diff.toDays() < 7 -> "${diff.toDays()}d ago" - else -> timestamp.format( - DateTimeFormatter.ofPattern("MMM dd", Locale.getDefault()) - ) + else -> + timestamp.format( + DateTimeFormatter.ofPattern("MMM dd", Locale.getDefault()), + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt index 362ee20..cc1fa95 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt @@ -23,7 +23,7 @@ sealed class HomeScreenEffect class HomeViewModel( private val historyItemRepository: TransferHistoryItemRepository, private val profileRepo: ProfileRepo, -): ViewModel(), ContainerHost{ +) : ViewModel(), ContainerHost { override val container: Container = container(HomeScreenState(emptyList(), UserProfile.empty())) @@ -47,5 +47,4 @@ class HomeViewModel( } } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt index a028bad..d9712ea 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt @@ -2,13 +2,20 @@ package dev.arkbuilders.drop.app.presentation.navigation sealed class DropDestination(val route: String) { object Home : DropDestination("home") + object Send : DropDestination("send") + object History : DropDestination("history") + object EditProfile : DropDestination("edit_profile") + object Receive : DropDestination("receive?ticket={ticket}&confirmation={confirmation}") { const val DEEP_LINK_PATTERN = "drop://receive?ticket={ticket}&confirmation={confirmation}" - - fun createRoute(ticket: String = "", confirmation: UByte): String { + + fun createRoute( + ticket: String = "", + confirmation: UByte, + ): String { return "receive?ticket=$ticket&confirmation=$confirmation" } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt index 4a728fd..63d7698 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt @@ -65,7 +65,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -100,9 +99,7 @@ import org.orbitmvi.orbit.compose.collectSideEffect @OptIn(ExperimentalMaterial3Api::class) @Composable -fun EditProfileEnhanced( - navController: NavController, -) { +fun EditProfileEnhanced(navController: NavController) { val viewModel: EditProfileViewModel = koinInject() val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current @@ -110,14 +107,14 @@ fun EditProfileEnhanced( val state by viewModel.collectAsState() - - val imagePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() - ) { uri: Uri? -> - if (uri != null) { - viewModel.onImagePicked(uri.toString()) + val imagePickerLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + ) { uri: Uri? -> + if (uri != null) { + viewModel.onImagePicked(uri.toString()) + } } - } viewModel.collectSideEffect { effect -> when (effect) { @@ -132,10 +129,11 @@ fun EditProfileEnhanced( } Column( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.ime) - .verticalScroll(rememberScrollState()) + modifier = + Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.ime) + .verticalScroll(rememberScrollState()), ) { // Enhanced Top App Bar TopAppBar( @@ -143,7 +141,7 @@ fun EditProfileEnhanced( Text( text = "Edit Profile", style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, ) }, navigationIcon = { @@ -151,14 +149,15 @@ fun EditProfileEnhanced( onClick = { navController.navigateUp() }, - modifier = Modifier.semantics { - contentDescription = "Go back to previous screen" - } + modifier = + Modifier.semantics { + contentDescription = "Go back to previous screen" + }, ) { Icon( Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, - tint = colorScheme.onSurface + tint = colorScheme.onSurface, ) } }, @@ -166,58 +165,65 @@ fun EditProfileEnhanced( AnimatedVisibility( visible = state.hasChanges, enter = scaleIn(spring(stiffness = Spring.StiffnessHigh)) + fadeIn(), - exit = scaleOut(spring(stiffness = Spring.StiffnessHigh)) + fadeOut() + exit = scaleOut(spring(stiffness = Spring.StiffnessHigh)) + fadeOut(), ) { DropButton( onClick = { viewModel.onSave() }, variant = DropButtonVariant.Primary, size = DropButtonSize.Medium, - contentDescription = "Save profile changes" + contentDescription = "Save profile changes", ) { Text( text = "Save", style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, ) } } }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = colorScheme.surface, - titleContentColor = colorScheme.onSurface - ) + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = colorScheme.surface, + titleContentColor = colorScheme.onSurface, + ), ) AnimatedVisibility( visible = state.avatarImageLoadingFailed, - enter = slideInVertically( - initialOffsetY = { -it }, - animationSpec = spring(stiffness = Spring.StiffnessMedium) - ) + fadeIn(), - exit = slideOutVertically( - targetOffsetY = { -it }, - animationSpec = spring(stiffness = Spring.StiffnessMedium) - ) + fadeOut() + enter = + slideInVertically( + initialOffsetY = { -it }, + animationSpec = spring(stiffness = Spring.StiffnessMedium), + ) + fadeIn(), + exit = + slideOutVertically( + targetOffsetY = { -it }, + animationSpec = spring(stiffness = Spring.StiffnessMedium), + ) + fadeOut(), ) { ErrorStateDisplay( - errorState = ErrorState( - type = ErrorType.Generic, - title = "Profile Update Failed", - message = "Failed to load image. Please check your storage permissions and try again.", - actionLabel = "Dismiss", - onAction = { - viewModel.clearAvatarLoadingError() - } - ), - modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg) + errorState = + ErrorState( + type = ErrorType.Generic, + title = "Profile Update Failed", + message = + "Failed to load image." + + " Please check your storage permissions and try again.", + actionLabel = "Dismiss", + onAction = { + viewModel.clearAvatarLoadingError() + }, + ), + modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg), ) } Column( - modifier = Modifier - .fillMaxSize() - .padding(DesignTokens.Spacing.lg), - verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.xl) + modifier = + Modifier + .fillMaxSize() + .padding(DesignTokens.Spacing.lg), + verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.xl), ) { ProfilePreviewSection( name = state.name, @@ -233,7 +239,7 @@ fun EditProfileEnhanced( onUploadClick = { viewModel.onPickImage() }, - hasError = state.avatarImageLoadingFailed + hasError = state.avatarImageLoadingFailed, ) AvatarSelectionSection( @@ -241,7 +247,7 @@ fun EditProfileEnhanced( avatar = state.avatar, onAvatarSelected = { avatarId -> viewModel.onAvatarSelected(avatarId) - } + }, ) PrivacyNoticeSection() @@ -262,20 +268,21 @@ private fun ProfilePreviewSection( DropCard( variant = DropCardVariant.Elevated, size = DropCardSize.Large, - contentDescription = "Profile preview and name editing" + contentDescription = "Profile preview and name editing", ) { DropCardContent(size = DropCardSize.Large) { Column( - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { var avatarScale by remember { mutableStateOf(0.8f) } val animatedAvatarScale by animateFloatAsState( targetValue = avatarScale, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessMedium - ), - label = "avatarScale" + animationSpec = + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium, + ), + label = "avatarScale", ) LaunchedEffect(avatar) { @@ -285,36 +292,39 @@ private fun ProfilePreviewSection( } Box( - modifier = Modifier - .size(120.dp) - .scale(animatedAvatarScale), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(120.dp) + .scale(animatedAvatarScale), + contentAlignment = Alignment.Center, ) { AvatarImage( - modifier = Modifier - .size(120.dp) - .semantics { - contentDescription = "Current profile avatar" - }, + modifier = + Modifier + .size(120.dp) + .semantics { + contentDescription = "Current profile avatar" + }, avatarB64 = avatar.base64, ) Surface( - modifier = Modifier - .align(Alignment.BottomEnd) - .size(32.dp), + modifier = + Modifier + .align(Alignment.BottomEnd) + .size(32.dp), shape = CircleShape, color = colorScheme.primary, - shadowElevation = DesignTokens.Elevation.sm + shadowElevation = DesignTokens.Elevation.sm, ) { Box( - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { Icon( Icons.Default.Edit, contentDescription = "Edit avatar", modifier = Modifier.size(16.dp), - tint = colorScheme.onPrimary + tint = colorScheme.onPrimary, ) } } @@ -329,28 +339,29 @@ private fun ProfilePreviewSection( label = { Text( "Display Name", - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) }, - modifier = Modifier - .fillMaxWidth() - .focusRequester(nameFocusRequester) - .semantics { - contentDescription = "Enter your display name" - }, + modifier = + Modifier + .fillMaxWidth() + .focusRequester(nameFocusRequester) + .semantics { + contentDescription = "Enter your display name" + }, singleLine = true, isError = nameError != null, supportingText = { AnimatedVisibility( visible = nameError != null, enter = slideInVertically() + fadeIn(), - exit = slideOutVertically() + fadeOut() + exit = slideOutVertically() + fadeOut(), ) { nameError?.let { Text( text = it.toString(), color = colorScheme.error, - style = MaterialTheme.typography.bodySmall + style = MaterialTheme.typography.bodySmall, ) } } @@ -359,46 +370,51 @@ private fun ProfilePreviewSection( if (name.isNotEmpty()) { IconButton( onClick = { onNameChange("") }, - modifier = Modifier.semantics { - contentDescription = "Clear name field" - } + modifier = + Modifier.semantics { + contentDescription = "Clear name field" + }, ) { Icon( Icons.Default.Clear, contentDescription = null, modifier = Modifier.size(20.dp), - tint = colorScheme.onSurfaceVariant + tint = colorScheme.onSurfaceVariant, ) } } }, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Words, - imeAction = ImeAction.Done - ), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = colorScheme.primary, - unfocusedBorderColor = colorScheme.outline, - errorBorderColor = colorScheme.error - ), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.Words, + imeAction = ImeAction.Done, + ), + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = colorScheme.primary, + unfocusedBorderColor = colorScheme.outline, + errorBorderColor = colorScheme.error, + ), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md), ) // Character count Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = DesignTokens.Spacing.xs), - horizontalArrangement = Arrangement.End + modifier = + Modifier + .fillMaxWidth() + .padding(top = DesignTokens.Spacing.xs), + horizontalArrangement = Arrangement.End, ) { Text( text = "${name.length}/50", style = MaterialTheme.typography.bodySmall, - color = if (name.length > 45) { - colorScheme.error - } else { - colorScheme.onSurfaceVariant - } + color = + if (name.length > 45) { + colorScheme.error + } else { + colorScheme.onSurfaceVariant + }, ) } } @@ -409,31 +425,31 @@ private fun ProfilePreviewSection( @Composable private fun CustomAvatarSection( onUploadClick: () -> Unit, - hasError: Boolean + hasError: Boolean, ) { DropCard( variant = DropCardVariant.Outlined, size = DropCardSize.Medium, - contentDescription = "Upload custom avatar option" + contentDescription = "Upload custom avatar option", ) { DropCardContent(size = DropCardSize.Medium) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, ) { Column(modifier = Modifier.weight(1f)) { Text( text = "Custom Avatar", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, - color = colorScheme.onSurface + color = colorScheme.onSurface, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xs)) Text( text = "Upload your own profile picture", style = MaterialTheme.typography.bodyMedium, - color = colorScheme.onSurfaceVariant + color = colorScheme.onSurfaceVariant, ) } @@ -441,20 +457,24 @@ private fun CustomAvatarSection( DropButton( onClick = onUploadClick, - variant = if (hasError) DropButtonVariant.Destructive else DropButtonVariant.Secondary, + variant = + if (hasError) + DropButtonVariant.Destructive + else + DropButtonVariant.Secondary, size = DropButtonSize.Medium, - contentDescription = "Upload custom avatar image" + contentDescription = "Upload custom avatar image", ) { Icon( TablerIcons.Camera, contentDescription = null, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Upload", style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } } @@ -466,7 +486,7 @@ private fun CustomAvatarSection( private fun AvatarSelectionSection( availableAvatars: List, avatar: UserAvatar, - onAvatarSelected: (String) -> Unit + onAvatarSelected: (String) -> Unit, ) { val columns = 3 @@ -476,15 +496,16 @@ private fun AvatarSelectionSection( style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = colorScheme.onSurface, - modifier = Modifier.semantics { - contentDescription = "Avatar selection section" - } + modifier = + Modifier.semantics { + contentDescription = "Avatar selection section" + }, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Column( - verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.lg) + verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.lg), ) { availableAvatars.chunked(columns).forEach { rowAvatars -> Row( @@ -493,21 +514,23 @@ private fun AvatarSelectionSection( ) { rowAvatars.forEach { avatarId -> EnhancedAvatarOption( - modifier = Modifier - .aspectRatio(1f) - .weight(1f), + modifier = + Modifier + .aspectRatio(1f) + .weight(1f), avatarId = avatarId, isSelected = avatar.predefinedId == avatarId, - onClick = { onAvatarSelected(avatarId) } + onClick = { onAvatarSelected(avatarId) }, ) } if (rowAvatars.size < columns) { repeat(columns - rowAvatars.size) { Spacer( - modifier = Modifier - .weight(1f) - .aspectRatio(1f) + modifier = + Modifier + .weight(1f) + .aspectRatio(1f), ) } } @@ -522,7 +545,7 @@ private fun EnhancedAvatarOption( modifier: Modifier, avatarId: String, isSelected: Boolean, - onClick: () -> Unit + onClick: () -> Unit, ) { val haptic = LocalHapticFeedback.current val avatarHelper: AvatarHelper = koinInject() @@ -530,49 +553,58 @@ private fun EnhancedAvatarOption( var scale by remember { mutableStateOf(1f) } val animatedScale by animateFloatAsState( targetValue = scale, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessHigh - ), - label = "avatarScale" + animationSpec = + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessHigh, + ), + label = "avatarScale", ) Card( - modifier = modifier - .scale(animatedScale), + modifier = + modifier + .scale(animatedScale), onClick = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) scale = 0.95f onClick() }, - colors = CardDefaults.cardColors( - containerColor = if (isSelected) { - colorScheme.primaryContainer + colors = + CardDefaults.cardColors( + containerColor = + if (isSelected) { + colorScheme.primaryContainer + } else { + colorScheme.surface + }, + ), + border = + if (isSelected) { + CardDefaults.outlinedCardBorder().copy( + width = 3.dp, + brush = SolidColor(colorScheme.primary), + ) } else { - colorScheme.surface - } - ), - border = if (isSelected) { - CardDefaults.outlinedCardBorder().copy( - width = 3.dp, - brush = SolidColor(colorScheme.primary) - ) - } else { - CardDefaults.outlinedCardBorder().copy( - width = 1.dp, - brush = SolidColor(colorScheme.outline) - ) - }, - elevation = CardDefaults.cardElevation( - defaultElevation = if (isSelected) DesignTokens.Elevation.md else DesignTokens.Elevation.xs - ), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + CardDefaults.outlinedCardBorder().copy( + width = 1.dp, + brush = SolidColor(colorScheme.outline), + ) + }, + elevation = + CardDefaults.cardElevation( + defaultElevation = + if (isSelected) + DesignTokens.Elevation.md + else + DesignTokens.Elevation.xs, + ), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), ) { - Box { Box( modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { AvatarImage( avatarB64 = avatarHelper.getDefaultAvatarBase64(avatarId), @@ -583,23 +615,25 @@ private fun EnhancedAvatarOption( visible = isSelected, enter = scaleIn(spring(stiffness = Spring.StiffnessHigh)) + fadeIn(), exit = scaleOut(spring(stiffness = Spring.StiffnessHigh)) + fadeOut(), - modifier = Modifier.align(Alignment.TopEnd) + modifier = Modifier.align(Alignment.TopEnd), ) { Surface( - modifier = Modifier.Companion - .padding(DesignTokens.Spacing.sm) - .size(20.dp), + modifier = + Modifier.Companion + .padding(DesignTokens.Spacing.sm) + .size(20.dp), shape = CircleShape, color = colorScheme.primary, - shadowElevation = DesignTokens.Elevation.sm + shadowElevation = DesignTokens.Elevation.sm, ) { Icon( Icons.Default.Check, contentDescription = "Selected", - modifier = Modifier - .fillMaxSize() - .padding(DesignTokens.Spacing.xs), - tint = colorScheme.onPrimary + modifier = + Modifier + .fillMaxSize() + .padding(DesignTokens.Spacing.xs), + tint = colorScheme.onPrimary, ) } } @@ -620,23 +654,25 @@ private fun PrivacyNoticeSection() { DropCard( variant = DropCardVariant.Filled, size = DropCardSize.Medium, - colors = CardDefaults.cardColors( - containerColor = colorScheme.surfaceVariant.copy(alpha = 0.5f), - contentColor = colorScheme.onSurfaceVariant - ), - contentDescription = "Privacy information about profile data" + colors = + CardDefaults.cardColors( + containerColor = colorScheme.surfaceVariant.copy(alpha = 0.5f), + contentColor = colorScheme.onSurfaceVariant, + ), + contentDescription = "Privacy information about profile data", ) { DropCardContent(size = DropCardSize.Medium) { Row( - verticalAlignment = Alignment.Top + verticalAlignment = Alignment.Top, ) { Icon( Icons.Default.Person, contentDescription = null, - modifier = Modifier - .size(20.dp) - .padding(top = 2.dp), - tint = colorScheme.primary + modifier = + Modifier + .size(20.dp) + .padding(top = 2.dp), + tint = colorScheme.primary, ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) @@ -646,16 +682,20 @@ private fun PrivacyNoticeSection() { text = "Privacy & Security", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, - color = colorScheme.onSurface + color = colorScheme.onSurface, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xs)) Text( - text = "Your profile information is only shared during file transfers and is stored locally on your device. Custom avatars are processed and stored securely without being uploaded to any server.", + text = + "Your profile information is only shared during file transfers and" + + " is stored locally on your device." + + " Custom avatars are processed and" + + " stored securely without being uploaded to any server.", style = MaterialTheme.typography.bodySmall, color = colorScheme.onSurfaceVariant, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight * 1.2 + lineHeight = MaterialTheme.typography.bodySmall.lineHeight * 1.2, ) } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt index a79622f..80a9442 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileViewModel.kt @@ -19,11 +19,14 @@ data class EditProfileScreenState( ) enum class EditProfileNameError { - EMPTY, TOO_SHORT, TOO_LONG + EMPTY, + TOO_SHORT, + TOO_LONG, } sealed class EditProfileScreenEffect { data object LaunchImagePicker : EditProfileScreenEffect() + data object NavigateBack : EditProfileScreenEffect() } @@ -31,7 +34,6 @@ class EditProfileViewModel( private val profileRepo: ProfileRepo, private val avatarHelper: AvatarHelper, ) : ViewModel(), ContainerHost { - override val container: Container = container( EditProfileScreenState( @@ -39,7 +41,7 @@ class EditProfileViewModel( name = "", nameError = null, avatar = UserAvatar("", null), - ) + ), ) init { @@ -51,60 +53,67 @@ class EditProfileViewModel( } } - fun onNameChanged(newName: String) = blockingIntent { - val nameError = when { - newName.isBlank() -> EditProfileNameError.EMPTY - newName.trim().length < 2 -> EditProfileNameError.TOO_SHORT - newName.length > 50 -> EditProfileNameError.TOO_LONG - else -> null + fun onNameChanged(newName: String) = + blockingIntent { + val nameError = + when { + newName.isBlank() -> EditProfileNameError.EMPTY + newName.trim().length < 2 -> EditProfileNameError.TOO_SHORT + newName.length > 50 -> EditProfileNameError.TOO_LONG + else -> null + } + reduce { + state.copy( + name = newName, + nameError = nameError, + hasChanges = state.currentProfile.name != newName, + ) + } } - reduce { - state.copy( - name = newName, - nameError = nameError, - hasChanges = state.currentProfile.name != newName, - ) + + fun onPickImage() = + intent { + postSideEffect(EditProfileScreenEffect.LaunchImagePicker) } - } - fun onPickImage() = intent { - postSideEffect(EditProfileScreenEffect.LaunchImagePicker) - } + fun onImagePicked(uri: String) = + intent { + val base64 = avatarHelper.uriToBase64(uri) + base64?.let { + reduce { + state.copy( + avatar = UserAvatar(base64, predefinedId = null), + hasChanges = state.avatar != state.currentProfile.avatar, + ) + } + } ?: let { + state.copy( + avatarImageLoadingFailed = true, + ) + } + } - fun onImagePicked(uri: String) = intent { - val base64 = avatarHelper.uriToBase64(uri) - base64?.let { + fun onAvatarSelected(id: String) = + intent { + val base64 = avatarHelper.getDefaultAvatarBase64(id) reduce { state.copy( - avatar = UserAvatar(base64, predefinedId = null), + avatar = UserAvatar(base64, predefinedId = id), hasChanges = state.avatar != state.currentProfile.avatar, ) } - } ?: let { - state.copy( - avatarImageLoadingFailed = true, - ) } - } - fun onAvatarSelected(id: String) = intent { - val base64 = avatarHelper.getDefaultAvatarBase64(id) - reduce { - state.copy( - avatar = UserAvatar(base64, predefinedId = id), - hasChanges = state.avatar != state.currentProfile.avatar, - ) + fun onSave() = + intent { + profileRepo.updateProfile(UserProfile(state.name, state.avatar)) + postSideEffect(EditProfileScreenEffect.NavigateBack) } - } - fun onSave() = intent { - profileRepo.updateProfile(UserProfile(state.name, state.avatar)) - postSideEffect(EditProfileScreenEffect.NavigateBack) - } - - fun clearAvatarLoadingError() = intent { - reduce { - state.copy(avatarImageLoadingFailed = false) + fun clearAvatarLoadingError() = + intent { + reduce { + state.copy(avatarImageLoadingFailed = false) + } } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt index e266fcc..e78af51 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt @@ -54,7 +54,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import dev.arkbuilders.drop.app.R @@ -93,6 +92,7 @@ sealed class ReceiveError(val message: String, val isRecoverable: Boolean = true ReceiveError("File transfer was interrupted. Please try again.", true) object NoFilesReceived : ReceiveError("No files were received from the sender.", true) + object StorageError : ReceiveError("Unable to save files. Please check your storage permissions.", true) @@ -104,18 +104,17 @@ sealed class ReceiveError(val message: String, val isRecoverable: Boolean = true @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @Composable -fun Receive( - navController: NavController, -) { +fun Receive(navController: NavController) { val viewModel: ReceiveViewModel = koinInject() val clipboardManager = LocalClipboardManager.current val keyboardController = LocalSoftwareKeyboardController.current - val requestPermissionLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted -> - viewModel.onCameraPermissionGranted(isGranted) - } + val requestPermissionLauncher = + rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { isGranted -> + viewModel.onCameraPermissionGranted(isGranted) + } val state by viewModel.collectAsState() viewModel.collectSideEffect { effect -> @@ -133,7 +132,6 @@ fun Receive( } ReceiveScreenEffect.ShowSuccessAnimation -> { - } } } @@ -149,37 +147,39 @@ fun Receive( val successScale by animateFloatAsState( targetValue = if (showSuccessAnimation) 1f else 0f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow - ), - label = "successScale" + animationSpec = + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow, + ), + label = "successScale", ) Column( - modifier = Modifier - .fillMaxSize() - .padding(DesignTokens.Spacing.lg) + modifier = + Modifier + .fillMaxSize() + .padding(DesignTokens.Spacing.lg), ) { Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Surface( onClick = { navController.navigateUp() }, shape = CircleShape, color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f), - modifier = Modifier.Companion.size(DesignTokens.TouchTarget.minimum) + modifier = Modifier.Companion.size(DesignTokens.TouchTarget.minimum), ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { Icon( Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) } } @@ -200,7 +200,7 @@ fun Receive( style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) } @@ -208,55 +208,68 @@ fun Receive( AnimatedVisibility( visible = showSuccessAnimation, - enter = scaleIn( - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow - ) - ) + fadeIn(), - exit = scaleOut( - animationSpec = tween(DesignTokens.Animation.normal) - ) + fadeOut() + enter = + scaleIn( + animationSpec = + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow, + ), + ) + fadeIn(), + exit = + scaleOut( + animationSpec = tween(DesignTokens.Animation.NORMAL), + ) + fadeOut(), ) { Box( modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .scale(successScale), + modifier = + Modifier + .fillMaxWidth() + .scale(successScale), shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.xl) + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.xl, + ), ) { Column( modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Box( - modifier = Modifier - .size(80.dp) - .background( - brush = Brush.radialGradient( - colors = listOf( - MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), - Color.Transparent - ) - ), - shape = CircleShape - ) - .clip(CircleShape), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(80.dp) + .background( + brush = + Brush.radialGradient( + colors = + listOf( + MaterialTheme.colorScheme.primary.copy( + alpha = 0.2f, + ), + Color.Transparent, + ), + ), + shape = CircleShape, + ) + .clip(CircleShape), + contentAlignment = Alignment.Center, ) { Icon( Icons.Default.CheckCircle, contentDescription = "Success", modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) } @@ -266,30 +279,32 @@ fun Receive( text = "Files Received!", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) Text( - text = "All files have been successfully saved to your Downloads folder.", + text = + "All files have been successfully saved to your" + + " Downloads folder.", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f), - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2 + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2, ) } } } } - val _state = state - when (_state) { + val receiveScreenState = state + when (receiveScreenState) { is ReceiveScreenState.Initial -> { - if (_state.cameraPermissionGranted) { + if (receiveScreenState.cameraPermissionGranted) { ReceiveReadyToScanCard( onStartScanning = { viewModel.onStartScanning() }, - onEnterManually = { viewModel.onEnterManually() } + onEnterManually = { viewModel.onEnterManually() }, ) } else { ReceivePermissionRequestCard( @@ -298,7 +313,7 @@ fun Receive( }, onEnterManually = { viewModel.onEnterManually() - } + }, ) } } @@ -316,22 +331,26 @@ fun Receive( viewModel.onError(error) }, onStopScanning = { viewModel.onStopScanning() }, - onEnterManually = { viewModel.onEnterManually() } + onEnterManually = { viewModel.onEnterManually() }, ) } is ReceiveScreenState.ManualInput -> { ReceiveManualInputCard( - inputText = _state.inputText, + inputText = receiveScreenState.inputText, onInputChange = { viewModel.onManualInputChanged(it) }, - inputError = _state.inputError, - onPasteFromClipboard = { viewModel.onPasteFromClipboard(clipboardManager.getText()?.text) }, + inputError = receiveScreenState.inputError, + onPasteFromClipboard = { + viewModel.onPasteFromClipboard( + clipboardManager.getText()?.text, + ) + }, onSubmit = { viewModel.handleManualInputSubmit() }, onCancel = { viewModel.onCancelManualInput() - } + }, ) } @@ -342,7 +361,7 @@ fun Receive( }, onScanAgain = { viewModel.onScanAgain() - } + }, ) } @@ -352,86 +371,90 @@ fun Receive( is ReceiveScreenState.Receiving -> { ReceiveProgressCard( - progress = _state.progress, + progress = receiveScreenState.progress, onCancel = { viewModel.onCancelReceiving() - } + }, ) } is ReceiveScreenState.Success -> { if (!showSuccessAnimation) { ReceiveCompleteCard( - receivedFiles = _state.receivedFiles, + receivedFiles = receiveScreenState.receivedFiles, onReceiveMore = { viewModel.onReceiveMore() }, onDone = { viewModel.onDone() - } + }, ) } } is ReceiveScreenState.Error -> { ReceiveErrorCard( - error = _state.error, + error = receiveScreenState.error, onRetry = { viewModel.onErrorRetry() }, onDismiss = { viewModel.onErrorDismiss() - } + }, ) } } - if (state !is ReceiveScreenState.Success - && state !is ReceiveScreenState.Error + if (state !is ReceiveScreenState.Success && + state !is ReceiveScreenState.Error ) { - Spacer(modifier = Modifier.weight(1f)) Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - contentColor = MaterialTheme.colorScheme.onSurfaceVariant - ), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + colors = + CardDefaults.cardColors( + containerColor = + MaterialTheme.colorScheme.surfaceVariant.copy( + alpha = 0.5f, + ), + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), ) { Column( - modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg) + modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg), ) { Text( text = "How to receive files:", style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) - val steps = listOf( - "Ask the sender to start a transfer", - "Scan QR code OR enter transfer code manually", - "Accept the transfer", - "Files will be saved to your Downloads folder" - ) + val steps = + listOf( + "Ask the sender to start a transfer", + "Scan QR code OR enter transfer code manually", + "Accept the transfer", + "Files will be saved to your Downloads folder", + ) steps.forEachIndexed { index, step -> Row( verticalAlignment = Alignment.Top, - modifier = Modifier.padding(vertical = 2.dp) + modifier = Modifier.padding(vertical = 2.dp), ) { Text( text = "${index + 1}.", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( text = step, style = MaterialTheme.typography.bodyMedium, - lineHeight = MaterialTheme.typography.bodyMedium.lineHeight * 1.2 + lineHeight = MaterialTheme.typography.bodyMedium.lineHeight * 1.2, ) } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt index 7f71f33..c5a39c8 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt @@ -14,20 +14,31 @@ import org.orbitmvi.orbit.viewmodel.container sealed class ReceiveScreenState { data class Initial(val cameraPermissionGranted: Boolean) : ReceiveScreenState() + data object RequestingPermission : ReceiveScreenState() + data object Scanning : ReceiveScreenState() + data class ManualInput(val inputText: String, val inputError: String?) : ReceiveScreenState() + data class QRCodeScanned(val ticket: String, val confirmation: UByte) : ReceiveScreenState() + data object Connecting : ReceiveScreenState() + data class Receiving(val progress: ReceivingProgress) : ReceiveScreenState() + data class Success(val receivedFiles: List) : ReceiveScreenState() + data class Error(val error: ReceiveError) : ReceiveScreenState() } sealed class ReceiveScreenEffect { data object HideKeyboard : ReceiveScreenEffect() + data object NavigateBack : ReceiveScreenEffect() + data object ShowSuccessAnimation : ReceiveScreenEffect() + data object RequestCameraPermission : ReceiveScreenEffect() } @@ -41,205 +52,229 @@ class ReceiveViewModel( init { intent { val receiveProgress = transferManager.receiveProgress?.value - val state = if (receiveProgress != null && receiveProgress.isConnected) { - listenToProgress() - ReceiveScreenState.Receiving(receiveProgress) - } else { - ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) - } + val state = + if (receiveProgress != null && receiveProgress.isConnected) { + listenToProgress() + ReceiveScreenState.Receiving(receiveProgress) + } else { + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) + } reduce { state } } } - fun onRequestCameraPermission() = intent { - reduce { - ReceiveScreenState.RequestingPermission - } - postSideEffect(ReceiveScreenEffect.RequestCameraPermission) - } - - fun onEnterManually() = intent { - reduce { - ReceiveScreenState.ManualInput(inputText = "", inputError = null) - } - } - - fun onStartScanning() = intent { - reduce { - ReceiveScreenState.Scanning + fun onRequestCameraPermission() = + intent { + reduce { + ReceiveScreenState.RequestingPermission + } + postSideEffect(ReceiveScreenEffect.RequestCameraPermission) } - } - fun onStopScanning() = intent { - reduce { - ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) + fun onEnterManually() = + intent { + reduce { + ReceiveScreenState.ManualInput(inputText = "", inputError = null) + } } - } - fun onError(error: ReceiveError) = intent { - reduce { - ReceiveScreenState.Error(error) + fun onStartScanning() = + intent { + reduce { + ReceiveScreenState.Scanning + } } - } - fun onAccept() = intent { - try { - val s = state - if (s !is ReceiveScreenState.QRCodeScanned) { - return@intent + fun onStopScanning() = + intent { + reduce { + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } - val ticket = s.ticket - val confirmation = s.confirmation + } + fun onError(error: ReceiveError) = + intent { reduce { - ReceiveScreenState.Connecting + ReceiveScreenState.Error(error) } + } - val bubble = - transferManager.receiveFiles(ticket, confirmation) - if (bubble != null) { - reduce { - ReceiveScreenState.Receiving(transferManager.receiveProgress!!.value) + fun onAccept() = + intent { + try { + val s = state + if (s !is ReceiveScreenState.QRCodeScanned) { + return@intent } - listenToProgress() - } else { + val ticket = s.ticket + val confirmation = s.confirmation + reduce { - ReceiveScreenState.Error(ReceiveError.ConnectionFailed) + ReceiveScreenState.Connecting } - } - } catch (e: Exception) { - val error = when { - e.message?.contains( - "network", - ignoreCase = true - ) == true -> ReceiveError.NetworkError - else -> ReceiveError.ConnectionFailed + val bubble = + transferManager.receiveFiles(ticket, confirmation) + if (bubble != null) { + reduce { + ReceiveScreenState.Receiving(transferManager.receiveProgress!!.value) + } + listenToProgress() + } else { + reduce { + ReceiveScreenState.Error(ReceiveError.ConnectionFailed) + } + } + } catch (e: Exception) { + val error = + when { + e.message?.contains( + "network", + ignoreCase = true, + ) == true -> ReceiveError.NetworkError + + else -> ReceiveError.ConnectionFailed + } + + reduce { + ReceiveScreenState.Error(error) + } } + } + fun onCameraPermissionGranted(isGranted: Boolean) = + intent { + val state = + if (isGranted) { + ReceiveScreenState.Scanning + } else { + ReceiveScreenState.Error(ReceiveError.CameraPermissionDenied) + } reduce { - ReceiveScreenState.Error(error) + state } } - } - fun onCameraPermissionGranted(isGranted: Boolean) = intent { - val state = if (isGranted) { - ReceiveScreenState.Scanning - } else { - ReceiveScreenState.Error(ReceiveError.CameraPermissionDenied) - } - reduce { - state + fun onScanAgain() = + intent { + val state = + if (permissionsHelper.isCameraGranted()) { + ReceiveScreenState.Scanning + } else { + ReceiveScreenState.ManualInput(inputText = "", inputError = null) + } + reduce { + state + } } - } - fun onScanAgain() = intent { - val state = if (permissionsHelper.isCameraGranted()) { - ReceiveScreenState.Scanning - } else { - ReceiveScreenState.ManualInput(inputText = "", inputError = null) - } - reduce { - state + fun onReceiveMore() = + intent { + transferManager.cancelReceive() + reduce { + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) + } } - } - fun onReceiveMore() = intent { - transferManager.cancelReceive() - reduce { - ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) + fun onDone() = + intent { + transferManager.cancelReceive() + postSideEffect(ReceiveScreenEffect.NavigateBack) } - } - fun onDone() = intent { - transferManager.cancelReceive() - postSideEffect(ReceiveScreenEffect.NavigateBack) - } + fun onPasteFromClipboard(clipText: String?) = + intent { + val s = state + if (s !is ReceiveScreenState.ManualInput) + return@intent - fun onPasteFromClipboard(clipText: String?) = intent { - val s = state - if (s !is ReceiveScreenState.ManualInput) - return@intent + if (!clipText.isNullOrEmpty()) { + reduce { + s.copy( + inputText = clipText, + inputError = null, + ) + } + } + } - if (!clipText.isNullOrEmpty()) { + fun onErrorRetry() = + intent { reduce { - s.copy( - inputText = clipText, - inputError = null, - ) + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } } - } - fun onErrorRetry() = intent { - reduce { - ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) + fun onErrorDismiss() = + intent { + transferManager.cancelReceive() + postSideEffect(ReceiveScreenEffect.NavigateBack) } - } - - fun onErrorDismiss() = intent { - transferManager.cancelReceive() - postSideEffect(ReceiveScreenEffect.NavigateBack) - } - fun onQrCodeScanned(ticket: String, confirmation: UByte) = intent { + fun onQrCodeScanned( + ticket: String, + confirmation: UByte, + ) = intent { reduce { ReceiveScreenState.QRCodeScanned(ticket, confirmation) } } - fun onManualInputChanged(input: String) = blockingIntent { - val s = state - if (s !is ReceiveScreenState.ManualInput) - return@blockingIntent - - reduce { - s.copy( - inputText = input, - inputError = null, - ) - } - } + fun onManualInputChanged(input: String) = + blockingIntent { + val s = state + if (s !is ReceiveScreenState.ManualInput) + return@blockingIntent - fun onCancelReceiving() = intent { - transferManager.cancelReceive() - reduce { - ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) + reduce { + s.copy( + inputText = input, + inputError = null, + ) + } } - } - fun onCancelManualInput() = intent { - reduce { - ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) + fun onCancelReceiving() = + intent { + transferManager.cancelReceive() + reduce { + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) + } } - postSideEffect(ReceiveScreenEffect.HideKeyboard) - } - - fun handleManualInputSubmit() = intent { - val s = state - if (s !is ReceiveScreenState.ManualInput) - return@intent - val parsed = parseManualInput(s.inputText) - if (parsed != null) { + fun onCancelManualInput() = + intent { reduce { - ReceiveScreenState.QRCodeScanned( - ticket = parsed.first, - confirmation = parsed.second - ) + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } postSideEffect(ReceiveScreenEffect.HideKeyboard) - } else { - reduce { - s.copy( - inputError = "Invalid format. Please enter: ticket confirmation", - ) + } + + fun handleManualInputSubmit() = + intent { + val s = state + if (s !is ReceiveScreenState.ManualInput) + return@intent + + val parsed = parseManualInput(s.inputText) + if (parsed != null) { + reduce { + ReceiveScreenState.QRCodeScanned( + ticket = parsed.first, + confirmation = parsed.second, + ) + } + postSideEffect(ReceiveScreenEffect.HideKeyboard) + } else { + reduce { + s.copy( + inputError = "Invalid format. Please enter: ticket confirmation", + ) + } } } - } private fun listenToProgress() { transferManager.receiveProgress!!.onEach { progress -> @@ -250,16 +285,17 @@ class ReceiveViewModel( reduce { s.copy( - progress = progress + progress = progress, ) } if (progress.isConnected && progress.files.isNotEmpty()) { // Check if all files are complete - val allFilesComplete = progress.files.all { file -> - val fileProgress = progress.fileProgress[file.id] - fileProgress?.isComplete == true - } + val allFilesComplete = + progress.files.all { file -> + val fileProgress = progress.fileProgress[file.id] + fileProgress?.isComplete == true + } if (allFilesComplete) { // Small delay to ensure UI updates are visible @@ -269,27 +305,28 @@ class ReceiveViewModel( if (savedFiles.isNotEmpty()) { reduce { ReceiveScreenState.Success( - receivedFiles = savedFiles.map { it.name } + receivedFiles = savedFiles.map { it.name }, ) } postSideEffect(ReceiveScreenEffect.ShowSuccessAnimation) } else { reduce { ReceiveScreenState.Error( - ReceiveError.NoFilesReceived + ReceiveError.NoFilesReceived, ) } } } catch (e: Exception) { - val error = when { - e.message?.contains("storage", ignoreCase = true) == true -> - ReceiveError.StorageError + val error = + when { + e.message?.contains("storage", ignoreCase = true) == true -> + ReceiveError.StorageError - e.message?.contains("network", ignoreCase = true) == true -> - ReceiveError.NetworkError + e.message?.contains("network", ignoreCase = true) == true -> + ReceiveError.NetworkError - else -> ReceiveError.UnknownError - } + else -> ReceiveError.UnknownError + } reduce { ReceiveScreenState.Error(error) } @@ -321,4 +358,4 @@ class ReceiveViewModel( null } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt index 1c70d24..910bf32 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt @@ -37,35 +37,40 @@ import dev.arkbuilders.drop.app.presentation.theme.DesignTokens fun ReceiveCompleteCard( receivedFiles: List, onReceiveMore: () -> Unit, - onDone: () -> Unit + onDone: () -> Unit, ) { ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + ), + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.lg, + ), ) { Column( modifier = Modifier.Companion.padding(DesignTokens.Spacing.xl), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Box( - modifier = Modifier - .size(80.dp) - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - shape = CircleShape - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(80.dp) + .background( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), + shape = CircleShape, + ), + contentAlignment = Alignment.Center, ) { Icon( Icons.Default.CheckCircle, contentDescription = "Complete", modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) } @@ -75,35 +80,38 @@ fun ReceiveCompleteCard( text = "Files Received Successfully!", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) Text( - text = "${receivedFiles.size} file${if (receivedFiles.size != 1) "s" else ""} saved to Downloads", + text = "${receivedFiles.size} file${ + if (receivedFiles.size != 1) "s" else "" + } saved to Downloads", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f) + color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f), ) if (receivedFiles.isNotEmpty()) { Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md), ) { Column( - modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg) + modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg), ) { Text( text = "Received Files:", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) @@ -114,7 +122,7 @@ fun ReceiveCompleteCard( style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) } if (receivedFiles.size > 3) { @@ -122,7 +130,7 @@ fun ReceiveCompleteCard( text = "• ... and ${receivedFiles.size - 3} more", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } } @@ -133,19 +141,20 @@ fun ReceiveCompleteCard( Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md), ) { OutlinedButton( onClick = onReceiveMore, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md), ) { Icon( Icons.Default.Refresh, contentDescription = null, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(18.dp), ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text("Receive More", fontWeight = FontWeight.Medium) @@ -153,14 +162,15 @@ fun ReceiveCompleteCard( Button( onClick = onDone, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md), ) { Text("Done", fontWeight = FontWeight.Medium) } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt index bc56278..8a6282d 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt @@ -37,35 +37,40 @@ import dev.arkbuilders.drop.app.presentation.theme.DesignTokens fun ReceiveErrorCard( error: ReceiveError, onRetry: () -> Unit, - onDismiss: () -> Unit + onDismiss: () -> Unit, ) { ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ), + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.lg, + ), ) { Column( modifier = Modifier.Companion.padding(DesignTokens.Spacing.xl), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Box( - modifier = Modifier - .size(80.dp) - .background( - color = MaterialTheme.colorScheme.error.copy(alpha = 0.1f), - shape = CircleShape - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(80.dp) + .background( + color = MaterialTheme.colorScheme.error.copy(alpha = 0.1f), + shape = CircleShape, + ), + contentAlignment = Alignment.Center, ) { Icon( if (error.isRecoverable) Icons.Default.Warning else TablerIcons.AlertCircle, contentDescription = null, modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.error + tint = MaterialTheme.colorScheme.error, ) } @@ -75,7 +80,7 @@ fun ReceiveErrorCard( text = if (error.isRecoverable) "Something went wrong" else "Error occurred", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) @@ -85,21 +90,22 @@ fun ReceiveErrorCard( style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f), - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2 + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md), ) { OutlinedButton( onClick = onDismiss, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md) + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md), ) { Text("Cancel", fontWeight = FontWeight.Medium) } @@ -107,13 +113,15 @@ fun ReceiveErrorCard( if (error.isRecoverable) { Button( onClick = onRetry, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), shape = RoundedCornerShape(DesignTokens.CornerRadius.md), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), ) { Text("Try Again", fontWeight = FontWeight.SemiBold) } @@ -121,4 +129,4 @@ fun ReceiveErrorCard( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt index d745a10..a9f10b8 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt @@ -21,36 +21,39 @@ import androidx.compose.ui.unit.dp import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable -fun ReceiveLoadingCard( - message: String -) { +fun ReceiveLoadingCard(message: String) { ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.md) + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.md, + ), ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(DesignTokens.Spacing.xl), + modifier = + Modifier + .fillMaxWidth() + .padding(DesignTokens.Spacing.xl), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.Center, ) { CircularProgressIndicator( modifier = Modifier.size(24.dp), strokeWidth = 3.dp, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.lg)) Text( text = message, style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt index bd1203b..691bf66 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt @@ -42,35 +42,40 @@ fun ReceiveManualInputCard( inputError: String?, onPasteFromClipboard: () -> Unit, onSubmit: () -> Unit, - onCancel: () -> Unit + onCancel: () -> Unit, ) { ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.lg, + ), ) { Column( modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Box( - modifier = Modifier - .size(80.dp) - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = CircleShape - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(80.dp) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, ) { Icon( TablerIcons.ArrowForward, contentDescription = null, modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer + tint = MaterialTheme.colorScheme.onPrimaryContainer, ) } @@ -80,17 +85,19 @@ fun ReceiveManualInputCard( text = "Enter Transfer Code", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) Text( - text = "Paste or type the transfer code from the sender in the format: ticket confirmation", + text = + "Paste or type the transfer code from the sender in the format:" + + " ticket confirmation", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) @@ -102,63 +109,69 @@ fun ReceiveManualInputCard( placeholder = { Text("ticket confirmation") }, modifier = Modifier.fillMaxWidth(), isError = inputError != null, - supportingText = inputError?.let { error -> - { Text(error, color = MaterialTheme.colorScheme.error) } - }, + supportingText = + inputError?.let { error -> + { Text(error, color = MaterialTheme.colorScheme.error) } + }, trailingIcon = { IconButton(onClick = onPasteFromClipboard) { Icon( TablerIcons.ArrowForward, contentDescription = "Paste", - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) } }, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { onSubmit() } - ), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + keyboardOptions = + KeyboardOptions( + imeAction = ImeAction.Done, + ), + keyboardActions = + KeyboardActions( + onDone = { onSubmit() }, + ), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md), ) { OutlinedButton( onClick = onCancel, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), ) { Text( "Cancel", - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } Button( onClick = onSubmit, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ), - enabled = inputText.trim().isNotEmpty() + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + enabled = inputText.trim().isNotEmpty(), ) { Text( "Connect", - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, ) } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt index 5aed5bb..5ef8ca9 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt @@ -33,35 +33,40 @@ import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceivePermissionRequestCard( onRequestPermission: () -> Unit, - onEnterManually: () -> Unit + onEnterManually: () -> Unit, ) { ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.lg, + ), ) { Column( modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Box( - modifier = Modifier - .size(80.dp) - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = CircleShape - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(80.dp) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, ) { Icon( TablerIcons.Camera, contentDescription = null, modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer + tint = MaterialTheme.colorScheme.onPrimaryContainer, ) } @@ -71,35 +76,39 @@ fun ReceivePermissionRequestCard( text = "Camera Permission Required", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) Text( - text = "We need camera access to scan QR codes for receiving files. Your privacy is protected - we only use the camera for QR code scanning.", + text = + "We need camera access to scan QR codes for receiving files." + + " Your privacy is protected - we only use the camera for QR code scanning.", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Button( onClick = onRequestPermission, - modifier = Modifier - .fillMaxWidth() - .height(DesignTokens.TouchTarget.comfortable), + modifier = + Modifier + .fillMaxWidth() + .height(DesignTokens.TouchTarget.comfortable), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), ) { Text( "Grant Permission", style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, ) } @@ -109,29 +118,30 @@ fun ReceivePermissionRequestCard( text = "Or", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) OutlinedButton( onClick = onEnterManually, - modifier = Modifier - .fillMaxWidth() - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + modifier = + Modifier + .fillMaxWidth() + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), ) { Icon( TablerIcons.ArrowForward, contentDescription = null, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(18.dp), ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Enter Code Manually", - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt index 1d7b657..85e8020 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt @@ -43,46 +43,50 @@ import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveProgressCard( progress: ReceivingProgress, - onCancel: () -> Unit + onCancel: () -> Unit, ) { ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.lg, + ), ) { Column( - modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg) + modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg), ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = if (progress.isConnected) "Receiving Files..." else "Connecting...", style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) Surface( onClick = onCancel, shape = CircleShape, color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), - modifier = Modifier.size(40.dp) + modifier = Modifier.size(40.dp), ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { Icon( Icons.Default.Close, contentDescription = "Cancel", tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) } } @@ -93,25 +97,25 @@ fun ReceiveProgressCard( Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md), ) { AvatarImageWithFallback( avatarB64 = progress.senderAvatar, fallbackText = progress.senderName, - size = 36.dp + size = 36.dp, ) Column { Text( text = "From:", style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f), ) Text( text = progress.senderName, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onPrimaryContainer + color = MaterialTheme.colorScheme.onPrimaryContainer, ) } } @@ -121,24 +125,31 @@ fun ReceiveProgressCard( Text( text = "Files (${progress.files.size}):", style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) LazyColumn( modifier = Modifier.heightIn(max = 280.dp), - verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.sm) + verticalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.sm), ) { items(progress.files) { file -> val fileProgress = progress.fileProgress[file.id] ReceivingFileItem( file = file, - progress = if (file.size > 0UL && fileProgress != null) { - (fileProgress.receivedBytes.toFloat() / file.size.toFloat()).coerceIn(0f, 1f) - } else 0f, + progress = + if (file.size > 0UL && fileProgress != null) { + (fileProgress.receivedBytes.toFloat() / file.size.toFloat()) + .coerceIn( + 0f, + 1f, + ) + } else { + 0f + }, receivedBytes = fileProgress?.receivedBytes ?: 0L, - isComplete = fileProgress?.isComplete ?: false + isComplete = fileProgress?.isComplete ?: false, ) } } @@ -146,12 +157,12 @@ fun ReceiveProgressCard( Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Box( modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { CircularProgressIndicator( modifier = Modifier.size(48.dp), strokeWidth = 4.dp, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, ) } } @@ -164,23 +175,27 @@ private fun ReceivingFileItem( file: ReceiveFileInfo, progress: Float, receivedBytes: Long, - isComplete: Boolean + isComplete: Boolean, ) { - ElevatedCard( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ), shape = RoundedCornerShape(DesignTokens.CornerRadius.md), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.xs) + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.xs, + ), ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(DesignTokens.Spacing.lg), - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier + .fillMaxWidth() + .padding(DesignTokens.Spacing.lg), + verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f)) { Text( @@ -189,17 +204,18 @@ private fun ReceivingFileItem( fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) LinearProgressIndicator( progress = { progress }, - modifier = Modifier - .fillMaxWidth() - .height(6.dp) - .clip(RoundedCornerShape(3.dp)), + modifier = + Modifier + .fillMaxWidth() + .height(6.dp) + .clip(RoundedCornerShape(3.dp)), color = MaterialTheme.colorScheme.primary, trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), ) @@ -209,26 +225,27 @@ private fun ReceivingFileItem( Text( text = "${formatBytes(receivedBytes)} / ${formatBytes(file.size.toLong())}", style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } if (isComplete) { Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) Box( - modifier = Modifier - .size(32.dp) - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - shape = CircleShape - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(32.dp) + .background( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), + shape = CircleShape, + ), + contentAlignment = Alignment.Center, ) { Icon( Icons.Default.CheckCircle, contentDescription = "Complete", tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) } } @@ -244,4 +261,4 @@ private fun formatBytes(bytes: Long): String { if (mb < 1024) return "%.1f MB".format(mb) val gb = mb / 1024.0 return "%.1f GB".format(gb) -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt index 8667534..c0008fe 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt @@ -33,35 +33,40 @@ import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveQRCodeScannedCard( onAccept: () -> Unit, - onScanAgain: () -> Unit + onScanAgain: () -> Unit, ) { ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.lg, + ), ) { Column( modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Box( - modifier = Modifier - .size(80.dp) - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = CircleShape - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(80.dp) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, ) { Icon( Icons.Default.CheckCircle, contentDescription = null, modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) } @@ -71,7 +76,7 @@ fun ReceiveQRCodeScannedCard( text = "Code Received!", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) @@ -81,44 +86,47 @@ fun ReceiveQRCodeScannedCard( style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2 + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md), ) { OutlinedButton( onClick = onScanAgain, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), ) { Text( "Try Again", - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } Button( onClick = onAccept, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), ) { Text( "Accept", - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, ) } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt index 46b9c2d..a2e5d31 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt @@ -35,40 +35,47 @@ import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveReadyToScanCard( onStartScanning: () -> Unit, - onEnterManually: () -> Unit + onEnterManually: () -> Unit, ) { ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.lg, + ), ) { Column( modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Box( - modifier = Modifier - .size(80.dp) - .background( - brush = Brush.radialGradient( - colors = listOf( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) - ) + modifier = + Modifier + .size(80.dp) + .background( + brush = + Brush.radialGradient( + colors = + listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.primary.copy(alpha = 0.8f), + ), + ), + shape = CircleShape, ), - shape = CircleShape - ), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { Icon( TablerIcons.Qrcode, contentDescription = null, modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.onPrimary + tint = MaterialTheme.colorScheme.onPrimary, ) } @@ -77,41 +84,45 @@ fun ReceiveReadyToScanCard( Text( text = "Ready to Receive", style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) Text( - text = "Scan the QR code from the sender's device or enter the transfer code manually to start receiving files securely.", + text = + "Scan the QR code from the sender's device or enter the transfer" + + " code manually to start receiving files securely.", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3 + lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.3, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) Button( onClick = onStartScanning, - modifier = Modifier - .fillMaxWidth() - .height(DesignTokens.TouchTarget.comfortable), + modifier = + Modifier + .fillMaxWidth() + .height(DesignTokens.TouchTarget.comfortable), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), ) { Icon( TablerIcons.Camera, contentDescription = null, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Start Scanning", style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, ) } @@ -121,29 +132,30 @@ fun ReceiveReadyToScanCard( text = "Or", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) OutlinedButton( onClick = onEnterManually, - modifier = Modifier - .fillMaxWidth() - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + modifier = + Modifier + .fillMaxWidth() + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), ) { Icon( TablerIcons.ArrowForward, contentDescription = null, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(18.dp), ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Enter Code Manually", - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt index 9f4a25e..4d5c1a1 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt @@ -55,19 +55,23 @@ fun ReceiveScanningCard( onQRCodeScanned: (String, UByte) -> Unit, onError: (ReceiveError) -> Unit, onStopScanning: () -> Unit, - onEnterManually: () -> Unit + onEnterManually: () -> Unit, ) { Column { ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f), + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f), shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = DesignTokens.Elevation.lg) + elevation = + CardDefaults.elevatedCardElevation( + defaultElevation = DesignTokens.Elevation.lg, + ), ) { QRCodeScanner( onQRCodeScanned = onQRCodeScanned, - onError = onError + onError = onError, ) } @@ -79,50 +83,52 @@ fun ReceiveScanningCard( fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md) + horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md), ) { OutlinedButton( onClick = onStopScanning, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), ) { Icon( TablerIcons.CameraOff, contentDescription = null, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(18.dp), ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Stop Scanning", - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } Button( onClick = onEnterManually, - modifier = Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg) + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), ) { Icon( TablerIcons.ArrowForward, contentDescription = null, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(18.dp), ) Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) Text( "Enter Code", - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } } @@ -133,7 +139,7 @@ fun ReceiveScanningCard( @Composable private fun QRCodeScanner( onQRCodeScanned: (String, UByte) -> Unit, - onError: (ReceiveError) -> Unit + onError: (ReceiveError) -> Unit, ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current @@ -148,18 +154,20 @@ private fun QRCodeScanner( try { val cameraProvider = cameraProviderFuture.get() - val preview = Preview.Builder().build().also { - it.setSurfaceProvider(previewView.surfaceProvider) - } + val preview = + Preview.Builder().build().also { + it.setSurfaceProvider(previewView.surfaceProvider) + } - val imageAnalyzer = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() - .also { - it.setAnalyzer(cameraExecutor) { imageProxy -> - processImageProxy(imageProxy, onQRCodeScanned, onError) + val imageAnalyzer = + ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + it.setAnalyzer(cameraExecutor) { imageProxy -> + processImageProxy(imageProxy, onQRCodeScanned, onError) + } } - } val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA @@ -168,7 +176,7 @@ private fun QRCodeScanner( lifecycleOwner, cameraSelector, preview, - imageAnalyzer + imageAnalyzer, ) } catch (exc: Exception) { onError(ReceiveError.CameraInitializationFailed) @@ -177,7 +185,7 @@ private fun QRCodeScanner( previewView }, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) DisposableEffect(Unit) { @@ -191,7 +199,7 @@ private fun QRCodeScanner( private fun processImageProxy( imageProxy: ImageProxy, onQRCodeScanned: (String, UByte) -> Unit, - onError: (ReceiveError) -> Unit + onError: (ReceiveError) -> Unit, ) { val mediaImage = imageProxy.image if (mediaImage != null) { @@ -234,11 +242,11 @@ private fun processImageProxy( when { exception.message?.contains( "camera", - ignoreCase = true + ignoreCase = true, ) == true -> ReceiveError.CameraInitializationFailed else -> ReceiveError.UnknownError - } + }, ) } .addOnCompleteListener { @@ -247,4 +255,4 @@ private fun processImageProxy( } else { imageProxy.close() } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt index a06b04a..28e10a4 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt @@ -94,62 +94,64 @@ sealed class SendException( val message: String, val icon: ImageVector, val isRecoverable: Boolean = true, - val actionLabel: String? = null + val actionLabel: String? = null, ) { object NetworkUnavailable : SendException( title = "No Network Connection", message = "Please check your Wi-Fi or mobile data connection and try again.", icon = Icons.Default.Warning, - actionLabel = "Retry" + actionLabel = "Retry", ) object FileTooLarge : SendException( title = "File Too Large", - message = "Some files exceed the 2GB limit and were skipped. You can send the remaining files.", + message = + "Some files exceed the 2GB limit and were skipped." + + " You can send the remaining files.", icon = Icons.Default.Warning, - actionLabel = "Continue" + actionLabel = "Continue", ) object NoFilesSelected : SendException( title = "No Files Selected", message = "Please select at least one file to send.", icon = Icons.Default.Warning, - isRecoverable = false + isRecoverable = false, ) object TransferInitializationFailed : SendException( title = "Transfer Setup Failed", message = "Unable to prepare files for transfer. Please try again.", icon = TablerIcons.AlertCircle, - actionLabel = "Retry" + actionLabel = "Retry", ) object QRGenerationFailed : SendException( title = "QR Code Generation Failed", message = "Unable to create QR code. Please restart the transfer.", icon = TablerIcons.AlertCircle, - actionLabel = "Retry" + actionLabel = "Retry", ) object TransferInterrupted : SendException( title = "Transfer Interrupted", message = "The connection was lost during transfer. You can try sending again.", icon = TablerIcons.AlertCircle, - actionLabel = "Retry" + actionLabel = "Retry", ) object ReceiverDisconnected : SendException( title = "Receiver Disconnected", message = "The receiving device disconnected. Please try again.", icon = Icons.Default.Warning, - actionLabel = "Retry" + actionLabel = "Retry", ) class UnknownError(details: String) : SendException( title = "Something Went Wrong", message = "An unexpected error occurred: $details", icon = TablerIcons.AlertCircle, - actionLabel = "Retry" + actionLabel = "Retry", ) } @@ -163,14 +165,14 @@ data class TransferProgressState( val bytesTransferred: Long = 0L, val totalBytes: Long = 0L, val transferSpeedBps: Long = 0L, - val estimatedTimeRemaining: Long = 0L + val estimatedTimeRemaining: Long = 0L, ) @OptIn(ExperimentalMaterial3Api::class) @Composable fun Send( navController: NavController, - transferManager: TransferManager + transferManager: TransferManager, ) { val haptic = LocalHapticFeedback.current val listState = rememberLazyListState() @@ -178,11 +180,12 @@ fun Send( val state by viewModel.collectAsState() - val filePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetMultipleContents() - ) { uris -> - viewModel.onFilesAdded(uris.map { it.toString() }) - } + val filePickerLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetMultipleContents(), + ) { uris -> + viewModel.onFilesAdded(uris.map { it.toString() }) + } viewModel.collectSideEffect { effect -> when (effect) { @@ -196,35 +199,36 @@ fun Send( } } - Scaffold( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.statusBars) - .windowInsetsPadding(WindowInsets.navigationBars) - .windowInsetsPadding(WindowInsets.ime), + modifier = + Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.statusBars) + .windowInsetsPadding(WindowInsets.navigationBars) + .windowInsetsPadding(WindowInsets.ime), topBar = { SendTopBar( onBackClick = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) navController.navigateUp() - } + }, ) }, - containerColor = MaterialTheme.colorScheme.background + containerColor = MaterialTheme.colorScheme.background, ) { paddingValues -> Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues), ) { - val _state = state - when (_state) { + val sendScreenState = state + when (sendScreenState) { is SendScreenState.FileSelection -> { FileSelectionPhase( - selectedFiles = _state.files, - totalFileSize = _state.size, + selectedFiles = sendScreenState.files, + totalFileSize = sendScreenState.size, onAddFiles = { viewModel.onAddFiles() }, @@ -234,8 +238,8 @@ fun Send( onStartTransfer = { viewModel.onStartTransfer() }, - canStartTransfer = _state.canStartTransfer, - listState = listState + canStartTransfer = sendScreenState.canStartTransfer, + listState = listState, ) } @@ -245,21 +249,21 @@ fun Send( is SendScreenState.WaitingForReceiver -> { WaitingForReceiverPhase( - fileCount = _state.files.size, - onCancel = { viewModel.onCancelTransfer() } + fileCount = sendScreenState.files.size, + onCancel = { viewModel.onCancelTransfer() }, ) } is SendScreenState.Transfer -> { TransferringPhase( - progress = _state, - onCancel = { viewModel.onCancelTransfer() } + progress = sendScreenState, + onCancel = { viewModel.onCancelTransfer() }, ) } is SendScreenState.Complete -> { TransferCompletePhase( - fileCount = _state.files.size, + fileCount = sendScreenState.files.size, onSendMore = { viewModel.onSendMore() }, @@ -297,7 +301,7 @@ fun Send( fileCount = s.files.size, copyString = s.copyString, onDismiss = { }, - onCancel = {} + onCancel = {}, ) } } @@ -306,14 +310,12 @@ fun Send( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun SendTopBar( - onBackClick: () -> Unit -) { +private fun SendTopBar(onBackClick: () -> Unit) { TopAppBar( title = { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Icon( modifier = Modifier.size(24.dp), @@ -322,59 +324,79 @@ private fun SendTopBar( tint = Color.Unspecified, ) Text( - text = "Send Files", style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.SemiBold, fontSize = 20.sp - ), color = MaterialTheme.colorScheme.onSurface + text = "Send Files", + style = + MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + ), + color = MaterialTheme.colorScheme.onSurface, ) } - }, navigationIcon = { + }, + navigationIcon = { IconButton( onClick = onBackClick, - modifier = Modifier - .size(44.dp) - .clip(CircleShape) - .semantics { contentDescription = "Go back" }) { + modifier = + Modifier + .size(44.dp) + .clip(CircleShape) + .semantics { contentDescription = "Go back" }, + ) { Icon( Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(24.dp), ) } - }, actions = { + }, + actions = { // Network status indicator // NetworkStatusIndicator( // connected = networkConnected, modifier = Modifier.padding(end = 8.dp) // ) - }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f), - titleContentColor = MaterialTheme.colorScheme.onSurface - ) + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f), + titleContentColor = MaterialTheme.colorScheme.onSurface, + ), ) } @Composable private fun NetworkStatusIndicator( - connected: Boolean, modifier: Modifier = Modifier + connected: Boolean, + modifier: Modifier = Modifier, ) { val alpha by animateFloatAsState( targetValue = if (connected) 0.7f else 1f, animationSpec = tween(300), - label = "networkAlpha" + label = "networkAlpha", ) Icon( imageVector = if (connected) TablerIcons.CloudUpload else Icons.Default.Warning, contentDescription = if (connected) "Network connected" else "Network disconnected", - modifier = modifier - .size(20.dp) - .alpha(alpha), - tint = if (connected) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.error + modifier = + modifier + .size(20.dp) + .alpha(alpha), + tint = + if (connected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.error + }, ) } -private fun copyToClipboard(context: Context, text: String, label: String = "Transfer Info") { +private fun copyToClipboard( + context: Context, + text: String, + label: String = "Transfer Info", +) { val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipData = ClipData.newPlainText(label, text) clipboardManager.setPrimaryClip(clipData) @@ -386,7 +408,7 @@ private fun SendQRDialog( fileCount: Int, copyString: String?, onDismiss: () -> Unit, - onCancel: () -> Unit + onCancel: () -> Unit, ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -397,37 +419,39 @@ private fun SendQRDialog( title = { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Icon( TablerIcons.Qrcode, contentDescription = null, modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) Text( "QR Code for Transfer", - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.Bold - ) + style = + MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold, + ), ) } }, text = { Column( - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Surface( shape = RoundedCornerShape(16.dp), color = Color.White, - shadowElevation = 4.dp + shadowElevation = 4.dp, ) { Image( bitmap = qrBitmap.asImageBitmap(), contentDescription = "QR code for file transfer", - modifier = Modifier - .size(220.dp) - .padding(16.dp) + modifier = + Modifier + .size(220.dp) + .padding(16.dp), ) } @@ -435,11 +459,12 @@ private fun SendQRDialog( Text( text = "Show this QR code to the receiver", - style = MaterialTheme.typography.bodyLarge.copy( - fontWeight = FontWeight.Medium - ), + style = + MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium, + ), color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(8.dp)) @@ -448,7 +473,7 @@ private fun SendQRDialog( text = "$fileCount file${if (fileCount != 1) "s" else ""} ready to transfer", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) // Copy functionality @@ -465,17 +490,17 @@ private fun SendQRDialog( } }, variant = ButtonVariant.Secondary, - size = ButtonSize.Medium + size = ButtonSize.Medium, ) { Icon( TablerIcons.Copy, contentDescription = null, - modifier = Modifier.size(16.dp) + modifier = Modifier.size(16.dp), ) Spacer(modifier = Modifier.width(8.dp)) Text( if (showCopySuccess) "Copied!" else "Copy Code", - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } @@ -485,7 +510,7 @@ private fun SendQRDialog( text = "Transfer code copied to clipboard", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } @@ -494,15 +519,16 @@ private fun SendQRDialog( Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { SendLoadingIndicator() Text( text = "Waiting for receiver to scan...", - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium - ), - color = MaterialTheme.colorScheme.primary + style = + MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.primary, ) } } @@ -511,14 +537,14 @@ private fun SendQRDialog( dismissButton = { TextButton( onClick = onCancel, - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(8.dp), ) { Text( "Cancel Transfer", - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } }, - shape = RoundedCornerShape(20.dp) + shape = RoundedCornerShape(20.dp), ) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt index df34188..a402a47 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt @@ -51,7 +51,7 @@ sealed class SendScreenState { val bytesTransferred: Long = 0L, val totalBytes: Long = 0L, val transferSpeedBps: Long = 0L, - val estimatedTimeRemaining: Long = 0L + val estimatedTimeRemaining: Long = 0L, ) : SendScreenState() data class Complete(val files: List) : SendScreenState() @@ -61,6 +61,7 @@ sealed class SendScreenState { sealed class SendScreenEffect { data object LaunchFilePicker : SendScreenEffect() + data object NavigateBack : SendScreenEffect() } @@ -68,116 +69,125 @@ class SendViewModel( private val resourcesHelper: ResourcesHelper, private val networkStatus: NetworkStatus, private val transferManager: TransferManager, -): ViewModel(), ContainerHost { +) : ViewModel(), ContainerHost { override val container: Container = container(SendScreenState.FileSelection()) - fun onAddFiles() = intent { - postSideEffect(SendScreenEffect.LaunchFilePicker) - } + fun onAddFiles() = + intent { + postSideEffect(SendScreenEffect.LaunchFilePicker) + } - fun onFilesAdded(newFiles: List) = intent { - val s = state - if (s is SendScreenState.FileSelection) { - val validated = resourcesHelper.validateUris(newFiles) - val allFiles = s.files + validated.first - val canStartTransfer = allFiles.isNotEmpty() && networkStatus.isOnline() + fun onFilesAdded(newFiles: List) = + intent { + val s = state + if (s is SendScreenState.FileSelection) { + val validated = resourcesHelper.validateUris(newFiles) + val allFiles = s.files + validated.first + val canStartTransfer = allFiles.isNotEmpty() && networkStatus.isOnline() - val size = allFiles.sumOf { resourcesHelper.getFileSize(it) } + val size = allFiles.sumOf { resourcesHelper.getFileSize(it) } - reduce { - s.copy(files = allFiles, size = size, canStartTransfer = canStartTransfer) + reduce { + s.copy(files = allFiles, size = size, canStartTransfer = canStartTransfer) + } } } - } - fun onFileRemove(file: String) = intent { - val s = state - if (s is SendScreenState.FileSelection) { - val newFiles = s.files - file - val size = newFiles.sumOf { resourcesHelper.getFileSize(it) } - reduce { - s.copy(files = newFiles, size = size) + fun onFileRemove(file: String) = + intent { + val s = state + if (s is SendScreenState.FileSelection) { + val newFiles = s.files - file + val size = newFiles.sumOf { resourcesHelper.getFileSize(it) } + reduce { + s.copy(files = newFiles, size = size) + } } } - } - fun onStartTransfer() = intent { - val s = state - if (s !is SendScreenState.FileSelection) { - reduce { - SendScreenState.Error(Error("")) + fun onStartTransfer() = + intent { + val s = state + if (s !is SendScreenState.FileSelection) { + reduce { + SendScreenState.Error(Error("")) + } + return@intent } - return@intent - } - val bubble = transferManager.sendFiles(s.files.map { it.toUri() }) - if (bubble == null) { - reduce { - SendScreenState.Error(Error("")) + val bubble = transferManager.sendFiles(s.files.map { it.toUri() }) + if (bubble == null) { + reduce { + SendScreenState.Error(Error("")) + } + return@intent } - return@intent - } - val ticket = transferManager.getCurrentSendTicket() ?: "" - val confirmation = transferManager.getCurrentSendConfirmation() ?: 0u + val ticket = transferManager.getCurrentSendTicket() ?: "" + val confirmation = transferManager.getCurrentSendConfirmation() ?: 0u - if (ticket.isEmpty()) { - reduce { - SendScreenState.Error(Error("")) + if (ticket.isEmpty()) { + reduce { + SendScreenState.Error(Error("")) + } + return@intent } - return@intent - } - val copyString = "${bubble.getTicket()} ${bubble.getConfirmation()}" + val copyString = "${bubble.getTicket()} ${bubble.getConfirmation()}" - val qrBitmap = generateQRCodeSafely(ticket, confirmation) - if (qrBitmap == null) { + val qrBitmap = generateQRCodeSafely(ticket, confirmation) + if (qrBitmap == null) { + reduce { + SendScreenState.Error(Error("")) + } + return@intent + } + listenToSendProgress() + monitorTransferCompletion() reduce { - SendScreenState.Error(Error("")) + SendScreenState.WaitingForReceiver( + files = s.files, + qrBitmap = qrBitmap, + copyString = copyString, + ) } - return@intent } - listenToSendProgress() - monitorTransferCompletion() - reduce { - SendScreenState.WaitingForReceiver( - files = s.files, - qrBitmap = qrBitmap, - copyString = copyString, - ) - } - } - fun onCancelTransfer() = intent { - transferManager.cancelSend() - postSideEffect(SendScreenEffect.NavigateBack) - } - - fun onCancelQrGeneration() = intent { - reduce { - SendScreenState.FileSelection() + fun onCancelTransfer() = + intent { + transferManager.cancelSend() + postSideEffect(SendScreenEffect.NavigateBack) } - } - fun onComplete() = intent { - val s = state - if (s is SendScreenState.Transfer) { - transferManager.recordSendCompletion(s.files.map { it.toUri() }) + fun onCancelQrGeneration() = + intent { reduce { - SendScreenState.Complete(files = s.files) + SendScreenState.FileSelection() } } - } - fun onDone() = intent { - transferManager.cancelSend() - postSideEffect(SendScreenEffect.NavigateBack) - } + fun onComplete() = + intent { + val s = state + if (s is SendScreenState.Transfer) { + transferManager.recordSendCompletion(s.files.map { it.toUri() }) + reduce { + SendScreenState.Complete(files = s.files) + } + } + } - fun onSendMore() = intent { - transferManager.cancelSend() - reduce { - SendScreenState.FileSelection() + fun onDone() = + intent { + transferManager.cancelSend() + postSideEffect(SendScreenEffect.NavigateBack) + } + + fun onSendMore() = + intent { + transferManager.cancelSend() + reduce { + SendScreenState.FileSelection() + } } - } private fun listenToSendProgress() { transferManager.sendProgress!!.onEach { progress -> @@ -186,23 +196,25 @@ class SendViewModel( return@intent val s = state - val files = when (s) { - is SendScreenState.Transfer -> s.files - is SendScreenState.WaitingForReceiver -> s.files - else -> return@intent - } + val files = + when (s) { + is SendScreenState.Transfer -> s.files + is SendScreenState.WaitingForReceiver -> s.files + else -> return@intent + } - val transfer = SendScreenState.Transfer( - files = files, - isConnected = progress.isConnected, - receiverName = progress.receiverName, - receiverAvatar = progress.receiverAvatar, - currentFileName = progress.fileName, - bytesTransferred = progress.sent.toLong(), - totalBytes = (progress.sent + progress.remaining).toLong(), - transferSpeedBps = 0L, - estimatedTimeRemaining = 0L, - ) + val transfer = + SendScreenState.Transfer( + files = files, + isConnected = progress.isConnected, + receiverName = progress.receiverName, + receiverAvatar = progress.receiverAvatar, + currentFileName = progress.fileName, + bytesTransferred = progress.sent.toLong(), + totalBytes = (progress.sent + progress.remaining).toLong(), + transferSpeedBps = 0L, + estimatedTimeRemaining = 0L, + ) reduce { transfer @@ -213,18 +225,21 @@ class SendViewModel( private fun monitorTransferCompletion() { viewModelScope.launch { - while (coroutineContext.isActive) { - val isFinished = transferManager.isSendFinished() - if (isFinished) { - onComplete() - break - } - delay(500) - } + while (coroutineContext.isActive) { + val isFinished = transferManager.isSendFinished() + if (isFinished) { + onComplete() + break + } + delay(500) + } } } - private fun generateQRCodeSafely(ticket: String, confirmation: UByte): Bitmap? { + private fun generateQRCodeSafely( + ticket: String, + confirmation: UByte, + ): Bitmap? { val writer = QRCodeWriter() try { if (ticket.isEmpty()) { @@ -239,21 +254,22 @@ class SendViewModel( for (x in 0 until width) { for (y in 0 until height) { - bitmap[x, y] = if (bitMatrix[x, y]) { - Color.BLACK - } else { - Color.WHITE - } + bitmap[x, y] = + if (bitMatrix[x, y]) { + Color.BLACK + } else { + Color.WHITE + } } } return bitmap } catch (e: WriterException) { // TODO - // throw RuntimeException("QR code generation failed: ${e.message}", e) + // throw RuntimeException("QR code generation failed: ${e.message}", e) return null } catch (e: Exception) { - // throw RuntimeException("Unexpected error during QR code generation: ${e.message}", e) + // throw RuntimeException("Unexpected error during QR code generation: ${e.message}", e) return null } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendButton.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendButton.kt index 6a1be99..186186c 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendButton.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendButton.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp enum class ButtonVariant { Primary, Secondary } + enum class ButtonSize { Medium, Large } @Composable @@ -25,12 +26,13 @@ fun SendButton( modifier: Modifier = Modifier, enabled: Boolean = true, loading: Boolean = false, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { - val height = when (size) { - ButtonSize.Medium -> 44.dp - ButtonSize.Large -> 56.dp - } + val height = + when (size) { + ButtonSize.Medium -> 44.dp + ButtonSize.Large -> 56.dp + } when (variant) { ButtonVariant.Primary -> { @@ -39,21 +41,31 @@ fun SendButton( modifier = modifier.height(height), enabled = enabled && !loading, shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), - disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f) - ), - elevation = ButtonDefaults.buttonElevation( - defaultElevation = 2.dp, pressedElevation = 4.dp, disabledElevation = 0.dp - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor = + MaterialTheme.colorScheme.primary.copy( + alpha = 0.3f, + ), + disabledContentColor = + MaterialTheme.colorScheme.onPrimary.copy( + alpha = 0.5f, + ), + ), + elevation = + ButtonDefaults.buttonElevation( + defaultElevation = 2.dp, + pressedElevation = 4.dp, + disabledElevation = 0.dp, + ), ) { if (loading) { CircularProgressIndicator( modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp + strokeWidth = 2.dp, ) Spacer(modifier = Modifier.width(12.dp)) } @@ -67,16 +79,17 @@ fun SendButton( modifier = modifier.height(height), enabled = enabled && !loading, shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer - ) + colors = + ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ), ) { if (loading) { CircularProgressIndicator( modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onSecondaryContainer, - strokeWidth = 2.dp + strokeWidth = 2.dp, ) Spacer(modifier = Modifier.width(12.dp)) } @@ -84,4 +97,4 @@ fun SendButton( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendCard.kt index 263ab09..4218433 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendCard.kt @@ -13,15 +13,21 @@ import androidx.compose.ui.unit.dp fun SendCard( modifier: Modifier = Modifier, backgroundColor: Color = MaterialTheme.colorScheme.surface, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { Card( - modifier = modifier, shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( - containerColor = backgroundColor - ), elevation = CardDefaults.cardElevation( - defaultElevation = 1.dp, pressedElevation = 2.dp - ) + modifier = modifier, + shape = RoundedCornerShape(16.dp), + colors = + CardDefaults.cardColors( + containerColor = backgroundColor, + ), + elevation = + CardDefaults.cardElevation( + defaultElevation = 1.dp, + pressedElevation = 2.dp, + ), ) { content() } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendEmptyState.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendEmptyState.kt index 9033162..ea27222 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendEmptyState.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendEmptyState.kt @@ -21,29 +21,36 @@ import androidx.compose.ui.unit.dp @Composable fun SendEmptyState( - title: String, description: String, icon: ImageVector + title: String, + description: String, + icon: ImageVector, ) { Surface( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), ) { Column( - modifier = Modifier.padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier.padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( imageVector = icon, contentDescription = null, modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = title, style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold - ), color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center + text = title, + style = + MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(8.dp)) @@ -52,8 +59,8 @@ fun SendEmptyState( text = description, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt index 7f01d0f..eb4e799 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt @@ -27,39 +27,51 @@ import dev.arkbuilders.drop.app.presentation.send.SendException @Composable fun SendErrorOverlay( - error: SendException, onDismiss: () -> Unit, onAction: (String) -> Unit + error: SendException, + onDismiss: () -> Unit, + onAction: (String) -> Unit, ) { Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.5f)) - .clickable( - interactionSource = remember { MutableInteractionSource() }, indication = null - ) { onDismiss() }, contentAlignment = Alignment.Center + modifier = + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { onDismiss() }, + contentAlignment = Alignment.Center, ) { SendCard( - modifier = Modifier - .padding(20.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, indication = null - ) { /* Prevent dismiss on card click */ }) { + modifier = + Modifier + .padding(20.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { /* Prevent dismiss on card click */ }, + ) { Column( modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( imageVector = error.icon, contentDescription = null, modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error + tint = MaterialTheme.colorScheme.error, ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = error.title, style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold - ), color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center + text = error.title, + style = + MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(8.dp)) @@ -68,22 +80,23 @@ fun SendErrorOverlay( text = error.message, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(20.dp)) Row( - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { if (error.isRecoverable && error.actionLabel != null) { SendButton( onClick = { onAction(error.actionLabel) }, variant = ButtonVariant.Primary, - size = ButtonSize.Medium + size = ButtonSize.Medium, ) { Text( - error.actionLabel, fontWeight = FontWeight.Medium + error.actionLabel, + fontWeight = FontWeight.Medium, ) } } @@ -91,14 +104,15 @@ fun SendErrorOverlay( SendButton( onClick = onDismiss, variant = ButtonVariant.Secondary, - size = ButtonSize.Medium + size = ButtonSize.Medium, ) { Text( - "Dismiss", fontWeight = FontWeight.Medium + "Dismiss", + fontWeight = FontWeight.Medium, ) } } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt index 1f8318f..5c7b30b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendFileItem.kt @@ -40,7 +40,8 @@ import org.koin.java.KoinJavaComponent.inject @Composable fun SendFileItem( - uri: String, onRemove: () -> Unit + uri: String, + onRemove: () -> Unit, ) { val context = LocalContext.current val haptic = LocalHapticFeedback.current @@ -49,9 +50,10 @@ fun SendFileItem( LaunchedEffect(uri) { try { - val resourcesHelper: ResourcesHelper = inject( - ResourcesHelper::class.java - ).value + val resourcesHelper: ResourcesHelper = + inject( + ResourcesHelper::class.java, + ).value fileName = resourcesHelper.getFileName(uri) ?: "Unknown file" fileSize = resourcesHelper.getFileSize(uri) @@ -65,19 +67,20 @@ fun SendFileItem( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), - tonalElevation = 1.dp + tonalElevation = 1.dp, ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, ) { Icon( TablerIcons.FileText, contentDescription = null, modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.width(12.dp)) @@ -85,12 +88,13 @@ fun SendFileItem( Column(modifier = Modifier.weight(1f)) { Text( text = fileName, - style = MaterialTheme.typography.bodyLarge.copy( - fontWeight = FontWeight.Medium - ), + style = + MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium, + ), maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) if (fileSize > 0) { @@ -98,7 +102,7 @@ fun SendFileItem( Text( text = formatBytes(fileSize), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), ) } } @@ -107,17 +111,19 @@ fun SendFileItem( onClick = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) onRemove() - }, modifier = Modifier - .size(36.dp) - .clip(CircleShape) + }, + modifier = + Modifier + .size(36.dp) + .clip(CircleShape), ) { Icon( Icons.Default.Delete, contentDescription = "Remove file", modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.error + tint = MaterialTheme.colorScheme.error, ) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt index 280f6db..126b8c9 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt @@ -21,43 +21,48 @@ import androidx.compose.ui.unit.dp @Composable fun SendInstructionsCard() { SendCard( - backgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + backgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), ) { Column( - modifier = Modifier.padding(20.dp) + modifier = Modifier.padding(20.dp), ) { Text( - text = "How to Send Files", style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Bold - ), color = MaterialTheme.colorScheme.onSurface + text = "How to Send Files", + style = + MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.onSurface, ) Spacer(modifier = Modifier.height(16.dp)) - val instructions = listOf( - "Select files you want to send", - "Tap 'Send Files' to generate QR code", - "Let the receiver scan the QR code", - "Files transfer automatically" - ) + val instructions = + listOf( + "Select files you want to send", + "Tap 'Send Files' to generate QR code", + "Let the receiver scan the QR code", + "Files transfer automatically", + ) instructions.forEachIndexed { index, instruction -> Row( verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Surface( modifier = Modifier.size(20.dp), shape = CircleShape, - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), ) { Box(contentAlignment = Alignment.Center) { Text( text = "${index + 1}", - style = MaterialTheme.typography.labelSmall.copy( - fontWeight = FontWeight.Bold - ), - color = MaterialTheme.colorScheme.primary + style = + MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.primary, ) } } @@ -66,7 +71,7 @@ fun SendInstructionsCard() { text = instruction, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 2.dp) + modifier = Modifier.padding(top = 2.dp), ) } @@ -76,4 +81,4 @@ fun SendInstructionsCard() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendLoadingIndicator.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendLoadingIndicator.kt index 2c62546..db21573 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendLoadingIndicator.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendLoadingIndicator.kt @@ -15,25 +15,27 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @Composable -fun SendLoadingIndicator( - message: String? = null -) { +fun SendLoadingIndicator(message: String? = null) { Column( - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { CircularProgressIndicator( modifier = Modifier.size(32.dp), color = MaterialTheme.colorScheme.primary, - strokeWidth = 3.dp + strokeWidth = 3.dp, ) message?.let { Spacer(modifier = Modifier.height(16.dp)) Text( - text = it, style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium - ), color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center + text = it, + style = + MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendProgressBar.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendProgressBar.kt index f78995a..7781649 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendProgressBar.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendProgressBar.kt @@ -11,14 +11,16 @@ import androidx.compose.ui.unit.dp @Composable fun SendProgressBar( - progress: Float, modifier: Modifier = Modifier + progress: Float, + modifier: Modifier = Modifier, ) { LinearProgressIndicator( progress = { progress }, - modifier = modifier - .height(6.dp) - .clip(RoundedCornerShape(3.dp)), + modifier = + modifier + .height(6.dp) + .clip(RoundedCornerShape(3.dp)), color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) + trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), ) -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt index af2f056..407da94 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt @@ -26,19 +26,23 @@ import dev.arkbuilders.drop.app.presentation.send.components.SendCard @Composable private fun ErrorPhase( - error: SendException?, onRetry: () -> Unit, onCancel: () -> Unit + error: SendException?, + onRetry: () -> Unit, + onCancel: () -> Unit, ) { Box( - modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, ) { Column( modifier = Modifier.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(24.dp) + verticalArrangement = Arrangement.spacedBy(24.dp), ) { error?.let { err -> SendErrorCard( - error = err, onAction = if (err.isRecoverable) onRetry else null + error = err, + onAction = if (err.isRecoverable) onRetry else null, ) } @@ -46,12 +50,14 @@ private fun ErrorPhase( onClick = onCancel, variant = ButtonVariant.Secondary, size = ButtonSize.Large, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) { Text( - "Cancel", style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Medium - ) + "Cancel", + style = + MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Medium, + ), ) } } @@ -60,27 +66,33 @@ private fun ErrorPhase( @Composable private fun SendErrorCard( - error: SendException, onAction: (() -> Unit)? = null + error: SendException, + onAction: (() -> Unit)? = null, ) { SendCard( - backgroundColor = MaterialTheme.colorScheme.errorContainer + backgroundColor = MaterialTheme.colorScheme.errorContainer, ) { Column( - modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( imageVector = error.icon, contentDescription = null, modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error + tint = MaterialTheme.colorScheme.error, ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = error.title, style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold - ), color = MaterialTheme.colorScheme.onErrorContainer, textAlign = TextAlign.Center + text = error.title, + style = + MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.onErrorContainer, + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(8.dp)) @@ -89,20 +101,23 @@ private fun SendErrorCard( text = error.message, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) if (onAction != null && error.actionLabel != null) { Spacer(modifier = Modifier.height(20.dp)) SendButton( - onClick = onAction, variant = ButtonVariant.Primary, size = ButtonSize.Medium + onClick = onAction, + variant = ButtonVariant.Primary, + size = ButtonSize.Medium, ) { Text( - error.actionLabel, fontWeight = FontWeight.Medium + error.actionLabel, + fontWeight = FontWeight.Medium, ) } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt index 7a70505..a539f96 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt @@ -43,44 +43,47 @@ fun FileSelectionPhase( onRemoveFile: (String) -> Unit, onStartTransfer: () -> Unit, canStartTransfer: Boolean, - listState: LazyListState + listState: LazyListState, ) { LazyColumn( state = listState, modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(20.dp), - verticalArrangement = Arrangement.spacedBy(24.dp) + verticalArrangement = Arrangement.spacedBy(24.dp), ) { // File selection section item { SendCard { Column( - modifier = Modifier.padding(20.dp) + modifier = Modifier.padding(20.dp), ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Column { Text( text = "Selected Files", - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.SemiBold - ), - color = MaterialTheme.colorScheme.onSurface + style = + MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, ) if (selectedFiles.isNotEmpty()) { Spacer(modifier = Modifier.height(4.dp)) Text( - text = "${selectedFiles.size} file${if (selectedFiles.size != 1) "s" else ""} • ${ + text = "${selectedFiles.size} file${ + if (selectedFiles.size != 1) "s" else "" + } • ${ formatBytes( - totalFileSize + totalFileSize, ) }", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -88,18 +91,20 @@ fun FileSelectionPhase( SendButton( onClick = onAddFiles, variant = ButtonVariant.Secondary, - size = ButtonSize.Medium + size = ButtonSize.Medium, ) { Icon( TablerIcons.Plus, contentDescription = null, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(18.dp), ) Spacer(modifier = Modifier.width(8.dp)) Text( - "Add Files", style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Medium - ) + "Add Files", + style = + MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Medium, + ), ) } } @@ -110,16 +115,18 @@ fun FileSelectionPhase( SendEmptyState( title = "No Files Selected", description = "Tap 'Add Files' to choose files you want to send.", - icon = TablerIcons.FileText + icon = TablerIcons.FileText, ) } else { LazyColumn( modifier = Modifier.heightIn(max = 300.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.spacedBy(12.dp), ) { items(selectedFiles) { uri -> SendFileItem( - uri = uri, onRemove = { onRemoveFile(uri) }) + uri = uri, + onRemove = { onRemoveFile(uri) }, + ) } } } @@ -134,17 +141,24 @@ fun FileSelectionPhase( variant = ButtonVariant.Primary, size = ButtonSize.Large, enabled = canStartTransfer, - modifier = Modifier - .fillMaxWidth() - .height(56.dp) + modifier = + Modifier + .fillMaxWidth() + .height(56.dp), ) { Text( - text = when { - selectedFiles.isEmpty() -> "Select Files First" - else -> "Send ${selectedFiles.size} File${if (selectedFiles.size != 1) "s" else ""}" - }, style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold - ) + text = + when { + selectedFiles.isEmpty() -> "Select Files First" + else -> + "Send ${selectedFiles.size} File${ + if (selectedFiles.size != 1) "s" else "" + }" + }, + style = + MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), ) } } @@ -154,4 +168,4 @@ fun FileSelectionPhase( SendInstructionsCard() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/GeneratingQRPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/GeneratingQRPhase.kt index 5abab93..6ec188a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/GeneratingQRPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/GeneratingQRPhase.kt @@ -18,29 +18,30 @@ import dev.arkbuilders.drop.app.presentation.send.components.SendCard import dev.arkbuilders.drop.app.presentation.send.components.SendLoadingIndicator @Composable -fun GeneratingQRPhase( - onCancel: () -> Unit -) { +fun GeneratingQRPhase(onCancel: () -> Unit) { Box( - modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, ) { SendCard { Column( modifier = Modifier.padding(40.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { SendLoadingIndicator( - message = "Generating QR Code..." + message = "Generating QR Code...", ) Spacer(modifier = Modifier.height(32.dp)) SendButton( - onClick = onCancel, variant = ButtonVariant.Secondary, size = ButtonSize.Medium + onClick = onCancel, + variant = ButtonVariant.Secondary, + size = ButtonSize.Medium, ) { Text("Cancel") } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt index 352164b..3268e6c 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt @@ -38,104 +38,112 @@ fun TransferCompletePhase( val haptic = LocalHapticFeedback.current Box( - modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, ) { Column( modifier = Modifier.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(24.dp) + verticalArrangement = Arrangement.spacedBy(24.dp), ) { SendCard( backgroundColor = MaterialTheme.colorScheme.primaryContainer, - modifier = Modifier + modifier = Modifier, ) { Column( modifier = Modifier.padding(40.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( Icons.Default.CheckCircle, contentDescription = "Success", modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.height(20.dp)) Text( text = "Transfer Complete!", - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.Bold - ), + style = + MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold, + ), textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onPrimaryContainer + color = MaterialTheme.colorScheme.onPrimaryContainer, ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "$fileCount file${if (fileCount != 1) "s" else ""} sent successfully", + text = "$fileCount file${ + if (fileCount != 1) "s" else "" + } sent successfully", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f), ) } } SendCard( - backgroundColor = MaterialTheme.colorScheme.tertiaryContainer + backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, ) { Column( modifier = Modifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( Icons.Default.CheckCircle, contentDescription = "Complete", modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.height(20.dp)) Text( text = "Files Sent Successfully!", - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold - ), + style = + MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold, + ), textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onTertiaryContainer + color = MaterialTheme.colorScheme.onTertiaryContainer, ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "$fileCount file${if (fileCount != 1) "s" else ""} transferred successfully", + text = "$fileCount file${ + if (fileCount != 1) "s" else "" + } transferred successfully", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f) + color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f), ) Spacer(modifier = Modifier.height(32.dp)) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp) + horizontalArrangement = Arrangement.spacedBy(16.dp), ) { SendButton( onClick = onSendMore, variant = ButtonVariant.Secondary, size = ButtonSize.Large, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) { Icon( Icons.Default.Refresh, contentDescription = null, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(18.dp), ) Spacer(modifier = Modifier.width(8.dp)) Text( - "Send More", fontWeight = FontWeight.Medium + "Send More", + fontWeight = FontWeight.Medium, ) } @@ -143,10 +151,11 @@ fun TransferCompletePhase( onClick = onDone, variant = ButtonVariant.Primary, size = ButtonSize.Large, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) { Text( - "Done", fontWeight = FontWeight.Medium + "Done", + fontWeight = FontWeight.Medium, ) } } @@ -154,4 +163,4 @@ fun TransferCompletePhase( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt index 3556811..803c88b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt @@ -34,45 +34,48 @@ import dev.arkbuilders.drop.app.presentation.send.components.SendProgressBar @Composable fun TransferringPhase( - progress: SendScreenState.Transfer?, onCancel: () -> Unit + progress: SendScreenState.Transfer?, + onCancel: () -> Unit, ) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(20.dp), - verticalArrangement = Arrangement.spacedBy(24.dp) + verticalArrangement = Arrangement.spacedBy(24.dp), ) { item { progress?.let { p -> SendCard( - backgroundColor = MaterialTheme.colorScheme.primaryContainer + backgroundColor = MaterialTheme.colorScheme.primaryContainer, ) { Column( - modifier = Modifier.padding(24.dp) + modifier = Modifier.padding(24.dp), ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = "Sending Files", - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold - ), - color = MaterialTheme.colorScheme.onPrimaryContainer + style = + MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.onPrimaryContainer, ) IconButton( onClick = onCancel, - modifier = Modifier - .size(32.dp) - .clip(CircleShape) + modifier = + Modifier + .size(32.dp) + .clip(CircleShape), ) { Icon( Icons.Default.Close, contentDescription = "Cancel transfer", modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer + tint = MaterialTheme.colorScheme.onPrimaryContainer, ) } } @@ -82,18 +85,21 @@ fun TransferringPhase( Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { AvatarImageWithFallback( avatarB64 = p.receiverAvatar, fallbackText = p.receiverName, - size = 32.dp + size = 32.dp, ) Text( text = "Connected to: ${p.receiverName}", style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) + color = + MaterialTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.8f, + ), ) } } @@ -103,45 +109,58 @@ fun TransferringPhase( Text( text = "Sending: ${p.currentFileName}", - style = MaterialTheme.typography.bodyLarge.copy( - fontWeight = FontWeight.Medium - ), + style = + MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium, + ), color = MaterialTheme.colorScheme.onPrimaryContainer, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) - val progressValue = if (p.totalBytes > 0) { - (p.bytesTransferred.toFloat() / p.totalBytes.toFloat()).coerceIn( - 0f, 1f - ) - } else 0f + val progressValue = + if (p.totalBytes > 0) { + (p.bytesTransferred.toFloat() / p.totalBytes.toFloat()) + .coerceIn( + 0f, + 1f, + ) + } else { + 0f + } Spacer(modifier = Modifier.height(16.dp)) SendProgressBar( - progress = progressValue, modifier = Modifier.fillMaxWidth() + progress = progressValue, + modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(12.dp)) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = "${formatBytes(p.bytesTransferred)} / ${formatBytes(p.totalBytes)}", + text = "${formatBytes( + p.bytesTransferred, + )} / ${formatBytes(p.totalBytes)}", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + color = + MaterialTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.7f, + ), ) if (p.transferSpeedBps > 0) { Text( text = "${formatBytes(p.transferSpeedBps)}/s", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy( - alpha = 0.7f - ) + color = + MaterialTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.7f, + ), ) } } @@ -149,9 +168,14 @@ fun TransferringPhase( if (p.estimatedTimeRemaining > 0) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Time remaining: ${formatDuration(p.estimatedTimeRemaining)}", + text = "Time remaining: ${formatDuration( + p.estimatedTimeRemaining, + )}", style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f) + color = + MaterialTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.6f, + ), ) } } @@ -160,4 +184,4 @@ fun TransferringPhase( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt index 0e8f288..54e2780 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt @@ -26,48 +26,56 @@ import dev.arkbuilders.drop.app.presentation.send.components.SendLoadingIndicato @Composable fun WaitingForReceiverPhase( - fileCount: Int, onCancel: () -> Unit + fileCount: Int, + onCancel: () -> Unit, ) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(20.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(24.dp) + verticalArrangement = Arrangement.spacedBy(24.dp), ) { item { SendCard { Column( modifier = Modifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = "Ready to Send", style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.Bold - ), color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center + text = "Ready to Send", + style = + MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "$fileCount file${if (fileCount != 1) "s" else ""} ready for transfer", + text = + "$fileCount file${if (fileCount != 1) "s" else ""}" + + " ready for transfer", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(32.dp)) Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { SendLoadingIndicator() Text( text = "Waiting for receiver to scan...", - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium - ), - color = MaterialTheme.colorScheme.primary + style = + MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.primary, ) } } @@ -79,14 +87,16 @@ fun WaitingForReceiverPhase( onClick = onCancel, variant = ButtonVariant.Secondary, size = ButtonSize.Large, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) { Text( - "Cancel Transfer", style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Medium - ) + "Cancel Transfer", + style = + MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Medium, + ), ) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/DesignTokens.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/DesignTokens.kt index a0dbd4b..9812f9d 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/DesignTokens.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/DesignTokens.kt @@ -7,56 +7,55 @@ import androidx.compose.ui.unit.dp * Following Material Design 3 and Apple HIG principles */ object DesignTokens { - // Spacing Scale - 8pt grid system object Spacing { - val xs = 4.dp // Micro spacing - val sm = 8.dp // Small spacing - val md = 12.dp // Medium spacing - val lg = 16.dp // Large spacing - val xl = 24.dp // Extra large spacing - val xxl = 32.dp // Double extra large spacing - val xxxl = 48.dp // Triple extra large spacing - val huge = 64.dp // Huge spacing + val xs = 4.dp // Micro spacing + val sm = 8.dp // Small spacing + val md = 12.dp // Medium spacing + val lg = 16.dp // Large spacing + val xl = 24.dp // Extra large spacing + val xxl = 32.dp // Double extra large spacing + val xxxl = 48.dp // Triple extra large spacing + val huge = 64.dp // Huge spacing } - + // Elevation Scale object Elevation { val none = 0.dp - val xs = 1.dp // Subtle elevation - val sm = 3.dp // Small elevation - val md = 6.dp // Medium elevation - val lg = 8.dp // Large elevation - val xl = 12.dp // Extra large elevation - val xxl = 16.dp // Maximum elevation + val xs = 1.dp // Subtle elevation + val sm = 3.dp // Small elevation + val md = 6.dp // Medium elevation + val lg = 8.dp // Large elevation + val xl = 12.dp // Extra large elevation + val xxl = 16.dp // Maximum elevation } - + // Corner Radius Scale object CornerRadius { - val xs = 4.dp // Small corners - val sm = 8.dp // Medium corners - val md = 12.dp // Default corners - val lg = 16.dp // Large corners - val xl = 20.dp // Extra large corners - val xxl = 24.dp // Maximum corners - val round = 50.dp // Fully rounded (pills) + val xs = 4.dp // Small corners + val sm = 8.dp // Medium corners + val md = 12.dp // Default corners + val lg = 16.dp // Large corners + val xl = 20.dp // Extra large corners + val xxl = 24.dp // Maximum corners + val round = 50.dp // Fully rounded (pills) } - + // Touch Targets object TouchTarget { - val minimum = 48.dp // Minimum touch target size + val minimum = 48.dp // Minimum touch target size val comfortable = 56.dp // Comfortable touch target size - val large = 64.dp // Large touch target size + val large = 64.dp // Large touch target size } - + // Animation Durations object Animation { - const val fast = 150 - const val normal = 300 - const val slow = 500 - const val extraSlow = 800 + const val FAST = 150 + const val NORMAL = 300 + const val SLOW = 500 + const val EXTRA_SLOW = 800 } - + // Content Width Constraints object Layout { val maxContentWidth = 600.dp diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt index c9cecde..24c2cca 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt @@ -16,106 +16,100 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat -private val DarkColorScheme = darkColorScheme( - primary = Blue80, - onPrimary = Grey10, - primaryContainer = Blue40, - onPrimaryContainer = Grey10, - - secondary = Teal80, - onSecondary = Grey10, - secondaryContainer = Teal40, - onSecondaryContainer = Grey10, - - tertiary = BlueGrey80, - onTertiary = Grey10, - tertiaryContainer = BlueGrey40, - onTertiaryContainer = Grey10, - - error = Red80, - onError = Grey10, - errorContainer = Red40, - onErrorContainer = Grey10, - - background = Grey95, - onBackground = Grey10, - surface = SurfaceDark, - onSurface = Grey10, - surfaceVariant = SurfaceVariantDark, - onSurfaceVariant = Grey30, - - outline = Grey60, - outlineVariant = Grey70, - scrim = Color.Black, - inverseSurface = Grey10, - inverseOnSurface = Grey90, - inversePrimary = Blue40, - surfaceDim = Grey80, - surfaceBright = Grey70, - surfaceContainerLowest = Grey95, - surfaceContainerLow = Grey90, - surfaceContainer = Grey80, - surfaceContainerHigh = Grey70, - surfaceContainerHighest = Grey60 -) - -private val LightColorScheme = lightColorScheme( - primary = Blue40, - onPrimary = Color.White, - primaryContainer = Blue80, - onPrimaryContainer = Grey90, - - secondary = Teal40, - onSecondary = Color.White, - secondaryContainer = Teal80, - onSecondaryContainer = Grey90, - - tertiary = BlueGrey40, - onTertiary = Color.White, - tertiaryContainer = BlueGrey80, - onTertiaryContainer = Grey90, - - error = Red40, - onError = Color.White, - errorContainer = Red80, - onErrorContainer = Grey90, - - background = Grey10, - onBackground = Grey90, - surface = SurfaceLight, - onSurface = Grey90, - surfaceVariant = SurfaceVariantLight, - onSurfaceVariant = Grey70, +private val DarkColorScheme = + darkColorScheme( + primary = Blue80, + onPrimary = Grey10, + primaryContainer = Blue40, + onPrimaryContainer = Grey10, + secondary = Teal80, + onSecondary = Grey10, + secondaryContainer = Teal40, + onSecondaryContainer = Grey10, + tertiary = BlueGrey80, + onTertiary = Grey10, + tertiaryContainer = BlueGrey40, + onTertiaryContainer = Grey10, + error = Red80, + onError = Grey10, + errorContainer = Red40, + onErrorContainer = Grey10, + background = Grey95, + onBackground = Grey10, + surface = SurfaceDark, + onSurface = Grey10, + surfaceVariant = SurfaceVariantDark, + onSurfaceVariant = Grey30, + outline = Grey60, + outlineVariant = Grey70, + scrim = Color.Black, + inverseSurface = Grey10, + inverseOnSurface = Grey90, + inversePrimary = Blue40, + surfaceDim = Grey80, + surfaceBright = Grey70, + surfaceContainerLowest = Grey95, + surfaceContainerLow = Grey90, + surfaceContainer = Grey80, + surfaceContainerHigh = Grey70, + surfaceContainerHighest = Grey60, + ) - outline = Grey60, - outlineVariant = Grey40, - scrim = Color.Black, - inverseSurface = Grey90, - inverseOnSurface = Grey10, - inversePrimary = Blue80, - surfaceDim = Grey30, - surfaceBright = Color.White, - surfaceContainerLowest = Color.White, - surfaceContainerLow = Grey20, - surfaceContainer = Grey30, - surfaceContainerHigh = Grey40, - surfaceContainerHighest = Grey50 -) +private val LightColorScheme = + lightColorScheme( + primary = Blue40, + onPrimary = Color.White, + primaryContainer = Blue80, + onPrimaryContainer = Grey90, + secondary = Teal40, + onSecondary = Color.White, + secondaryContainer = Teal80, + onSecondaryContainer = Grey90, + tertiary = BlueGrey40, + onTertiary = Color.White, + tertiaryContainer = BlueGrey80, + onTertiaryContainer = Grey90, + error = Red40, + onError = Color.White, + errorContainer = Red80, + onErrorContainer = Grey90, + background = Grey10, + onBackground = Grey90, + surface = SurfaceLight, + onSurface = Grey90, + surfaceVariant = SurfaceVariantLight, + onSurfaceVariant = Grey70, + outline = Grey60, + outlineVariant = Grey40, + scrim = Color.Black, + inverseSurface = Grey90, + inverseOnSurface = Grey10, + inversePrimary = Blue80, + surfaceDim = Grey30, + surfaceBright = Color.White, + surfaceContainerLowest = Color.White, + surfaceContainerLow = Grey20, + surfaceContainer = Grey30, + surfaceContainerHigh = Grey40, + surfaceContainerHighest = Grey50, + ) @Composable fun DropTheme( darkTheme: Boolean = isSystemInDarkTheme(), - dynamicColor: Boolean = false, // Disabled for consistent branding - content: @Composable () -> Unit + // Disabled for consistent branding + dynamicColor: Boolean = false, + content: @Composable () -> Unit, ) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + val colorScheme = + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } val view = LocalView.current if (!view.isInEditMode) { @@ -129,6 +123,6 @@ fun DropTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/TopBar.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/TopBar.kt index 468f4d9..796c36b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/TopBar.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/TopBar.kt @@ -40,12 +40,16 @@ fun TopBar(modifier: Modifier = Modifier) { ) } IconButton( - modifier = innerModifier, onClick = {}, enabled = false, colors = IconButtonColors( - containerColor = Color.LightGray, - disabledContainerColor = Color.LightGray, - contentColor = Color.Gray, - disabledContentColor = Color.Gray, - ) + modifier = innerModifier, + onClick = {}, + enabled = false, + colors = + IconButtonColors( + containerColor = Color.LightGray, + disabledContainerColor = Color.LightGray, + contentColor = Color.Gray, + disabledContentColor = Color.Gray, + ), ) { Icon(imageVector = Icons.Rounded.Person, contentDescription = null) } @@ -58,4 +62,4 @@ fun TopBar(modifier: Modifier = Modifier) { @Composable fun AppBarPreview() { TopBar() -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Type.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Type.kt index 9789499..e27f006 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Type.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Type.kt @@ -6,119 +6,131 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp -val Typography = Typography( - // Display styles - displayLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 57.sp, - lineHeight = 64.sp, - letterSpacing = (-0.25).sp - ), - displayMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 45.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp - ), - displaySmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp - ), - - // Headline styles - headlineLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.SemiBold, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp - ), - headlineMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.SemiBold, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp - ), - headlineSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.sp - ), - - // Title styles - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - titleMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - titleSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - - // Body styles - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ), - bodyMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp - ), - bodySmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp - ), - - // Label styles - labelLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - labelMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp +val Typography = + Typography( + // Display styles + displayLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp, + ), + displayMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp, + ), + displaySmall = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp, + ), + // Headline styles + headlineLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + ), + headlineMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp, + ), + headlineSmall = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp, + ), + // Title styles + titleLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp, + ), + titleMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + ), + titleSmall = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + // Body styles + bodyLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), + bodyMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + bodySmall = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + ), + // Label styles + labelLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + labelMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), + labelSmall = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), ) -) diff --git a/app/src/test/java/dev/arkbuilders/drop/app/ExampleUnitTest.kt b/app/src/test/java/dev/arkbuilders/drop/app/ExampleUnitTest.kt index b5c2b78..b20fbf3 100644 --- a/app/src/test/java/dev/arkbuilders/drop/app/ExampleUnitTest.kt +++ b/app/src/test/java/dev/arkbuilders/drop/app/ExampleUnitTest.kt @@ -13,4 +13,4 @@ class ExampleUnitTest { fun addition_isCorrect() { Assert.assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/build.gradle.kts b/build.gradle.kts index a9531fd..07b8f85 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,6 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.ksp) - // alias(libs.plugins.dagger.hilt) apply false alias(libs.plugins.triplet.play) apply false + alias(libs.plugins.ktlint.gradle) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 023f93a..51f06e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ drop = "17348879247" devsrsouzaIcons = "1.1.0" coil = "2.5.0" koin = "4.1.1" +ktlintGradlePlugin = "12.2.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -73,10 +74,6 @@ orbit-viewmodel = { module = "org.orbit-mvi:orbit-viewmodel", version.ref = "orb arkbuilders-drop = { module = "dev.arkbuilders:drop", version.ref = "drop" } -dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } -androidx-hilt-nav-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavCompose" } -dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } - simple-icons = { module = "br.com.devsrsouza.compose.icons:simple-icons", version.ref = "devsrsouzaIcons" } font-awesome = { module = "br.com.devsrsouza.compose.icons:font-awesome", version.ref = "devsrsouzaIcons" } tabler-icons = { module = "br.com.devsrsouza.compose.icons:tabler-icons", version.ref = "devsrsouzaIcons" } @@ -97,3 +94,4 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"} dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref="hilt" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } triplet-play = { id = "com.github.triplet.play", version.ref = "tripletPlay" } +ktlint-gradle = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlintGradlePlugin" } From 720450dceba51b9fa20b03cb92618ac5ca3e1fa8 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Fri, 14 Nov 2025 06:56:15 +0600 Subject: [PATCH 15/33] Extract DropTopBar --- .../app/presentation/components/DropTopBar.kt | 80 +++++++++++++ .../drop/app/presentation/history/History.kt | 26 ++--- .../presentation/profile/EditProfileScreen.kt | 41 +------ .../drop/app/presentation/receive/Receive.kt | 51 +-------- .../drop/app/presentation/send/Send.kt | 107 +----------------- app/src/main/res/drawable/ic_back.xml | 5 + 6 files changed, 107 insertions(+), 203 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropTopBar.kt create mode 100644 app/src/main/res/drawable/ic_back.xml diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropTopBar.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropTopBar.kt new file mode 100644 index 0000000..dd336a0 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropTopBar.kt @@ -0,0 +1,80 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package dev.arkbuilders.drop.app.presentation.components + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import dev.arkbuilders.drop.app.R + +@Composable +fun DropTopBar( + title: String, + leadingIcon: Painter? = null, + onLeadingIconClick: (() -> Unit)? = null, + trailingContent: (@Composable () -> Unit)? = null, + containerColor: Color = MaterialTheme.colorScheme.background, + contentColor: Color = MaterialTheme.colorScheme.onBackground, +) { + TopAppBar( + title = { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + ) + }, + navigationIcon = { + leadingIcon?.let { + IconButton( + onClick = { + onLeadingIconClick?.invoke() + }, + ) { + Icon( + painter = leadingIcon, + contentDescription = null, + ) + } + } + }, + actions = { + trailingContent?.let { + it() + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = containerColor, + titleContentColor = contentColor, + actionIconContentColor = contentColor, + ), + ) +} + +@Composable +fun DropTopBarBack( + title: String, + onBackClick: (() -> Unit)? = null, + trailingContent: (@Composable () -> Unit)? = null, + containerColor: Color = MaterialTheme.colorScheme.background, + contentColor: Color = MaterialTheme.colorScheme.onBackground, +) { + DropTopBar( + title = title, + leadingIcon = painterResource(R.drawable.ic_back), + onLeadingIconClick = onBackClick, + trailingContent = trailingContent, + containerColor = containerColor, + contentColor = contentColor, + ) +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt index 5239f9a..3d17589 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -48,6 +47,7 @@ import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import dev.arkbuilders.drop.app.presentation.components.AvatarImageWithFallback +import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState import java.time.Duration @@ -72,22 +72,10 @@ fun History( .fillMaxSize() .padding(16.dp), ) { - // Top bar - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton(onClick = { navController.navigateUp() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - Text( - text = "Transfer History", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.weight(1f), - ) - - if (state.historyItems.isNotEmpty()) { + DropTopBarBack( + title = "Transfer history", + onBackClick = { navController.navigateUp() }, + trailingContent = { IconButton(onClick = { viewModel.onShowClearDialog() }) { Icon( TablerIcons.ClearAll, @@ -95,8 +83,8 @@ fun History( tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - } - } + }, + ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt index 63d7698..dea4674 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt @@ -34,7 +34,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Edit @@ -50,8 +49,6 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -88,6 +85,7 @@ import dev.arkbuilders.drop.app.presentation.components.DropCard import dev.arkbuilders.drop.app.presentation.components.DropCardContent import dev.arkbuilders.drop.app.presentation.components.DropCardSize import dev.arkbuilders.drop.app.presentation.components.DropCardVariant +import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack import dev.arkbuilders.drop.app.presentation.components.ErrorState import dev.arkbuilders.drop.app.presentation.components.ErrorStateDisplay import dev.arkbuilders.drop.app.presentation.components.ErrorType @@ -135,34 +133,12 @@ fun EditProfileEnhanced(navController: NavController) { .windowInsetsPadding(WindowInsets.ime) .verticalScroll(rememberScrollState()), ) { - // Enhanced Top App Bar - TopAppBar( - title = { - Text( - text = "Edit Profile", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.SemiBold, - ) - }, - navigationIcon = { - IconButton( - onClick = { - navController.navigateUp() - }, - modifier = - Modifier.semantics { - contentDescription = "Go back to previous screen" - }, - ) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null, - tint = colorScheme.onSurface, - ) - } - }, - actions = { + DropTopBarBack( + title = "Edit Profile", + onBackClick = { navController.navigateUp() }, + trailingContent = { AnimatedVisibility( + modifier = Modifier.padding(end = DesignTokens.Spacing.lg), visible = state.hasChanges, enter = scaleIn(spring(stiffness = Spring.StiffnessHigh)) + fadeIn(), exit = scaleOut(spring(stiffness = Spring.StiffnessHigh)) + fadeOut(), @@ -181,11 +157,6 @@ fun EditProfileEnhanced(navController: NavController) { } } }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = colorScheme.surface, - titleContentColor = colorScheme.onSurface, - ), ) AnimatedVisibility( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt index e78af51..841314b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -34,7 +33,6 @@ import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -50,13 +48,12 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi -import dev.arkbuilders.drop.app.R +import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveCompleteCard import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveErrorCard import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveLoadingCard @@ -161,48 +158,10 @@ fun Receive(navController: NavController) { .fillMaxSize() .padding(DesignTokens.Spacing.lg), ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Surface( - onClick = { navController.navigateUp() }, - shape = CircleShape, - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f), - modifier = Modifier.Companion.size(DesignTokens.TouchTarget.minimum), - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize(), - ) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - } - } - - Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) - - Icon( - modifier = Modifier.size(32.dp), - painter = painterResource(R.drawable.ic_logo), - contentDescription = null, - tint = Color.Unspecified, - ) - - Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) - - Text( - text = "Receive Files", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.onSurface, - ) - } + DropTopBarBack( + title = "Receive files", + onBackClick = { navController.navigateUp() }, + ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt index 28e10a4..fa93dc8 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt @@ -6,8 +6,6 @@ import android.content.Context import android.graphics.Bitmap import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -25,22 +23,17 @@ import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -50,29 +43,23 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.AlertCircle -import compose.icons.tablericons.CloudUpload import compose.icons.tablericons.Copy import compose.icons.tablericons.Qrcode -import dev.arkbuilders.drop.app.R import dev.arkbuilders.drop.app.data.repository.TransferManager +import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant import dev.arkbuilders.drop.app.presentation.send.components.SendButton @@ -207,11 +194,9 @@ fun Send( .windowInsetsPadding(WindowInsets.navigationBars) .windowInsetsPadding(WindowInsets.ime), topBar = { - SendTopBar( - onBackClick = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - navController.navigateUp() - }, + DropTopBarBack( + title = "Send files", + onBackClick = { navController.navigateUp() }, ) }, containerColor = MaterialTheme.colorScheme.background, @@ -308,90 +293,6 @@ fun Send( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun SendTopBar(onBackClick: () -> Unit) { - TopAppBar( - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(R.drawable.ic_logo), - contentDescription = null, - tint = Color.Unspecified, - ) - Text( - text = "Send Files", - style = - MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 20.sp, - ), - color = MaterialTheme.colorScheme.onSurface, - ) - } - }, - navigationIcon = { - IconButton( - onClick = onBackClick, - modifier = - Modifier - .size(44.dp) - .clip(CircleShape) - .semantics { contentDescription = "Go back" }, - ) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(24.dp), - ) - } - }, - actions = { - // Network status indicator -// NetworkStatusIndicator( -// connected = networkConnected, modifier = Modifier.padding(end = 8.dp) -// ) - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f), - titleContentColor = MaterialTheme.colorScheme.onSurface, - ), - ) -} - -@Composable -private fun NetworkStatusIndicator( - connected: Boolean, - modifier: Modifier = Modifier, -) { - val alpha by animateFloatAsState( - targetValue = if (connected) 0.7f else 1f, - animationSpec = tween(300), - label = "networkAlpha", - ) - - Icon( - imageVector = if (connected) TablerIcons.CloudUpload else Icons.Default.Warning, - contentDescription = if (connected) "Network connected" else "Network disconnected", - modifier = - modifier - .size(20.dp) - .alpha(alpha), - tint = - if (connected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.error - }, - ) -} - private fun copyToClipboard( context: Context, text: String, diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..075e95d --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,5 @@ + + + + + From 3d37da4a48ed4eec824b498a7ab999eab20ee3b1 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Fri, 14 Nov 2025 17:57:21 +0600 Subject: [PATCH 16/33] Apply safeDrawingPadding in root --- .../dev/arkbuilders/drop/app/presentation/MainActivity.kt | 4 +++- .../dev/arkbuilders/drop/app/presentation/theme/Theme.kt | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt index f92eab7..f010ad4 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt @@ -6,6 +6,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -41,7 +42,8 @@ class MainActivity : ComponentActivity() { Scaffold( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .safeDrawingPadding(), ) { innerPadding -> DropNavigation( modifier = diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt index 24c2cca..a11559c 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/theme/Theme.kt @@ -115,8 +115,11 @@ fun DropTheme( if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window - window.statusBarColor = colorScheme.surface.toArgb() + window.statusBarColor = colorScheme.background.toArgb() + window.navigationBarColor = colorScheme.background.toArgb() WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme + WindowCompat.getInsetsController(window, view) + .isAppearanceLightNavigationBars = !darkTheme } } From 8cc703d86c1b3b638889bd42f9ff1a831ef52904 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Fri, 14 Nov 2025 19:05:44 +0600 Subject: [PATCH 17/33] Fix paddings in Receive screen --- .../dev/arkbuilders/drop/app/presentation/receive/Receive.kt | 4 ++-- .../presentation/receive/components/ReceiveCompleteCard.kt | 2 +- .../app/presentation/receive/components/ReceiveErrorCard.kt | 2 +- .../app/presentation/receive/components/ReceiveLoadingCard.kt | 2 +- .../presentation/receive/components/ReceiveManualInputCard.kt | 2 +- .../receive/components/ReceivePermissionRequestCard.kt | 2 +- .../presentation/receive/components/ReceiveProgressCard.kt | 2 +- .../receive/components/ReceiveQRCodeScannedCard.kt | 2 +- .../presentation/receive/components/ReceiveReadyToScanCard.kt | 2 +- .../presentation/receive/components/ReceiveScanningCard.kt | 3 ++- 10 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt index 841314b..5d381fc 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt @@ -155,8 +155,7 @@ fun Receive(navController: NavController) { Column( modifier = Modifier - .fillMaxSize() - .padding(DesignTokens.Spacing.lg), + .fillMaxSize(), ) { DropTopBarBack( title = "Receive files", @@ -370,6 +369,7 @@ fun Receive(navController: NavController) { Spacer(modifier = Modifier.weight(1f)) Card( + modifier = Modifier.padding(16.dp), colors = CardDefaults.cardColors( containerColor = diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt index 910bf32..008e54f 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveCompleteCard.kt @@ -40,7 +40,7 @@ fun ReceiveCompleteCard( onDone: () -> Unit, ) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), colors = CardDefaults.elevatedCardColors( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt index 8a6282d..bfc7a5b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt @@ -40,7 +40,7 @@ fun ReceiveErrorCard( onDismiss: () -> Unit, ) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), colors = CardDefaults.elevatedCardColors( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt index a9f10b8..88e4647 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveLoadingCard.kt @@ -23,7 +23,7 @@ import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable fun ReceiveLoadingCard(message: String) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), colors = CardDefaults.elevatedCardColors( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt index 691bf66..56acdb6 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveManualInputCard.kt @@ -45,7 +45,7 @@ fun ReceiveManualInputCard( onCancel: () -> Unit, ) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), colors = CardDefaults.elevatedCardColors( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt index 5ef8ca9..e9a31a3 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt @@ -36,7 +36,7 @@ fun ReceivePermissionRequestCard( onEnterManually: () -> Unit, ) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), colors = CardDefaults.elevatedCardColors( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt index 85e8020..8f87916 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveProgressCard.kt @@ -46,7 +46,7 @@ fun ReceiveProgressCard( onCancel: () -> Unit, ) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), colors = CardDefaults.elevatedCardColors( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt index c0008fe..66e6793 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveQRCodeScannedCard.kt @@ -36,7 +36,7 @@ fun ReceiveQRCodeScannedCard( onScanAgain: () -> Unit, ) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), colors = CardDefaults.elevatedCardColors( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt index a2e5d31..ed187c4 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt @@ -38,7 +38,7 @@ fun ReceiveReadyToScanCard( onEnterManually: () -> Unit, ) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), colors = CardDefaults.elevatedCardColors( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt index 4d5c1a1..107bbc1 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveScanningCard.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape @@ -57,7 +58,7 @@ fun ReceiveScanningCard( onStopScanning: () -> Unit, onEnterManually: () -> Unit, ) { - Column { + Column(modifier = Modifier.padding(16.dp)) { ElevatedCard( modifier = Modifier From ce1265365970625253fb3a98e614b570beb9e8de Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Sun, 16 Nov 2025 04:17:27 +0600 Subject: [PATCH 18/33] Add session models and split TransferManager into send/receive repos --- .../app/data/helper/ResourcesHelperImpl.kt | 86 ++++ .../app/data/repository/ReceiveSessionRepo.kt | 125 +++++ .../app/data/repository/SendSessionRepo.kt | 102 ++++ .../app/data/repository/TransferManager.kt | 445 ------------------ .../dev/arkbuilders/drop/app/di/AppModule.kt | 6 +- .../drop/app/domain/ResourcesHelper.kt | 5 + .../drop/app/domain/model/ReceiveSession.kt | 9 + .../drop/app/domain/model/SendSession.kt | 9 + .../drop/app/presentation/MainActivity.kt | 6 - .../presentation/receive/ReceiveViewModel.kt | 91 ++-- .../drop/app/presentation/send/Send.kt | 153 +++--- .../app/presentation/send/SendViewModel.kt | 62 ++- 12 files changed, 505 insertions(+), 594 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/model/ReceiveSession.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/model/SendSession.kt diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt index eef83ed..a8d7f0a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt @@ -1,10 +1,15 @@ package dev.arkbuilders.drop.app.data.helper +import android.content.ContentValues import android.content.Context +import android.os.Environment +import android.provider.MediaStore import android.provider.OpenableColumns +import android.webkit.MimeTypeMap import androidx.core.net.toUri import dev.arkbuilders.drop.app.domain.ResourcesHelper import timber.log.Timber +import java.net.URLConnection class ResourcesHelperImpl( private val context: Context, @@ -62,4 +67,85 @@ class ResourcesHelperImpl( 0L } } + + override fun saveFileToDownloads( + fileName: String, + data: ByteArray, + ): String? { + val resolver = context.contentResolver + + val uniqueName = getUniqueFileName(fileName) + val mime = getMimeType(uniqueName) + + val values = + ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, uniqueName) + put(MediaStore.MediaColumns.MIME_TYPE, mime) + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + + val uri = + resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + ?: return null + + resolver.openOutputStream(uri)?.use { it.write(data) } + + return uniqueName + } + + private fun getUniqueFileName(originalName: String): String { + val resolver = context.contentResolver + + val baseName = originalName.substringBeforeLast(".") + val ext = originalName.substringAfterLast(".", "") + var candidateName = originalName + var attempt = 1 + + while (true) { + val values = + ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, candidateName) + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + + val testUri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + + if (testUri != null) { + resolver.delete(testUri, null, null) + return candidateName + } + + candidateName = + if (ext.isNotEmpty()) { + "$baseName($attempt).$ext" + } else { + "$baseName($attempt)" + } + + attempt++ + + if (attempt > 1000) { + val ts = System.currentTimeMillis() + return if (ext.isNotEmpty()) { + "${baseName}_$ts.$ext" + } else { + "${baseName}_$ts" + } + } + } + } + + private fun getMimeType(fileName: String): String { + val ext = fileName.substringAfterLast(".", "").lowercase() + + MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)?.let { + return it + } + + URLConnection.guessContentTypeFromName(fileName)?.let { + return it + } + + return "application/octet-stream" + } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt new file mode 100644 index 0000000..fac391c --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt @@ -0,0 +1,125 @@ +package dev.arkbuilders.drop.app.data.repository + +import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl +import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.app.domain.model.ReceiveSession +import dev.arkbuilders.drop.app.domain.model.TransferStatus +import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import timber.log.Timber + +class ReceiveSessionRepo( + private val receiveFilesUseCase: ReceiveFilesUseCase, + private val transferHistoryRepository: TransferHistoryItemRepository, + private val resourcesHelper: ResourcesHelper, +) { + // Keep references to active sessions here so file transfers continue even if the ViewModel dies + private val activeSessions = mutableListOf() + private val activeSessionsMutex = Mutex() + private val cancelScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + suspend fun receiveFiles( + ticket: String, + confirmation: UByte, + ): ReceiveSession? = + withContext(Dispatchers.IO) { + receiveFilesUseCase.invoke(ticket, confirmation).fold( + onSuccess = { bubble -> + val subscriber = + ReceiveFilesSubscriberImpl().also { subscriber -> + bubble.subscribe(subscriber) + } + + val session = + ReceiveSession( + bubble, + subscriber, + ) + activeSessionsMutex.withLock { + activeSessions.add(session) + } + + bubble.start() + return@withContext session + }, + onFailure = { + return@withContext null + }, + ) + } + + suspend fun saveReceivedFiles(session: ReceiveSession): List = + withContext(Dispatchers.IO) { + val subscriber = session.subscriber + val completeFiles = subscriber.getCompleteFiles() + val savedFiles = mutableListOf() + + try { + completeFiles.forEach { (fileInfo, data) -> + val savedFile = resourcesHelper.saveFileToDownloads(fileInfo.name, data) + if (savedFile != null) { + savedFiles.add(savedFile) + Timber.i("Saved file name: $savedFile") + } else { + Timber.e("Failed to save file: ${fileInfo.name}") + } + } + + if (savedFiles.isNotEmpty()) { + val progress = subscriber.progress.value + val senderName = progress.senderName + val senderAvatar = progress.senderAvatar + + val totalSize = completeFiles.sumOf { it.second.size.toLong() } + val firstFileName = savedFiles.firstOrNull() ?: "Unknown" + + transferHistoryRepository.addReceivedTransfer( + fileName = firstFileName, + fileSize = totalSize, + peerName = senderName, + peerAvatar = senderAvatar, + fileCount = savedFiles.size, + status = TransferStatus.COMPLETED, + ) + } + } catch (e: Exception) { + Timber.e("Error saving received files ${e.message}") + + val progress = subscriber.progress.value + val senderName = progress.senderName + val senderAvatar = progress.senderAvatar + + transferHistoryRepository.addReceivedTransfer( + fileName = "Transfer failed", + fileSize = 0L, + peerName = senderName, + peerAvatar = senderAvatar, + fileCount = completeFiles.size, + status = TransferStatus.FAILED, + ) + } + + savedFiles + } + + fun cancelReceive(session: ReceiveSession) { + cancelScope.launch { + try { + activeSessionsMutex.withLock { + activeSessions.remove(session) + } + session.bubble.unsubscribe(session.subscriber) + session.bubble.cancel() + } catch (e: Throwable) { + Timber.e("Error cancelling receive ${e.message}") + } + } + } +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt new file mode 100644 index 0000000..5608ec9 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt @@ -0,0 +1,102 @@ +package dev.arkbuilders.drop.app.data.repository + +import android.net.Uri +import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl +import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.app.domain.model.SendSession +import dev.arkbuilders.drop.app.domain.model.TransferStatus +import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import timber.log.Timber + +class SendSessionRepo( + private val sendUseCase: SendFilesUseCase, + private val resourcesHelper: ResourcesHelper, + private val transferHistoryRepository: TransferHistoryItemRepository, +) { + // Keep references to active sessions here so file transfers continue even if the ViewModel dies + private val activeSessions = mutableListOf() + private val activeSessionsMutex = Mutex() + private val cancelScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + suspend fun sendFiles(fileUris: List): SendSession? = + withContext(Dispatchers.IO) { + cleanupFinishedSessions() + + sendUseCase.invoke(fileUris).fold( + onSuccess = { bubble -> + val subscriber = + SendFilesSubscriberImpl().also { subscriber -> + bubble.subscribe(subscriber) + } + + val session = SendSession(bubble, subscriber) + activeSessionsMutex.withLock { + activeSessions.add(session) + } + return@withContext session + }, + onFailure = { + return@withContext null + }, + ) + } + + suspend fun recordSendCompletion( + fileUris: List, + session: SendSession, + ) { + try { + cleanupFinishedSessions() + val progress = session.subscriber.progress.value + val receiverName = progress.receiverName + val receiverAvatar = progress.receiverAvatar + + val totalSize = + fileUris.sumOf { uri -> + resourcesHelper.getFileSize(uri.toString()) + } + + val firstFileName = + fileUris.firstOrNull()?.let { resourcesHelper.getFileName(it.toString()) } + ?: "Unknown" + + transferHistoryRepository.addSentTransfer( + fileName = firstFileName, + fileSize = totalSize, + peerName = receiverName, + peerAvatar = receiverAvatar, + fileCount = fileUris.size, + status = TransferStatus.COMPLETED, + ) + } catch (e: Exception) { + Timber.e("Error recording send completion ${e.message}") + } + } + + fun cancelSend(session: SendSession) { + cancelScope.launch { + try { + activeSessionsMutex.withLock { + activeSessions.remove(session) + } + session.bubble.unsubscribe(session.subscriber) + session.bubble.cancel() + } catch (e: Throwable) { + Timber.e("Error cancelling send ${e.message}") + } + } + } + + private suspend fun cleanupFinishedSessions() = + activeSessionsMutex.withLock { + activeSessions.removeAll { it.bubble.isFinished() } + } +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt deleted file mode 100644 index f6a780d..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferManager.kt +++ /dev/null @@ -1,445 +0,0 @@ -package dev.arkbuilders.drop.app.data.repository - -import android.content.ContentValues -import android.content.Context -import android.net.Uri -import android.os.Environment -import android.provider.MediaStore -import android.provider.OpenableColumns -import android.util.Log -import dev.arkbuilders.drop.ReceiveFilesBubble -import dev.arkbuilders.drop.SendFilesBubble -import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl -import dev.arkbuilders.drop.app.data.ReceivingProgress -import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl -import dev.arkbuilders.drop.app.data.SendingProgress -import dev.arkbuilders.drop.app.domain.model.TransferStatus -import dev.arkbuilders.drop.app.domain.repository.ProfileRepo -import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository -import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase -import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.withContext -import org.koin.core.component.KoinComponent -import org.koin.core.component.get -import java.io.File -import java.io.FileOutputStream - -class TransferManager( - private val context: Context, - private val profileRepo: ProfileRepo, - private val transferHistoryRepository: TransferHistoryItemRepository, -) : KoinComponent { - companion object { - private const val TAG = "TransferManager" - } - - private var currentSendBubble: SendFilesBubble? = null - private var currentReceiveBubble: ReceiveFilesBubble? = null - private var sendSubscriber: SendFilesSubscriberImpl? = null - private var receiveSubscriber: ReceiveFilesSubscriberImpl? = null - - val sendProgress: StateFlow? - get() = sendSubscriber?.progress - - val receiveProgress: StateFlow? - get() = receiveSubscriber?.progress - - suspend fun sendFiles(fileUris: List): SendFilesBubble? = - withContext(Dispatchers.IO) { - val sendUseCase: SendFilesUseCase = get() - - sendUseCase.invoke(fileUris).fold( - onSuccess = { bubble -> - currentSendBubble = bubble - sendSubscriber = - SendFilesSubscriberImpl().also { subscriber -> - bubble.subscribe(subscriber) - } - return@withContext bubble - }, - onFailure = { - return@withContext null - }, - ) - } - - suspend fun receiveFiles( - ticket: String, - confirmation: UByte, - ): ReceiveFilesBubble? = - withContext(Dispatchers.IO) { - val receiveFilesUseCase: ReceiveFilesUseCase = get() - - receiveFilesUseCase.invoke(ticket, confirmation).fold( - onSuccess = { bubble -> - currentReceiveBubble = bubble - receiveSubscriber = - ReceiveFilesSubscriberImpl().also { subscriber -> - bubble.subscribe(subscriber) - } - bubble.start() - return@withContext bubble - }, - onFailure = { - return@withContext null - }, - ) - } - - suspend fun saveReceivedFiles(): List = - withContext(Dispatchers.IO) { - val subscriber = receiveSubscriber ?: return@withContext emptyList() - val completeFiles = subscriber.getCompleteFiles() - val savedFiles = mutableListOf() - - try { - completeFiles.forEach { (fileInfo, data) -> - val savedFile = saveFileToDownloads(fileInfo.name, data) - if (savedFile != null) { - savedFiles.add(savedFile) - Log.d(TAG, "Saved file: ${savedFile.absolutePath}") - } else { - Log.e(TAG, "Failed to save file: ${fileInfo.name}") - } - } - - // Add to history if files were saved successfully - if (savedFiles.isNotEmpty()) { - val progress = receiveSubscriber?.progress?.value - val senderName = progress?.senderName ?: "Unknown" - val senderAvatar = progress?.senderAvatar - - val totalSize = completeFiles.sumOf { it.second.size.toLong() } - val firstFileName = savedFiles.firstOrNull()?.name ?: "Unknown" - - transferHistoryRepository.addReceivedTransfer( - fileName = firstFileName, - fileSize = totalSize, - peerName = senderName, - peerAvatar = senderAvatar, - fileCount = savedFiles.size, - status = TransferStatus.COMPLETED, - ) - } - } catch (e: Exception) { - Log.e(TAG, "Error saving received files", e) - - // Add failed transfer to history - val progress = receiveSubscriber?.progress?.value - val senderName = progress?.senderName ?: "Unknown" - val senderAvatar = progress?.senderAvatar - - transferHistoryRepository.addReceivedTransfer( - fileName = "Transfer failed", - fileSize = 0L, - peerName = senderName, - peerAvatar = senderAvatar, - fileCount = completeFiles.size, - status = TransferStatus.FAILED, - ) - } - - savedFiles - } - - suspend fun recordSendCompletion(fileUris: List) { - try { - val progress = sendSubscriber?.progress?.value - val receiverName = progress?.receiverName ?: "Unknown" - val receiverAvatar = progress?.receiverAvatar - - val totalSize = - fileUris.sumOf { uri -> - try { - context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - if (sizeIndex >= 0) cursor.getLong(sizeIndex) else 0L - } else { - 0L - } - } ?: 0L - } catch (_: Exception) { - 0L - } - } - - val firstFileName = getFileName(fileUris.firstOrNull()) ?: "Unknown" - - transferHistoryRepository.addSentTransfer( - fileName = firstFileName, - fileSize = totalSize, - peerName = receiverName, - peerAvatar = receiverAvatar, - fileCount = fileUris.size, - status = TransferStatus.COMPLETED, - ) - } catch (e: Exception) { - Log.e(TAG, "Error recording send completion", e) - } - } - - private suspend fun saveFileToDownloads( - fileName: String, - data: ByteArray, - ): File? = - withContext(Dispatchers.IO) { - try { - // Use MediaStore for Android 10+ (Scoped Storage) - return@withContext saveFileUsingMediaStore(fileName, data) - } catch (e: Exception) { - Log.e(TAG, "Error saving file: $fileName", e) - return@withContext null - } - } - - private fun saveFileUsingMediaStore( - fileName: String, - data: ByteArray, - ): File? { - try { - val resolver = context.contentResolver - - // Generate unique filename to avoid conflicts - val uniqueFileName = generateUniqueFileName(fileName) - - // Create content values for the file - val contentValues = - ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, uniqueFileName) - put(MediaStore.MediaColumns.MIME_TYPE, getMimeType(uniqueFileName)) - put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) - } - - // Insert the file into MediaStore - val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) - - if (uri != null) { - // Write the file data - resolver.openOutputStream(uri)?.use { outputStream -> - outputStream.write(data) - outputStream.flush() - } - - // Get the actual file path for return - val actualFile = getFileFromMediaStoreUri(uri, uniqueFileName) - Log.d(TAG, "File saved using MediaStore: $uniqueFileName") - return actualFile - } else { - Log.e(TAG, "Failed to create MediaStore entry for: $uniqueFileName") - return null - } - } catch (e: Exception) { - Log.e(TAG, "Error saving file using MediaStore: $fileName", e) - return null - } - } - - private fun saveFileUsingLegacyStorage( - fileName: String, - data: ByteArray, - ): File? { - try { - val downloadsDir = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - if (!downloadsDir.exists()) { - downloadsDir.mkdirs() - } - - // Generate unique filename to avoid conflicts - val uniqueFileName = generateUniqueFileNameForDirectory(downloadsDir, fileName) - val file = File(downloadsDir, uniqueFileName) - - FileOutputStream(file).use { outputStream -> - outputStream.write(data) - outputStream.flush() - } - - Log.d(TAG, "File saved using legacy storage: ${file.absolutePath}") - return file - } catch (e: Exception) { - Log.e(TAG, "Error saving file using legacy storage: $fileName", e) - return null - } - } - - private fun generateUniqueFileName(originalFileName: String): String { - val downloadsDir = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - return generateUniqueFileNameForDirectory(downloadsDir, originalFileName) - } - - private fun generateUniqueFileNameForDirectory( - directory: File, - originalFileName: String, - ): String { - val nameWithoutExt = originalFileName.substringBeforeLast(".", originalFileName) - val extension = originalFileName.substringAfterLast(".", "") - - var counter = 1 - var candidateFileName = originalFileName - var candidateFile = File(directory, candidateFileName) - - // Keep incrementing counter until we find a unique filename - while (candidateFile.exists() || isFileNameInMediaStore(candidateFileName)) { - candidateFileName = - if (extension.isNotEmpty()) { - "$nameWithoutExt($counter).$extension" - } else { - "$nameWithoutExt($counter)" - } - candidateFile = File(directory, candidateFileName) - counter++ - - // Safety check to prevent infinite loop - if (counter > 1000) { - Log.w(TAG, "Too many duplicate files, using timestamp suffix") - val timestamp = System.currentTimeMillis() - candidateFileName = - if (extension.isNotEmpty()) { - "${nameWithoutExt}_$timestamp.$extension" - } else { - "${nameWithoutExt}_$timestamp" - } - break - } - } - - Log.d(TAG, "Generated unique filename: $candidateFileName (original: $originalFileName)") - return candidateFileName - } - - private fun isFileNameInMediaStore(fileName: String): Boolean { - return try { - val resolver = context.contentResolver - val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME) - val selection = - "${MediaStore.MediaColumns.DISPLAY_NAME} = ? AND ${ - MediaStore.MediaColumns.RELATIVE_PATH - } = ?" - val selectionArgs = arrayOf(fileName, "${Environment.DIRECTORY_DOWNLOADS}/") - - resolver.query( - MediaStore.Downloads.EXTERNAL_CONTENT_URI, - projection, - selection, - selectionArgs, - null, - )?.use { cursor -> - cursor.count > 0 - } ?: false - } catch (e: Exception) { - Log.w(TAG, "Error checking MediaStore for filename: $fileName", e) - false - } - } - - private fun getFileFromMediaStoreUri( - uri: Uri, - fileName: String, - ): File { - // For MediaStore files, we create a reference file object - // The actual file is managed by the system - val downloadsDir = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - return File(downloadsDir, fileName) - } - - private fun getMimeType(fileName: String): String { - val extension = fileName.substringAfterLast(".", "").lowercase() - return when (extension) { - "jpg", "jpeg" -> "image/jpeg" - "png" -> "image/png" - "gif" -> "image/gif" - "webp" -> "image/webp" - "pdf" -> "application/pdf" - "txt" -> "text/plain" - "doc" -> "application/msword" - "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - "xls" -> "application/vnd.ms-excel" - "xlsx" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - "ppt" -> "application/vnd.ms-powerpoint" - "pptx" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation" - "zip" -> "application/zip" - "rar" -> "application/x-rar-compressed" - "7z" -> "application/x-7z-compressed" - "mp3" -> "audio/mpeg" - "wav" -> "audio/wav" - "flac" -> "audio/flac" - "mp4" -> "video/mp4" - "avi" -> "video/x-msvideo" - "mkv" -> "video/x-matroska" - "mov" -> "video/quicktime" - else -> "application/octet-stream" - } - } - - fun cancelSend() { - try { - currentSendBubble?.let { bubble -> - sendSubscriber?.let { subscriber -> - bubble.unsubscribe(subscriber) - } - // Note: cancel() is async in the UDL, but we'll call it anyway - // bubble.cancel() // Commented out as it's async and we can't await here - } - } catch (e: Exception) { - Log.e(TAG, "Error cancelling send", e) - } finally { - cleanup() - } - } - - fun cancelReceive() { - try { - currentReceiveBubble?.let { bubble -> - receiveSubscriber?.let { subscriber -> - bubble.unsubscribe(subscriber) - } - bubble.cancel() - } - } catch (e: Exception) { - Log.e(TAG, "Error cancelling receive", e) - } finally { - cleanup() - } - } - - fun getCurrentSendTicket(): String? = currentSendBubble?.getTicket() - - fun getCurrentSendConfirmation(): UByte? = currentSendBubble?.getConfirmation() - - fun isSendFinished(): Boolean = currentSendBubble?.isFinished() ?: true - - fun isReceiveFinished(): Boolean = currentReceiveBubble?.isFinished() ?: true - - fun isSendConnected(): Boolean = currentSendBubble?.isConnected() ?: false - - private fun cleanup() { - sendSubscriber?.reset() - receiveSubscriber?.reset() - sendSubscriber = null - receiveSubscriber = null - currentSendBubble = null - currentReceiveBubble = null - } - - private fun getFileName(uri: Uri?): String? { - if (uri == null) return null - return try { - context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - if (nameIndex >= 0) cursor.getString(nameIndex) else null - } else { - null - } - } - } catch (e: Exception) { - Log.e(TAG, "Error getting filename for URI: $uri", e) - null - } - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index 312baee..8ce6693 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -9,8 +9,9 @@ import dev.arkbuilders.drop.app.data.helper.PermissionsHelperImpl import dev.arkbuilders.drop.app.data.helper.ResourcesHelperImpl import dev.arkbuilders.drop.app.data.repository.NetworkStatusImpl import dev.arkbuilders.drop.app.data.repository.ProfileRepoImpl +import dev.arkbuilders.drop.app.data.repository.ReceiveSessionRepo +import dev.arkbuilders.drop.app.data.repository.SendSessionRepo import dev.arkbuilders.drop.app.data.repository.TransferHistoryItemRepositoryImpl -import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.domain.AvatarHelper import dev.arkbuilders.drop.app.domain.PermissionsHelper import dev.arkbuilders.drop.app.domain.ResourcesHelper @@ -24,7 +25,6 @@ import org.koin.dsl.module val appModule = module { single { ProfileRepoImpl(get(), get()) } - single { TransferManager(get(), get(), get()) } single { ResourcesHelperImpl(get()) } single { Database.build(get()) } single { TransferHistoryItemRepositoryImpl(get()) } @@ -33,6 +33,8 @@ val appModule = single { AvatarHelperImpl(get()) } single { ProfileLocalDataSource(get(), get()) } single { TransferHistoryItemLocalDataSource(get()) } + single { SendSessionRepo(get(), get(), get()) } + single { ReceiveSessionRepo(get(), get(), get()) } factory { val db: Database = get() db.transferHistoryDao() diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt index 69a058c..d730dd7 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt @@ -6,4 +6,9 @@ interface ResourcesHelper { fun validateUris(uris: List): Pair, Int> fun getFileSize(uri: String): Long + + fun saveFileToDownloads( + fileName: String, + data: ByteArray, + ): String? } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/ReceiveSession.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/ReceiveSession.kt new file mode 100644 index 0000000..4f8e7b8 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/ReceiveSession.kt @@ -0,0 +1,9 @@ +package dev.arkbuilders.drop.app.domain.model + +import dev.arkbuilders.drop.ReceiveFilesBubble +import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl + +class ReceiveSession( + val bubble: ReceiveFilesBubble, + val subscriber: ReceiveFilesSubscriberImpl, +) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/SendSession.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/SendSession.kt new file mode 100644 index 0000000..25208bb --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/SendSession.kt @@ -0,0 +1,9 @@ +package dev.arkbuilders.drop.app.domain.model + +import dev.arkbuilders.drop.SendFilesBubble +import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl + +class SendSession( + val bubble: SendFilesBubble, + val subscriber: SendFilesSubscriberImpl, +) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt index f010ad4..bd29728 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt @@ -15,7 +15,6 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink -import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository import dev.arkbuilders.drop.app.presentation.history.History @@ -28,8 +27,6 @@ import dev.arkbuilders.drop.app.presentation.theme.DropTheme import org.koin.android.ext.android.get class MainActivity : ComponentActivity() { - private val transferManager: TransferManager = get() - private val profileRepo: ProfileRepo = get() private val transferHistoryItemRepository: TransferHistoryItemRepository = get() @@ -49,7 +46,6 @@ class MainActivity : ComponentActivity() { modifier = Modifier .padding(innerPadding), - transferManager = transferManager, profileRepo = profileRepo, transferHistoryItemRepository = transferHistoryItemRepository, ) @@ -63,7 +59,6 @@ class MainActivity : ComponentActivity() { fun DropNavigation( modifier: Modifier = Modifier, navController: NavHostController = rememberNavController(), - transferManager: TransferManager, profileRepo: ProfileRepo, transferHistoryItemRepository: TransferHistoryItemRepository, ) { @@ -82,7 +77,6 @@ fun DropNavigation( composable(DropDestination.Send.route) { Send( navController = navController, - transferManager = transferManager, ) } composable( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt index c5a39c8..a0353f6 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt @@ -3,8 +3,9 @@ package dev.arkbuilders.drop.app.presentation.receive import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dev.arkbuilders.drop.app.data.ReceivingProgress -import dev.arkbuilders.drop.app.data.repository.TransferManager +import dev.arkbuilders.drop.app.data.repository.ReceiveSessionRepo import dev.arkbuilders.drop.app.domain.PermissionsHelper +import dev.arkbuilders.drop.app.domain.model.ReceiveSession import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -25,11 +26,20 @@ sealed class ReceiveScreenState { data object Connecting : ReceiveScreenState() - data class Receiving(val progress: ReceivingProgress) : ReceiveScreenState() + data class Receiving( + val session: ReceiveSession, + val progress: ReceivingProgress, + ) : ReceiveScreenState() - data class Success(val receivedFiles: List) : ReceiveScreenState() + data class Success( + val session: ReceiveSession, + val receivedFiles: List, + ) : ReceiveScreenState() - data class Error(val error: ReceiveError) : ReceiveScreenState() + data class Error( + val session: ReceiveSession? = null, + val error: ReceiveError, + ) : ReceiveScreenState() } sealed class ReceiveScreenEffect { @@ -43,7 +53,7 @@ sealed class ReceiveScreenEffect { } class ReceiveViewModel( - private val transferManager: TransferManager, + private val receiveSessionRepo: ReceiveSessionRepo, private val permissionsHelper: PermissionsHelper, ) : ViewModel(), ContainerHost { override val container: Container = @@ -51,16 +61,8 @@ class ReceiveViewModel( init { intent { - val receiveProgress = transferManager.receiveProgress?.value - val state = - if (receiveProgress != null && receiveProgress.isConnected) { - listenToProgress() - ReceiveScreenState.Receiving(receiveProgress) - } else { - ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) - } reduce { - state + ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } } } @@ -97,7 +99,7 @@ class ReceiveViewModel( fun onError(error: ReceiveError) = intent { reduce { - ReceiveScreenState.Error(error) + ReceiveScreenState.Error(error = error) } } @@ -115,16 +117,19 @@ class ReceiveViewModel( ReceiveScreenState.Connecting } - val bubble = - transferManager.receiveFiles(ticket, confirmation) - if (bubble != null) { + val session = + receiveSessionRepo.receiveFiles(ticket, confirmation) + if (session != null) { reduce { - ReceiveScreenState.Receiving(transferManager.receiveProgress!!.value) + ReceiveScreenState.Receiving( + session, + session.subscriber.progress.value, + ) } - listenToProgress() + listenToProgress(session) } else { reduce { - ReceiveScreenState.Error(ReceiveError.ConnectionFailed) + ReceiveScreenState.Error(error = ReceiveError.ConnectionFailed) } } } catch (e: Exception) { @@ -139,7 +144,7 @@ class ReceiveViewModel( } reduce { - ReceiveScreenState.Error(error) + ReceiveScreenState.Error(error = error) } } } @@ -150,7 +155,7 @@ class ReceiveViewModel( if (isGranted) { ReceiveScreenState.Scanning } else { - ReceiveScreenState.Error(ReceiveError.CameraPermissionDenied) + ReceiveScreenState.Error(error = ReceiveError.CameraPermissionDenied) } reduce { state @@ -172,7 +177,10 @@ class ReceiveViewModel( fun onReceiveMore() = intent { - transferManager.cancelReceive() + val s = state + if (s is ReceiveScreenState.Success) { + receiveSessionRepo.cancelReceive(s.session) + } reduce { ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } @@ -180,7 +188,10 @@ class ReceiveViewModel( fun onDone() = intent { - transferManager.cancelReceive() + val s = state + if (s is ReceiveScreenState.Success) { + receiveSessionRepo.cancelReceive(s.session) + } postSideEffect(ReceiveScreenEffect.NavigateBack) } @@ -209,7 +220,12 @@ class ReceiveViewModel( fun onErrorDismiss() = intent { - transferManager.cancelReceive() + val s = state + if (s is ReceiveScreenState.Error) { + s.session?.let { + receiveSessionRepo.cancelReceive(it) + } + } postSideEffect(ReceiveScreenEffect.NavigateBack) } @@ -238,7 +254,11 @@ class ReceiveViewModel( fun onCancelReceiving() = intent { - transferManager.cancelReceive() + val s = state + if (s is ReceiveScreenState.Receiving) { + receiveSessionRepo.cancelReceive(s.session) + } + reduce { ReceiveScreenState.Initial(permissionsHelper.isCameraGranted()) } @@ -276,8 +296,8 @@ class ReceiveViewModel( } } - private fun listenToProgress() { - transferManager.receiveProgress!!.onEach { progress -> + private fun listenToProgress(session: ReceiveSession) { + session.subscriber.progress.onEach { progress -> intent { val s = state if (s !is ReceiveScreenState.Receiving) @@ -301,18 +321,20 @@ class ReceiveViewModel( // Small delay to ensure UI updates are visible delay(1000) try { - val savedFiles = transferManager.saveReceivedFiles() + val savedFiles = receiveSessionRepo.saveReceivedFiles(session) if (savedFiles.isNotEmpty()) { reduce { ReceiveScreenState.Success( - receivedFiles = savedFiles.map { it.name }, + session = session, + receivedFiles = savedFiles, ) } postSideEffect(ReceiveScreenEffect.ShowSuccessAnimation) } else { reduce { ReceiveScreenState.Error( - ReceiveError.NoFilesReceived, + session = session, + error = ReceiveError.NoFilesReceived, ) } } @@ -328,7 +350,10 @@ class ReceiveViewModel( else -> ReceiveError.UnknownError } reduce { - ReceiveScreenState.Error(error) + ReceiveScreenState.Error( + session = session, + error = error, + ) } } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt index fa93dc8..d39d2d1 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt @@ -8,20 +8,14 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -30,7 +24,6 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -58,7 +51,6 @@ import compose.icons.TablerIcons import compose.icons.tablericons.AlertCircle import compose.icons.tablericons.Copy import compose.icons.tablericons.Qrcode -import dev.arkbuilders.drop.app.data.repository.TransferManager import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant @@ -157,10 +149,7 @@ data class TransferProgressState( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Send( - navController: NavController, - transferManager: TransferManager, -) { +fun Send(navController: NavController) { val haptic = LocalHapticFeedback.current val listState = rememberLazyListState() val viewModel: SendViewModel = koinInject() @@ -186,79 +175,67 @@ fun Send( } } - Scaffold( + Column( modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.statusBars) - .windowInsetsPadding(WindowInsets.navigationBars) - .windowInsetsPadding(WindowInsets.ime), - topBar = { - DropTopBarBack( - title = "Send files", - onBackClick = { navController.navigateUp() }, - ) - }, - containerColor = MaterialTheme.colorScheme.background, - ) { paddingValues -> - - Box( - modifier = - Modifier - .fillMaxSize() - .padding(paddingValues), - ) { - val sendScreenState = state - when (sendScreenState) { - is SendScreenState.FileSelection -> { - FileSelectionPhase( - selectedFiles = sendScreenState.files, - totalFileSize = sendScreenState.size, - onAddFiles = { - viewModel.onAddFiles() - }, - onRemoveFile = { uri -> - viewModel.onFileRemove(uri) - }, - onStartTransfer = { - viewModel.onStartTransfer() - }, - canStartTransfer = sendScreenState.canStartTransfer, - listState = listState, - ) - } + .fillMaxSize(), + ) { + DropTopBarBack( + title = "Send files", + onBackClick = { navController.navigateUp() }, + ) + + val sendScreenState = state + when (sendScreenState) { + is SendScreenState.FileSelection -> { + FileSelectionPhase( + selectedFiles = sendScreenState.files, + totalFileSize = sendScreenState.size, + onAddFiles = { + viewModel.onAddFiles() + }, + onRemoveFile = { uri -> + viewModel.onFileRemove(uri) + }, + onStartTransfer = { + viewModel.onStartTransfer() + }, + canStartTransfer = sendScreenState.canStartTransfer, + listState = listState, + ) + } - is SendScreenState.GeneratingQR -> { - GeneratingQRPhase(onCancel = { viewModel.onCancelQrGeneration() }) - } + is SendScreenState.GeneratingQR -> { + GeneratingQRPhase(onCancel = { viewModel.onCancelQrGeneration() }) + } - is SendScreenState.WaitingForReceiver -> { - WaitingForReceiverPhase( - fileCount = sendScreenState.files.size, - onCancel = { viewModel.onCancelTransfer() }, - ) - } + is SendScreenState.WaitingForReceiver -> { + WaitingForReceiverPhase( + fileCount = sendScreenState.files.size, + onCancel = { viewModel.onCancelTransfer() }, + ) + } - is SendScreenState.Transfer -> { - TransferringPhase( - progress = sendScreenState, - onCancel = { viewModel.onCancelTransfer() }, - ) - } + is SendScreenState.Transfer -> { + TransferringPhase( + progress = sendScreenState, + onCancel = { viewModel.onCancelTransfer() }, + ) + } - is SendScreenState.Complete -> { - TransferCompletePhase( - fileCount = sendScreenState.files.size, - onSendMore = { - viewModel.onSendMore() - }, - onDone = { - viewModel.onDone() - }, - ) - } + is SendScreenState.Complete -> { + TransferCompletePhase( + fileCount = sendScreenState.files.size, + onSendMore = { + viewModel.onSendMore() + }, + onDone = { + viewModel.onDone() + }, + ) + } - is SendScreenState.Error -> { + is SendScreenState.Error -> { // ErrorPhase( // error = sendState.error, // onRetry = { handleError("Retry") }, @@ -267,8 +244,8 @@ fun Send( // navController.navigateUp() // } // ) - } } + } // sendState.error?.let { error -> // if (sendState.phase != SendPhase.Error) { @@ -279,16 +256,14 @@ fun Send( // } // } - val s = state - if (s is SendScreenState.WaitingForReceiver) { - SendQRDialog( - qrBitmap = s.qrBitmap, - fileCount = s.files.size, - copyString = s.copyString, - onDismiss = { }, - onCancel = {}, - ) - } + if (sendScreenState is SendScreenState.WaitingForReceiver) { + SendQRDialog( + qrBitmap = sendScreenState.qrBitmap, + fileCount = sendScreenState.files.size, + copyString = sendScreenState.copyString, + onDismiss = { }, + onCancel = {}, + ) } } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt index a402a47..a34a22f 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt @@ -11,8 +11,9 @@ import com.google.zxing.BarcodeFormat import com.google.zxing.WriterException import com.google.zxing.common.BitMatrix import com.google.zxing.qrcode.QRCodeWriter -import dev.arkbuilders.drop.app.data.repository.TransferManager +import dev.arkbuilders.drop.app.data.repository.SendSessionRepo import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.app.domain.model.SendSession import dev.arkbuilders.drop.app.domain.repository.NetworkStatus import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn @@ -35,12 +36,14 @@ sealed class SendScreenState { ) : SendScreenState() data class WaitingForReceiver( + val session: SendSession, val files: List, val qrBitmap: Bitmap, val copyString: String, ) : SendScreenState() data class Transfer( + val session: SendSession, val files: List, val isConnected: Boolean = false, val receiverName: String = "", @@ -54,7 +57,10 @@ sealed class SendScreenState { val estimatedTimeRemaining: Long = 0L, ) : SendScreenState() - data class Complete(val files: List) : SendScreenState() + data class Complete( + val session: SendSession, + val files: List, + ) : SendScreenState() data class Error(val error: Throwable) : SendScreenState() } @@ -68,7 +74,7 @@ sealed class SendScreenEffect { class SendViewModel( private val resourcesHelper: ResourcesHelper, private val networkStatus: NetworkStatus, - private val transferManager: TransferManager, + private val sendSessionRepo: SendSessionRepo, ) : ViewModel(), ContainerHost { override val container: Container = container(SendScreenState.FileSelection()) @@ -115,15 +121,15 @@ class SendViewModel( } return@intent } - val bubble = transferManager.sendFiles(s.files.map { it.toUri() }) - if (bubble == null) { + val session = sendSessionRepo.sendFiles(s.files.map { it.toUri() }) + if (session == null) { reduce { SendScreenState.Error(Error("")) } return@intent } - val ticket = transferManager.getCurrentSendTicket() ?: "" - val confirmation = transferManager.getCurrentSendConfirmation() ?: 0u + val ticket = session.bubble.getTicket() + val confirmation = session.bubble.getConfirmation() if (ticket.isEmpty()) { reduce { @@ -131,7 +137,7 @@ class SendViewModel( } return@intent } - val copyString = "${bubble.getTicket()} ${bubble.getConfirmation()}" + val copyString = "${session.bubble.getTicket()} ${session.bubble.getConfirmation()}" val qrBitmap = generateQRCodeSafely(ticket, confirmation) if (qrBitmap == null) { @@ -140,10 +146,11 @@ class SendViewModel( } return@intent } - listenToSendProgress() - monitorTransferCompletion() + listenToSendProgress(session) + monitorTransferCompletion(session) reduce { SendScreenState.WaitingForReceiver( + session = session, files = s.files, qrBitmap = qrBitmap, copyString = copyString, @@ -153,7 +160,14 @@ class SendViewModel( fun onCancelTransfer() = intent { - transferManager.cancelSend() + val s = state + val session = + when (s) { + is SendScreenState.WaitingForReceiver -> s.session + is SendScreenState.Transfer -> s.session + else -> null + } + session?.let { sendSessionRepo.cancelSend(it) } postSideEffect(SendScreenEffect.NavigateBack) } @@ -168,29 +182,38 @@ class SendViewModel( intent { val s = state if (s is SendScreenState.Transfer) { - transferManager.recordSendCompletion(s.files.map { it.toUri() }) + sendSessionRepo.recordSendCompletion( + s.files.map { it.toUri() }, + s.session, + ) reduce { - SendScreenState.Complete(files = s.files) + SendScreenState.Complete(session = s.session, files = s.files) } } } fun onDone() = intent { - transferManager.cancelSend() + val s = state + if (s is SendScreenState.Complete) { + sendSessionRepo.cancelSend(s.session) + } postSideEffect(SendScreenEffect.NavigateBack) } fun onSendMore() = intent { - transferManager.cancelSend() + val s = state + if (s is SendScreenState.Complete) { + sendSessionRepo.cancelSend(s.session) + } reduce { SendScreenState.FileSelection() } } - private fun listenToSendProgress() { - transferManager.sendProgress!!.onEach { progress -> + private fun listenToSendProgress(session: SendSession) { + session.subscriber.progress.onEach { progress -> intent { if (progress.isConnected.not()) return@intent @@ -205,6 +228,7 @@ class SendViewModel( val transfer = SendScreenState.Transfer( + session = session, files = files, isConnected = progress.isConnected, receiverName = progress.receiverName, @@ -223,10 +247,10 @@ class SendViewModel( }.launchIn(viewModelScope) } - private fun monitorTransferCompletion() { + private fun monitorTransferCompletion(session: SendSession) { viewModelScope.launch { while (coroutineContext.isActive) { - val isFinished = transferManager.isSendFinished() + val isFinished = session.bubble.isFinished() if (isFinished) { onComplete() break From 611d171112ed78a55d2c4da6a7c9da33e1061528 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Sun, 16 Nov 2025 06:37:46 +0600 Subject: [PATCH 19/33] Refactor error handling in Send screen --- .../drop/app/presentation/send/Send.kt | 111 ++---------- .../app/presentation/send/SendScreenState.kt | 61 +++++++ .../app/presentation/send/SendViewModel.kt | 170 ++++++++++-------- .../send/components/SendErrorOverlay.kt | 118 ------------ .../send/components/phase/ErrorPhase.kt | 53 +++--- 5 files changed, 193 insertions(+), 320 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendScreenState.kt delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt index d39d2d1..8b5d428 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt @@ -18,8 +18,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -38,7 +36,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.semantics.contentDescription @@ -48,7 +45,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import compose.icons.TablerIcons -import compose.icons.tablericons.AlertCircle import compose.icons.tablericons.Copy import compose.icons.tablericons.Qrcode import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack @@ -56,6 +52,7 @@ import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant import dev.arkbuilders.drop.app.presentation.send.components.SendButton import dev.arkbuilders.drop.app.presentation.send.components.SendLoadingIndicator +import dev.arkbuilders.drop.app.presentation.send.components.phase.ErrorPhase import dev.arkbuilders.drop.app.presentation.send.components.phase.FileSelectionPhase import dev.arkbuilders.drop.app.presentation.send.components.phase.GeneratingQRPhase import dev.arkbuilders.drop.app.presentation.send.components.phase.TransferCompletePhase @@ -67,86 +64,6 @@ import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect -// Comprehensive exception handling -sealed class SendException( - val title: String, - val message: String, - val icon: ImageVector, - val isRecoverable: Boolean = true, - val actionLabel: String? = null, -) { - object NetworkUnavailable : SendException( - title = "No Network Connection", - message = "Please check your Wi-Fi or mobile data connection and try again.", - icon = Icons.Default.Warning, - actionLabel = "Retry", - ) - - object FileTooLarge : SendException( - title = "File Too Large", - message = - "Some files exceed the 2GB limit and were skipped." + - " You can send the remaining files.", - icon = Icons.Default.Warning, - actionLabel = "Continue", - ) - - object NoFilesSelected : SendException( - title = "No Files Selected", - message = "Please select at least one file to send.", - icon = Icons.Default.Warning, - isRecoverable = false, - ) - - object TransferInitializationFailed : SendException( - title = "Transfer Setup Failed", - message = "Unable to prepare files for transfer. Please try again.", - icon = TablerIcons.AlertCircle, - actionLabel = "Retry", - ) - - object QRGenerationFailed : SendException( - title = "QR Code Generation Failed", - message = "Unable to create QR code. Please restart the transfer.", - icon = TablerIcons.AlertCircle, - actionLabel = "Retry", - ) - - object TransferInterrupted : SendException( - title = "Transfer Interrupted", - message = "The connection was lost during transfer. You can try sending again.", - icon = TablerIcons.AlertCircle, - actionLabel = "Retry", - ) - - object ReceiverDisconnected : SendException( - title = "Receiver Disconnected", - message = "The receiving device disconnected. Please try again.", - icon = Icons.Default.Warning, - actionLabel = "Retry", - ) - - class UnknownError(details: String) : SendException( - title = "Something Went Wrong", - message = "An unexpected error occurred: $details", - icon = TablerIcons.AlertCircle, - actionLabel = "Retry", - ) -} - -data class TransferProgressState( - val isConnected: Boolean = false, - val receiverName: String = "", - val receiverAvatar: String? = null, - val currentFileName: String = "", - val filesCompleted: Int = 0, - val totalFiles: Int = 0, - val bytesTransferred: Long = 0L, - val totalBytes: Long = 0L, - val transferSpeedBps: Long = 0L, - val estimatedTimeRemaining: Long = 0L, -) - @OptIn(ExperimentalMaterial3Api::class) @Composable fun Send(navController: NavController) { @@ -236,26 +153,18 @@ fun Send(navController: NavController) { } is SendScreenState.Error -> { -// ErrorPhase( -// error = sendState.error, -// onRetry = { handleError("Retry") }, -// onCancel = { -// transferManager.cancelSend() -// navController.navigateUp() -// } -// ) + ErrorPhase( + error = sendScreenState.error, + onRetry = { + viewModel.onErrorRetry() + }, + onCancel = { + viewModel.onErrorDismiss() + }, + ) } } -// sendState.error?.let { error -> -// if (sendState.phase != SendPhase.Error) { -// SendErrorOverlay( -// error = error, -// onDismiss = { sendState = sendState.copy(error = null) }, -// onAction = { action -> handleError(action) }) -// } -// } - if (sendScreenState is SendScreenState.WaitingForReceiver) { SendQRDialog( qrBitmap = sendScreenState.qrBitmap, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendScreenState.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendScreenState.kt new file mode 100644 index 0000000..0193c5f --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendScreenState.kt @@ -0,0 +1,61 @@ +package dev.arkbuilders.drop.app.presentation.send + +import android.graphics.Bitmap +import dev.arkbuilders.drop.app.domain.model.SendSession + +sealed class SendScreenState { + data class FileSelection( + val files: List = emptyList(), + val size: Long = 0L, + val canStartTransfer: Boolean = false, + ) : SendScreenState() + + data class GeneratingQR( + val files: List, + ) : SendScreenState() + + data class WaitingForReceiver( + val session: SendSession, + val files: List, + val qrBitmap: Bitmap, + val copyString: String, + ) : SendScreenState() + + data class Transfer( + val session: SendSession, + val files: List, + val isConnected: Boolean = false, + val receiverName: String = "", + val receiverAvatar: String? = null, + val currentFileName: String = "", + val filesCompleted: Int = 0, + val totalFiles: Int = 0, + val bytesTransferred: Long = 0L, + val totalBytes: Long = 0L, + val transferSpeedBps: Long = 0L, + val estimatedTimeRemaining: Long = 0L, + ) : SendScreenState() + + data class Complete( + val session: SendSession, + val files: List, + ) : SendScreenState() + + data class Error( + val session: SendSession? = null, + val files: List? = null, + val error: SendException, + ) : SendScreenState() +} + +sealed class SendScreenEffect { + data object LaunchFilePicker : SendScreenEffect() + + data object NavigateBack : SendScreenEffect() +} + +enum class SendException { + TransferInitializationFailed, + QRGenerationFailed, + TransferInterrupted, +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt index a34a22f..fc44e32 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt @@ -23,53 +23,10 @@ import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container - -sealed class SendScreenState { - data class FileSelection( - val files: List = emptyList(), - val size: Long = 0L, - val canStartTransfer: Boolean = false, - ) : SendScreenState() - - data class GeneratingQR( - val files: List, - ) : SendScreenState() - - data class WaitingForReceiver( - val session: SendSession, - val files: List, - val qrBitmap: Bitmap, - val copyString: String, - ) : SendScreenState() - - data class Transfer( - val session: SendSession, - val files: List, - val isConnected: Boolean = false, - val receiverName: String = "", - val receiverAvatar: String? = null, - val currentFileName: String = "", - val filesCompleted: Int = 0, - val totalFiles: Int = 0, - val bytesTransferred: Long = 0L, - val totalBytes: Long = 0L, - val transferSpeedBps: Long = 0L, - val estimatedTimeRemaining: Long = 0L, - ) : SendScreenState() - - data class Complete( - val session: SendSession, - val files: List, - ) : SendScreenState() - - data class Error(val error: Throwable) : SendScreenState() -} - -sealed class SendScreenEffect { - data object LaunchFilePicker : SendScreenEffect() - - data object NavigateBack : SendScreenEffect() -} +import timber.log.Timber +import kotlin.collections.isNotEmpty +import kotlin.collections.plus +import kotlin.collections.sumOf class SendViewModel( private val resourcesHelper: ResourcesHelper, @@ -116,15 +73,19 @@ class SendViewModel( intent { val s = state if (s !is SendScreenState.FileSelection) { - reduce { - SendScreenState.Error(Error("")) - } return@intent } + reduce { + SendScreenState.GeneratingQR(s.files) + } + val session = sendSessionRepo.sendFiles(s.files.map { it.toUri() }) if (session == null) { reduce { - SendScreenState.Error(Error("")) + SendScreenState.Error( + files = s.files, + error = SendException.TransferInitializationFailed, + ) } return@intent } @@ -133,7 +94,11 @@ class SendViewModel( if (ticket.isEmpty()) { reduce { - SendScreenState.Error(Error("")) + SendScreenState.Error( + session = session, + files = s.files, + error = SendException.TransferInitializationFailed, + ) } return@intent } @@ -142,7 +107,11 @@ class SendViewModel( val qrBitmap = generateQRCodeSafely(ticket, confirmation) if (qrBitmap == null) { reduce { - SendScreenState.Error(Error("")) + SendScreenState.Error( + session = session, + files = s.files, + error = SendException.QRGenerationFailed, + ) } return@intent } @@ -173,8 +142,16 @@ class SendViewModel( fun onCancelQrGeneration() = intent { + val s = state + val files = + if (s is SendScreenState.GeneratingQR) { + s.files + } else { + emptyList() + } + reduce { - SendScreenState.FileSelection() + SendScreenState.FileSelection(files) } } @@ -212,12 +189,47 @@ class SendViewModel( } } + fun onErrorRetry() = + intent { + val s = state + if (s is SendScreenState.Error) { + s.session?.let { + sendSessionRepo.cancelSend(it) + } + } + val files = + when (s) { + is SendScreenState.Error -> s.files + else -> emptyList() + } ?: emptyList() + + val validated = resourcesHelper.validateUris(files).first + val canStartTransfer = validated.isNotEmpty() && networkStatus.isOnline() + val size = validated.sumOf { resourcesHelper.getFileSize(it) } + + reduce { + SendScreenState.FileSelection( + files = validated, + size = size, + canStartTransfer = canStartTransfer, + ) + } + } + + fun onErrorDismiss() = + intent { + val s = state + if (s is SendScreenState.Error) { + s.session?.let { + sendSessionRepo.cancelSend(it) + } + } + postSideEffect(SendScreenEffect.NavigateBack) + } + private fun listenToSendProgress(session: SendSession) { session.subscriber.progress.onEach { progress -> intent { - if (progress.isConnected.not()) - return@intent - val s = state val files = when (s) { @@ -226,22 +238,36 @@ class SendViewModel( else -> return@intent } - val transfer = - SendScreenState.Transfer( - session = session, - files = files, - isConnected = progress.isConnected, - receiverName = progress.receiverName, - receiverAvatar = progress.receiverAvatar, - currentFileName = progress.fileName, - bytesTransferred = progress.sent.toLong(), - totalBytes = (progress.sent + progress.remaining).toLong(), - transferSpeedBps = 0L, - estimatedTimeRemaining = 0L, - ) + try { + if (progress.isConnected.not()) + return@intent - reduce { - transfer + val transfer = + SendScreenState.Transfer( + session = session, + files = files, + isConnected = progress.isConnected, + receiverName = progress.receiverName, + receiverAvatar = progress.receiverAvatar, + currentFileName = progress.fileName, + bytesTransferred = progress.sent.toLong(), + totalBytes = (progress.sent + progress.remaining).toLong(), + transferSpeedBps = 0L, + estimatedTimeRemaining = 0L, + ) + + reduce { + transfer + } + } catch (e: Throwable) { + Timber.e("Transfer interrupted: ${e::class.simpleName} ${e.message}") + reduce { + SendScreenState.Error( + session = session, + files = files, + error = SendException.TransferInterrupted, + ) + } } } }.launchIn(viewModelScope) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt deleted file mode 100644 index eb4e799..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendErrorOverlay.kt +++ /dev/null @@ -1,118 +0,0 @@ -package dev.arkbuilders.drop.app.presentation.send.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.presentation.send.SendException - -@Composable -fun SendErrorOverlay( - error: SendException, - onDismiss: () -> Unit, - onAction: (String) -> Unit, -) { - Box( - modifier = - Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.5f)) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - ) { onDismiss() }, - contentAlignment = Alignment.Center, - ) { - SendCard( - modifier = - Modifier - .padding(20.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - ) { /* Prevent dismiss on card click */ }, - ) { - Column( - modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - imageVector = error.icon, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = error.title, - style = - MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold, - ), - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = error.message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - if (error.isRecoverable && error.actionLabel != null) { - SendButton( - onClick = { onAction(error.actionLabel) }, - variant = ButtonVariant.Primary, - size = ButtonSize.Medium, - ) { - Text( - error.actionLabel, - fontWeight = FontWeight.Medium, - ) - } - } - - SendButton( - onClick = onDismiss, - variant = ButtonVariant.Secondary, - size = ButtonSize.Medium, - ) { - Text( - "Dismiss", - fontWeight = FontWeight.Medium, - ) - } - } - } - } - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt index 407da94..0ceb0e2 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -25,7 +27,7 @@ import dev.arkbuilders.drop.app.presentation.send.components.SendButton import dev.arkbuilders.drop.app.presentation.send.components.SendCard @Composable -private fun ErrorPhase( +fun ErrorPhase( error: SendException?, onRetry: () -> Unit, onCancel: () -> Unit, @@ -42,7 +44,7 @@ private fun ErrorPhase( error?.let { err -> SendErrorCard( error = err, - onAction = if (err.isRecoverable) onRetry else null, + onAction = onRetry, ) } @@ -67,7 +69,7 @@ private fun ErrorPhase( @Composable private fun SendErrorCard( error: SendException, - onAction: (() -> Unit)? = null, + onAction: () -> Unit, ) { SendCard( backgroundColor = MaterialTheme.colorScheme.errorContainer, @@ -77,7 +79,7 @@ private fun SendErrorCard( horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( - imageVector = error.icon, + imageVector = Icons.Default.Warning, contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.error, @@ -86,38 +88,31 @@ private fun SendErrorCard( Spacer(modifier = Modifier.height(16.dp)) Text( - text = error.title, - style = - MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold, - ), - color = MaterialTheme.colorScheme.onErrorContainer, - textAlign = TextAlign.Center, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = error.message, + text = error.toMessage(), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f), textAlign = TextAlign.Center, ) - if (onAction != null && error.actionLabel != null) { - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(20.dp)) - SendButton( - onClick = onAction, - variant = ButtonVariant.Primary, - size = ButtonSize.Medium, - ) { - Text( - error.actionLabel, - fontWeight = FontWeight.Medium, - ) - } + SendButton( + onClick = onAction, + variant = ButtonVariant.Primary, + size = ButtonSize.Medium, + ) { + Text( + "Retry", + fontWeight = FontWeight.Medium, + ) } } } } + +private fun SendException.toMessage() = + when (this) { + SendException.TransferInitializationFailed -> "Transfer initialization failed" + SendException.QRGenerationFailed -> "QR generation failed" + SendException.TransferInterrupted -> "Transfer interrupted" + } From 3e28cbdadad9da1679bd1bf7cc25e9402408486b Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Mon, 17 Nov 2025 05:53:02 +0600 Subject: [PATCH 20/33] Convert ReceiveError to enum, extract DropErrorCard --- .../DropErrorCard.kt} | 59 ++++----- .../drop/app/presentation/receive/Receive.kt | 54 ++++---- .../receive/ReceiveScreenState.kt | 56 +++++++++ .../presentation/receive/ReceiveViewModel.kt | 40 ------ .../drop/app/presentation/send/Send.kt | 18 +-- .../send/components/phase/ErrorPhase.kt | 118 ------------------ 6 files changed, 112 insertions(+), 233 deletions(-) rename app/src/main/java/dev/arkbuilders/drop/app/presentation/{receive/components/ReceiveErrorCard.kt => components/DropErrorCard.kt} (69%) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveScreenState.kt delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropErrorCard.kt similarity index 69% rename from app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt rename to app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropErrorCard.kt index bfc7a5b..963254f 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveErrorCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropErrorCard.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.drop.app.presentation.receive.components +package dev.arkbuilders.drop.app.presentation.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -20,27 +20,27 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import compose.icons.TablerIcons -import compose.icons.tablericons.AlertCircle -import dev.arkbuilders.drop.app.presentation.receive.ReceiveError import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable -fun ReceiveErrorCard( - error: ReceiveError, +fun DropErrorCard( + message: String, onRetry: () -> Unit, onDismiss: () -> Unit, ) { ElevatedCard( - modifier = Modifier.fillMaxWidth().padding(16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), colors = CardDefaults.elevatedCardColors( @@ -67,7 +67,7 @@ fun ReceiveErrorCard( contentAlignment = Alignment.Center, ) { Icon( - if (error.isRecoverable) Icons.Default.Warning else TablerIcons.AlertCircle, + Icons.Default.Warning, contentDescription = null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.error, @@ -77,16 +77,7 @@ fun ReceiveErrorCard( Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) Text( - text = if (error.isRecoverable) "Something went wrong" else "Error occurred", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - ) - - Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) - - Text( - text = error.message, + text = message, style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f), @@ -99,7 +90,7 @@ fun ReceiveErrorCard( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(DesignTokens.Spacing.md), ) { - OutlinedButton( + TextButton( onClick = onDismiss, modifier = Modifier @@ -110,21 +101,19 @@ fun ReceiveErrorCard( Text("Cancel", fontWeight = FontWeight.Medium) } - if (error.isRecoverable) { - Button( - onClick = onRetry, - modifier = - Modifier - .weight(1f) - .height(DesignTokens.TouchTarget.comfortable), - shape = RoundedCornerShape(DesignTokens.CornerRadius.md), - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - ) { - Text("Try Again", fontWeight = FontWeight.SemiBold) - } + Button( + onClick = onRetry, + modifier = + Modifier + .weight(1f) + .height(DesignTokens.TouchTarget.comfortable), + shape = RoundedCornerShape(DesignTokens.CornerRadius.md), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + ) { + Text("Retry", fontWeight = FontWeight.SemiBold) } } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt index 5d381fc..ce76e2b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt @@ -53,9 +53,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi +import dev.arkbuilders.drop.app.presentation.components.DropErrorCard import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveCompleteCard -import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveErrorCard import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveLoadingCard import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveManualInputCard import dev.arkbuilders.drop.app.presentation.receive.components.ReceivePermissionRequestCard @@ -69,36 +69,6 @@ import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect -sealed class ReceiveError(val message: String, val isRecoverable: Boolean = true) { - object CameraPermissionDenied : - ReceiveError("Camera permission is required to scan QR codes", true) - - object CameraInitializationFailed : - ReceiveError("Unable to initialize camera. Please try again.", true) - - object InvalidQRCode : - ReceiveError("This QR code is not from Drop. Please scan a valid Drop QR code.", true) - - object InvalidManualInput : - ReceiveError("Invalid format. Please enter: ticket confirmation", true) - - object ConnectionFailed : - ReceiveError("Unable to connect to sender. Please ensure you're on the same network.", true) - - object TransferInterrupted : - ReceiveError("File transfer was interrupted. Please try again.", true) - - object NoFilesReceived : ReceiveError("No files were received from the sender.", true) - - object StorageError : - ReceiveError("Unable to save files. Please check your storage permissions.", true) - - object NetworkError : - ReceiveError("Network connection lost. Please check your connection and try again.", true) - - object UnknownError : ReceiveError("An unexpected error occurred. Please try again.", true) -} - @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @Composable fun Receive(navController: NavController) { @@ -351,8 +321,8 @@ fun Receive(navController: NavController) { } is ReceiveScreenState.Error -> { - ReceiveErrorCard( - error = receiveScreenState.error, + DropErrorCard( + message = receiveScreenState.error.toMessage(), onRetry = { viewModel.onErrorRetry() }, @@ -422,3 +392,21 @@ fun Receive(navController: NavController) { } } } + +private fun ReceiveError.toMessage() = + when (this) { + ReceiveError.CameraInitializationFailed -> "Unable to initialize camera. Please try again." + ReceiveError.CameraPermissionDenied -> "Camera permission is required to scan QR codes" + ReceiveError.ConnectionFailed -> "Unable to connect to sender." + ReceiveError.InvalidManualInput -> "Invalid format. Please enter: ticket confirmation" + ReceiveError.InvalidQRCode -> + "This QR code is not from Drop. Please scan a valid Drop QR code." + + ReceiveError.NetworkError -> + "Network connection lost. Please check your connection and try again." + + ReceiveError.NoFilesReceived -> "No files were received from the sender." + ReceiveError.StorageError -> "Unable to save files. Please check your storage permissions." + ReceiveError.TransferInterrupted -> "File transfer was interrupted. Please try again." + ReceiveError.UnknownError -> "An unexpected error occurred. Please try again." + } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveScreenState.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveScreenState.kt new file mode 100644 index 0000000..b80ef3f --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveScreenState.kt @@ -0,0 +1,56 @@ +package dev.arkbuilders.drop.app.presentation.receive + +import dev.arkbuilders.drop.app.data.ReceivingProgress +import dev.arkbuilders.drop.app.domain.model.ReceiveSession + +sealed class ReceiveScreenState { + data class Initial(val cameraPermissionGranted: Boolean) : ReceiveScreenState() + + data object RequestingPermission : ReceiveScreenState() + + data object Scanning : ReceiveScreenState() + + data class ManualInput(val inputText: String, val inputError: String?) : ReceiveScreenState() + + data class QRCodeScanned(val ticket: String, val confirmation: UByte) : ReceiveScreenState() + + data object Connecting : ReceiveScreenState() + + data class Receiving( + val session: ReceiveSession, + val progress: ReceivingProgress, + ) : ReceiveScreenState() + + data class Success( + val session: ReceiveSession, + val receivedFiles: List, + ) : ReceiveScreenState() + + data class Error( + val session: ReceiveSession? = null, + val error: ReceiveError, + ) : ReceiveScreenState() +} + +sealed class ReceiveScreenEffect { + data object HideKeyboard : ReceiveScreenEffect() + + data object NavigateBack : ReceiveScreenEffect() + + data object ShowSuccessAnimation : ReceiveScreenEffect() + + data object RequestCameraPermission : ReceiveScreenEffect() +} + +enum class ReceiveError { + CameraPermissionDenied, + CameraInitializationFailed, + InvalidQRCode, + InvalidManualInput, + ConnectionFailed, + TransferInterrupted, + NoFilesReceived, + StorageError, + NetworkError, + UnknownError, +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt index a0353f6..c99daf6 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt @@ -2,7 +2,6 @@ package dev.arkbuilders.drop.app.presentation.receive import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dev.arkbuilders.drop.app.data.ReceivingProgress import dev.arkbuilders.drop.app.data.repository.ReceiveSessionRepo import dev.arkbuilders.drop.app.domain.PermissionsHelper import dev.arkbuilders.drop.app.domain.model.ReceiveSession @@ -13,45 +12,6 @@ import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container -sealed class ReceiveScreenState { - data class Initial(val cameraPermissionGranted: Boolean) : ReceiveScreenState() - - data object RequestingPermission : ReceiveScreenState() - - data object Scanning : ReceiveScreenState() - - data class ManualInput(val inputText: String, val inputError: String?) : ReceiveScreenState() - - data class QRCodeScanned(val ticket: String, val confirmation: UByte) : ReceiveScreenState() - - data object Connecting : ReceiveScreenState() - - data class Receiving( - val session: ReceiveSession, - val progress: ReceivingProgress, - ) : ReceiveScreenState() - - data class Success( - val session: ReceiveSession, - val receivedFiles: List, - ) : ReceiveScreenState() - - data class Error( - val session: ReceiveSession? = null, - val error: ReceiveError, - ) : ReceiveScreenState() -} - -sealed class ReceiveScreenEffect { - data object HideKeyboard : ReceiveScreenEffect() - - data object NavigateBack : ReceiveScreenEffect() - - data object ShowSuccessAnimation : ReceiveScreenEffect() - - data object RequestCameraPermission : ReceiveScreenEffect() -} - class ReceiveViewModel( private val receiveSessionRepo: ReceiveSessionRepo, private val permissionsHelper: PermissionsHelper, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt index 8b5d428..fa9d805 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt @@ -33,26 +33,23 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.Copy import compose.icons.tablericons.Qrcode +import dev.arkbuilders.drop.app.presentation.components.DropErrorCard import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant import dev.arkbuilders.drop.app.presentation.send.components.SendButton import dev.arkbuilders.drop.app.presentation.send.components.SendLoadingIndicator -import dev.arkbuilders.drop.app.presentation.send.components.phase.ErrorPhase import dev.arkbuilders.drop.app.presentation.send.components.phase.FileSelectionPhase import dev.arkbuilders.drop.app.presentation.send.components.phase.GeneratingQRPhase import dev.arkbuilders.drop.app.presentation.send.components.phase.TransferCompletePhase @@ -153,12 +150,12 @@ fun Send(navController: NavController) { } is SendScreenState.Error -> { - ErrorPhase( - error = sendScreenState.error, + DropErrorCard( + message = sendScreenState.error.toMessage(), onRetry = { viewModel.onErrorRetry() }, - onCancel = { + onDismiss = { viewModel.onErrorDismiss() }, ) @@ -333,3 +330,10 @@ private fun SendQRDialog( shape = RoundedCornerShape(20.dp), ) } + +private fun SendException.toMessage() = + when (this) { + SendException.TransferInitializationFailed -> "Transfer initialization failed" + SendException.QRGenerationFailed -> "QR generation failed" + SendException.TransferInterrupted -> "Transfer interrupted" + } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt deleted file mode 100644 index 0ceb0e2..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/ErrorPhase.kt +++ /dev/null @@ -1,118 +0,0 @@ -package dev.arkbuilders.drop.app.presentation.send.components.phase - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import dev.arkbuilders.drop.app.presentation.send.SendException -import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize -import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant -import dev.arkbuilders.drop.app.presentation.send.components.SendButton -import dev.arkbuilders.drop.app.presentation.send.components.SendCard - -@Composable -fun ErrorPhase( - error: SendException?, - onRetry: () -> Unit, - onCancel: () -> Unit, -) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Column( - modifier = Modifier.padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { - error?.let { err -> - SendErrorCard( - error = err, - onAction = onRetry, - ) - } - - SendButton( - onClick = onCancel, - variant = ButtonVariant.Secondary, - size = ButtonSize.Large, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - "Cancel", - style = - MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Medium, - ), - ) - } - } - } -} - -@Composable -private fun SendErrorCard( - error: SendException, - onAction: () -> Unit, -) { - SendCard( - backgroundColor = MaterialTheme.colorScheme.errorContainer, - ) { - Column( - modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = error.toMessage(), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f), - textAlign = TextAlign.Center, - ) - - Spacer(modifier = Modifier.height(20.dp)) - - SendButton( - onClick = onAction, - variant = ButtonVariant.Primary, - size = ButtonSize.Medium, - ) { - Text( - "Retry", - fontWeight = FontWeight.Medium, - ) - } - } - } -} - -private fun SendException.toMessage() = - when (this) { - SendException.TransferInitializationFailed -> "Transfer initialization failed" - SendException.QRGenerationFailed -> "QR generation failed" - SendException.TransferInterrupted -> "Transfer interrupted" - } From d301756ab0d49aff479dc44b869ee36eb4839bc2 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 18 Nov 2025 00:59:51 +0600 Subject: [PATCH 21/33] Rename TransferHistoryItem to TransferSession, store files info as json --- app/build.gradle.kts | 1 + .../1.json | 24 +++------ .../TransferHistoryItemLocalDataSource.kt | 49 ------------------- .../TransferSessionLocalDataSource.kt | 44 +++++++++++++++++ .../arkbuilders/drop/app/data/db/Database.kt | 10 ++-- .../app/data/db/dao/TransferHistoryItemDao.kt | 26 ---------- .../app/data/db/dao/TransferSessionDao.kt | 26 ++++++++++ ...ryItemEntity.kt => RoomTransferSession.kt} | 7 ++- .../typeconverters/DropFileListConverter.kt | 17 +++++++ .../app/data/repository/ReceiveSessionRepo.kt | 22 +++------ .../app/data/repository/SendSessionRepo.kt | 24 +++++---- ...toryImpl.kt => TransferSessionRepoImpl.kt} | 37 ++++++-------- .../dev/arkbuilders/drop/app/di/AppModule.kt | 14 +++--- .../drop/app/domain/model/DropFileInfo.kt | 9 ++++ ...nsferHistoryItem.kt => TransferSession.kt} | 6 +-- ...emRepository.kt => TransferSessionRepo.kt} | 17 +++---- .../drop/app/presentation/MainActivity.kt | 12 ++--- .../drop/app/presentation/history/History.kt | 17 +++---- .../presentation/history/HistoryViewModel.kt | 10 ++-- .../drop/app/presentation/home/Home.kt | 15 +++--- .../app/presentation/home/HomeViewModel.kt | 8 +-- .../presentation/receive/ReceiveViewModel.kt | 2 + 22 files changed, 194 insertions(+), 203 deletions(-) delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferSessionLocalDataSource.kt delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferSessionDao.kt rename app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/{TransferHistoryItemEntity.kt => RoomTransferSession.kt} (79%) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/DropFileListConverter.kt rename app/src/main/java/dev/arkbuilders/drop/app/data/repository/{TransferHistoryItemRepositoryImpl.kt => TransferSessionRepoImpl.kt} (52%) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/model/DropFileInfo.kt rename app/src/main/java/dev/arkbuilders/drop/app/domain/model/{TransferHistoryItem.kt => TransferSession.kt} (70%) rename app/src/main/java/dev/arkbuilders/drop/app/domain/repository/{TransferHistoryItemRepository.kt => TransferSessionRepo.kt} (57%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 03d64ea..5b4e4f0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.triplet.play) alias(libs.plugins.ksp) alias(libs.plugins.ktlint.gradle) + alias(libs.plugins.kotlin.serialization) } kotlin { diff --git a/app/schemas/dev.arkbuilders.drop.app.data.db.Database/1.json b/app/schemas/dev.arkbuilders.drop.app.data.db.Database/1.json index ebd7541..f9a8d14 100644 --- a/app/schemas/dev.arkbuilders.drop.app.data.db.Database/1.json +++ b/app/schemas/dev.arkbuilders.drop.app.data.db.Database/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "5fa9ecea78d66afdf6f2502843c1f445", + "identityHash": "be40b7222d453a16e4b006fe48729c8e", "entities": [ { - "tableName": "TransferHistoryItemEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fileName` TEXT NOT NULL, `fileSize` INTEGER NOT NULL, `type` TEXT NOT NULL, `timestamp` TEXT NOT NULL, `status` TEXT NOT NULL, `peerName` TEXT NOT NULL, `peerAvatar` TEXT, `fileCount` INTEGER NOT NULL)", + "tableName": "RoomTransferSession", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `files` TEXT NOT NULL, `type` TEXT NOT NULL, `timestamp` TEXT NOT NULL, `status` TEXT NOT NULL, `peerName` TEXT NOT NULL, `peerAvatar` TEXT)", "fields": [ { "fieldPath": "id", @@ -15,17 +15,11 @@ "notNull": true }, { - "fieldPath": "fileName", - "columnName": "fileName", + "fieldPath": "files", + "columnName": "files", "affinity": "TEXT", "notNull": true }, - { - "fieldPath": "fileSize", - "columnName": "fileSize", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "type", "columnName": "type", @@ -54,12 +48,6 @@ "fieldPath": "peerAvatar", "columnName": "peerAvatar", "affinity": "TEXT" - }, - { - "fieldPath": "fileCount", - "columnName": "fileCount", - "affinity": "INTEGER", - "notNull": true } ], "primaryKey": { @@ -72,7 +60,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5fa9ecea78d66afdf6f2502843c1f445')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be40b7222d453a16e4b006fe48729c8e')" ] } } \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt deleted file mode 100644 index 9e84c22..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferHistoryItemLocalDataSource.kt +++ /dev/null @@ -1,49 +0,0 @@ -package dev.arkbuilders.drop.app.data.datasource - -import dev.arkbuilders.drop.app.data.db.dao.TransferHistoryItemDao -import dev.arkbuilders.drop.app.data.db.entity.TransferHistoryItemEntity -import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class TransferHistoryItemLocalDataSource( - private val dao: TransferHistoryItemDao, -) { - fun flow(): Flow> = - dao.getAll().map { list -> list.map { it.toDomain() } } - - suspend fun add(item: TransferHistoryItem): Long = dao.insert(item.toEntity()) - - suspend fun addAll(items: List) = - dao.insertAll(items.map { it.toEntity() }) - - suspend fun delete(itemId: Long) = dao.deleteById(itemId) - - suspend fun clear() = dao.clear() -} - -fun TransferHistoryItemEntity.toDomain() = - TransferHistoryItem( - id = id, - fileName = fileName, - fileSize = fileSize, - type = type, - timestamp = timestamp, - status = status, - peerName = peerName, - peerAvatar = peerAvatar, - fileCount = fileCount, - ) - -fun TransferHistoryItem.toEntity() = - TransferHistoryItemEntity( - id = id, - fileName = fileName, - fileSize = fileSize, - type = type, - timestamp = timestamp, - status = status, - peerName = peerName, - peerAvatar = peerAvatar, - fileCount = fileCount, - ) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferSessionLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferSessionLocalDataSource.kt new file mode 100644 index 0000000..751d244 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/TransferSessionLocalDataSource.kt @@ -0,0 +1,44 @@ +package dev.arkbuilders.drop.app.data.datasource + +import dev.arkbuilders.drop.app.data.db.dao.TransferSessionDao +import dev.arkbuilders.drop.app.data.db.entity.RoomTransferSession +import dev.arkbuilders.drop.app.domain.model.TransferSession +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class TransferSessionLocalDataSource( + private val dao: TransferSessionDao, +) { + fun flow(): Flow> = + dao.getAll().map { list -> list.map { it.toDomain() } } + + suspend fun add(item: TransferSession): Long = dao.insert(item.toEntity()) + + suspend fun addAll(items: List) = dao.insertAll(items.map { it.toEntity() }) + + suspend fun delete(itemId: Long) = dao.deleteById(itemId) + + suspend fun clear() = dao.clear() +} + +fun RoomTransferSession.toDomain() = + TransferSession( + id = id, + files = files, + type = type, + timestamp = timestamp, + status = status, + peerName = peerName, + peerAvatar = peerAvatar, + ) + +fun TransferSession.toEntity() = + RoomTransferSession( + id = id, + files = files, + type = type, + timestamp = timestamp, + status = status, + peerName = peerName, + peerAvatar = peerAvatar, + ) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt index e8dc1d1..4b87b2c 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt @@ -4,22 +4,24 @@ import android.content.Context import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import dev.arkbuilders.drop.app.data.db.dao.TransferHistoryItemDao -import dev.arkbuilders.drop.app.data.db.entity.TransferHistoryItemEntity +import dev.arkbuilders.drop.app.data.db.dao.TransferSessionDao +import dev.arkbuilders.drop.app.data.db.entity.RoomTransferSession +import dev.arkbuilders.drop.app.data.db.typeconverters.DropFileListConverter import dev.arkbuilders.drop.app.data.db.typeconverters.OffsetDateTimeTypeConverter @androidx.room.Database( entities = [ - TransferHistoryItemEntity::class, + RoomTransferSession::class, ], version = 1, exportSchema = true, ) @TypeConverters( OffsetDateTimeTypeConverter::class, + DropFileListConverter::class, ) abstract class Database : RoomDatabase() { - abstract fun transferHistoryDao(): TransferHistoryItemDao + abstract fun transferHistoryDao(): TransferSessionDao companion object { const val DB_NAME = "arkdrop.db" diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt deleted file mode 100644 index db90a81..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferHistoryItemDao.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.arkbuilders.drop.app.data.db.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import dev.arkbuilders.drop.app.data.db.entity.TransferHistoryItemEntity -import kotlinx.coroutines.flow.Flow - -@Dao -interface TransferHistoryItemDao { - @Query("SELECT * FROM TransferHistoryItemEntity ORDER BY timestamp DESC") - fun getAll(): Flow> - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(item: TransferHistoryItemEntity): Long - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAll(items: List) - - @Query("DELETE FROM TransferHistoryItemEntity WHERE id = :itemId") - suspend fun deleteById(itemId: Long) - - @Query("DELETE FROM TransferHistoryItemEntity") - suspend fun clear() -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferSessionDao.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferSessionDao.kt new file mode 100644 index 0000000..61a4fd8 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/dao/TransferSessionDao.kt @@ -0,0 +1,26 @@ +package dev.arkbuilders.drop.app.data.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import dev.arkbuilders.drop.app.data.db.entity.RoomTransferSession +import kotlinx.coroutines.flow.Flow + +@Dao +interface TransferSessionDao { + @Query("SELECT * FROM RoomTransferSession ORDER BY timestamp DESC") + fun getAll(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(item: RoomTransferSession): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(items: List) + + @Query("DELETE FROM RoomTransferSession WHERE id = :itemId") + suspend fun deleteById(itemId: Long) + + @Query("DELETE FROM RoomTransferSession") + suspend fun clear() +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/RoomTransferSession.kt similarity index 79% rename from app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt rename to app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/RoomTransferSession.kt index d85214b..3485112 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/TransferHistoryItemEntity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/entity/RoomTransferSession.kt @@ -2,20 +2,19 @@ package dev.arkbuilders.drop.app.data.db.entity import androidx.room.Entity import androidx.room.PrimaryKey +import dev.arkbuilders.drop.app.domain.model.DropFileInfo import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.model.TransferType import java.time.OffsetDateTime @Entity -data class TransferHistoryItemEntity( +data class RoomTransferSession( @PrimaryKey(autoGenerate = true) val id: Long = 0, - val fileName: String, - val fileSize: Long, + val files: List, val type: TransferType, val timestamp: OffsetDateTime, val status: TransferStatus, val peerName: String, val peerAvatar: String?, - val fileCount: Int, ) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/DropFileListConverter.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/DropFileListConverter.kt new file mode 100644 index 0000000..fc6d961 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/DropFileListConverter.kt @@ -0,0 +1,17 @@ +package dev.arkbuilders.drop.app.data.db.typeconverters + +import androidx.room.TypeConverter +import dev.arkbuilders.drop.app.domain.model.DropFileInfo +import kotlinx.serialization.json.Json + +object DropFileListConverter { + @TypeConverter + fun fromList(list: List): String { + return Json.encodeToString(list) + } + + @TypeConverter + fun toList(data: String): List { + return Json.decodeFromString(data) + } +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt index fac391c..cacc4f2 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt @@ -2,9 +2,10 @@ package dev.arkbuilders.drop.app.data.repository import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.app.domain.model.DropFileInfo import dev.arkbuilders.drop.app.domain.model.ReceiveSession import dev.arkbuilders.drop.app.domain.model.TransferStatus -import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,7 +18,7 @@ import timber.log.Timber class ReceiveSessionRepo( private val receiveFilesUseCase: ReceiveFilesUseCase, - private val transferHistoryRepository: TransferHistoryItemRepository, + private val transferHistoryRepository: TransferSessionRepo, private val resourcesHelper: ResourcesHelper, ) { // Keep references to active sessions here so file transfers continue even if the ViewModel dies @@ -59,13 +60,13 @@ class ReceiveSessionRepo( withContext(Dispatchers.IO) { val subscriber = session.subscriber val completeFiles = subscriber.getCompleteFiles() - val savedFiles = mutableListOf() + val savedFiles = mutableListOf() try { completeFiles.forEach { (fileInfo, data) -> val savedFile = resourcesHelper.saveFileToDownloads(fileInfo.name, data) if (savedFile != null) { - savedFiles.add(savedFile) + savedFiles.add(DropFileInfo(savedFile, fileInfo.size.toLong())) Timber.i("Saved file name: $savedFile") } else { Timber.e("Failed to save file: ${fileInfo.name}") @@ -77,15 +78,10 @@ class ReceiveSessionRepo( val senderName = progress.senderName val senderAvatar = progress.senderAvatar - val totalSize = completeFiles.sumOf { it.second.size.toLong() } - val firstFileName = savedFiles.firstOrNull() ?: "Unknown" - transferHistoryRepository.addReceivedTransfer( - fileName = firstFileName, - fileSize = totalSize, + files = savedFiles, peerName = senderName, peerAvatar = senderAvatar, - fileCount = savedFiles.size, status = TransferStatus.COMPLETED, ) } @@ -97,16 +93,14 @@ class ReceiveSessionRepo( val senderAvatar = progress.senderAvatar transferHistoryRepository.addReceivedTransfer( - fileName = "Transfer failed", - fileSize = 0L, + files = emptyList(), peerName = senderName, peerAvatar = senderAvatar, - fileCount = completeFiles.size, status = TransferStatus.FAILED, ) } - savedFiles + return@withContext savedFiles.map { it.name } } fun cancelReceive(session: ReceiveSession) { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt index 5608ec9..07beed7 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt @@ -3,9 +3,10 @@ package dev.arkbuilders.drop.app.data.repository import android.net.Uri import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl import dev.arkbuilders.drop.app.domain.ResourcesHelper +import dev.arkbuilders.drop.app.domain.model.DropFileInfo import dev.arkbuilders.drop.app.domain.model.SendSession import dev.arkbuilders.drop.app.domain.model.TransferStatus -import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -19,7 +20,7 @@ import timber.log.Timber class SendSessionRepo( private val sendUseCase: SendFilesUseCase, private val resourcesHelper: ResourcesHelper, - private val transferHistoryRepository: TransferHistoryItemRepository, + private val transferSessionRepository: TransferSessionRepo, ) { // Keep references to active sessions here so file transfers continue even if the ViewModel dies private val activeSessions = mutableListOf() @@ -59,21 +60,18 @@ class SendSessionRepo( val receiverName = progress.receiverName val receiverAvatar = progress.receiverAvatar - val totalSize = - fileUris.sumOf { uri -> - resourcesHelper.getFileSize(uri.toString()) + val filesInfo = + fileUris.map { + DropFileInfo( + name = resourcesHelper.getFileName(it.toString()) ?: "", + size = resourcesHelper.getFileSize(it.toString()), + ) } - val firstFileName = - fileUris.firstOrNull()?.let { resourcesHelper.getFileName(it.toString()) } - ?: "Unknown" - - transferHistoryRepository.addSentTransfer( - fileName = firstFileName, - fileSize = totalSize, + transferSessionRepository.addSentTransfer( + files = filesInfo, peerName = receiverName, peerAvatar = receiverAvatar, - fileCount = fileUris.size, status = TransferStatus.COMPLETED, ) } catch (e: Exception) { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferSessionRepoImpl.kt similarity index 52% rename from app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt rename to app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferSessionRepoImpl.kt index 6284a70..a74867e 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferHistoryItemRepositoryImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/TransferSessionRepoImpl.kt @@ -1,63 +1,56 @@ package dev.arkbuilders.drop.app.data.repository -import dev.arkbuilders.drop.app.data.datasource.TransferHistoryItemLocalDataSource -import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.data.datasource.TransferSessionLocalDataSource +import dev.arkbuilders.drop.app.domain.model.DropFileInfo +import dev.arkbuilders.drop.app.domain.model.TransferSession import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.model.TransferType -import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo import kotlinx.coroutines.flow.Flow import java.time.OffsetDateTime -class TransferHistoryItemRepositoryImpl( - private val localSource: TransferHistoryItemLocalDataSource, -) : TransferHistoryItemRepository { - override val historyItems: Flow> = localSource.flow() +class TransferSessionRepoImpl( + private val localSource: TransferSessionLocalDataSource, +) : TransferSessionRepo { + override val historyItems: Flow> = localSource.flow() override suspend fun addSentTransfer( - fileName: String, - fileSize: Long, + files: List, peerName: String, peerAvatar: String?, - fileCount: Int, status: TransferStatus, ) { val newItem = - TransferHistoryItem( - fileName = if (fileCount > 1) "$fileName and ${fileCount - 1} more" else fileName, - fileSize = fileSize, + TransferSession( + files = files, type = TransferType.SENT, timestamp = OffsetDateTime.now(), status = status, peerName = peerName, peerAvatar = peerAvatar, - fileCount = fileCount, ) localSource.add(newItem) } override suspend fun addReceivedTransfer( - fileName: String, - fileSize: Long, + files: List, peerName: String, peerAvatar: String?, - fileCount: Int, status: TransferStatus, ) { val newItem = - TransferHistoryItem( - fileName = if (fileCount > 1) "$fileName and ${fileCount - 1} more" else fileName, - fileSize = fileSize, + TransferSession( + files = files, type = TransferType.RECEIVED, timestamp = OffsetDateTime.now(), status = status, peerName = peerName, peerAvatar = peerAvatar, - fileCount = fileCount, ) localSource.add(newItem) } - override suspend fun deleteHistoryItem(itemId: Long) { + override suspend fun deleteSession(itemId: Long) { localSource.delete(itemId) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index 8ce6693..14b38eb 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -1,9 +1,9 @@ package dev.arkbuilders.drop.app.di import dev.arkbuilders.drop.app.data.datasource.ProfileLocalDataSource -import dev.arkbuilders.drop.app.data.datasource.TransferHistoryItemLocalDataSource +import dev.arkbuilders.drop.app.data.datasource.TransferSessionLocalDataSource import dev.arkbuilders.drop.app.data.db.Database -import dev.arkbuilders.drop.app.data.db.dao.TransferHistoryItemDao +import dev.arkbuilders.drop.app.data.db.dao.TransferSessionDao import dev.arkbuilders.drop.app.data.helper.AvatarHelperImpl import dev.arkbuilders.drop.app.data.helper.PermissionsHelperImpl import dev.arkbuilders.drop.app.data.helper.ResourcesHelperImpl @@ -11,13 +11,13 @@ import dev.arkbuilders.drop.app.data.repository.NetworkStatusImpl import dev.arkbuilders.drop.app.data.repository.ProfileRepoImpl import dev.arkbuilders.drop.app.data.repository.ReceiveSessionRepo import dev.arkbuilders.drop.app.data.repository.SendSessionRepo -import dev.arkbuilders.drop.app.data.repository.TransferHistoryItemRepositoryImpl +import dev.arkbuilders.drop.app.data.repository.TransferSessionRepoImpl import dev.arkbuilders.drop.app.domain.AvatarHelper import dev.arkbuilders.drop.app.domain.PermissionsHelper import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.domain.repository.NetworkStatus import dev.arkbuilders.drop.app.domain.repository.ProfileRepo -import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase import org.koin.dsl.module @@ -27,15 +27,15 @@ val appModule = single { ProfileRepoImpl(get(), get()) } single { ResourcesHelperImpl(get()) } single { Database.build(get()) } - single { TransferHistoryItemRepositoryImpl(get()) } + single { TransferSessionRepoImpl(get()) } single { PermissionsHelperImpl(get()) } single { NetworkStatusImpl(get()) } single { AvatarHelperImpl(get()) } single { ProfileLocalDataSource(get(), get()) } - single { TransferHistoryItemLocalDataSource(get()) } + single { TransferSessionLocalDataSource(get()) } single { SendSessionRepo(get(), get(), get()) } single { ReceiveSessionRepo(get(), get(), get()) } - factory { + factory { val db: Database = get() db.transferHistoryDao() } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/DropFileInfo.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/DropFileInfo.kt new file mode 100644 index 0000000..c1f6708 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/DropFileInfo.kt @@ -0,0 +1,9 @@ +package dev.arkbuilders.drop.app.domain.model + +import kotlinx.serialization.Serializable + +@Serializable +data class DropFileInfo( + val name: String, + val size: Long, +) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferSession.kt similarity index 70% rename from app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt rename to app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferSession.kt index 4c45f41..24a855a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferHistoryItem.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/TransferSession.kt @@ -2,14 +2,12 @@ package dev.arkbuilders.drop.app.domain.model import java.time.OffsetDateTime -data class TransferHistoryItem( +data class TransferSession( val id: Long = 0, - val fileName: String, - val fileSize: Long, + val files: List, val type: TransferType, val timestamp: OffsetDateTime, val status: TransferStatus, val peerName: String, val peerAvatar: String?, - val fileCount: Int, ) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferSessionRepo.kt similarity index 57% rename from app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt rename to app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferSessionRepo.kt index f9353d7..604bf0f 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferHistoryItemRepository.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/TransferSessionRepo.kt @@ -1,31 +1,28 @@ package dev.arkbuilders.drop.app.domain.repository -import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.domain.model.DropFileInfo +import dev.arkbuilders.drop.app.domain.model.TransferSession import dev.arkbuilders.drop.app.domain.model.TransferStatus import kotlinx.coroutines.flow.Flow -interface TransferHistoryItemRepository { - val historyItems: Flow> +interface TransferSessionRepo { + val historyItems: Flow> suspend fun addSentTransfer( - fileName: String, - fileSize: Long, + files: List, peerName: String, peerAvatar: String?, - fileCount: Int = 1, status: TransferStatus = TransferStatus.COMPLETED, ) suspend fun addReceivedTransfer( - fileName: String, - fileSize: Long, + files: List, peerName: String, peerAvatar: String?, - fileCount: Int = 1, status: TransferStatus = TransferStatus.COMPLETED, ) - suspend fun deleteHistoryItem(itemId: Long) + suspend fun deleteSession(itemId: Long) suspend fun clearHistory() } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt index bd29728..12057e0 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt @@ -16,7 +16,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink import dev.arkbuilders.drop.app.domain.repository.ProfileRepo -import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo import dev.arkbuilders.drop.app.presentation.history.History import dev.arkbuilders.drop.app.presentation.home.Home import dev.arkbuilders.drop.app.presentation.navigation.DropDestination @@ -29,7 +29,7 @@ import org.koin.android.ext.android.get class MainActivity : ComponentActivity() { private val profileRepo: ProfileRepo = get() - private val transferHistoryItemRepository: TransferHistoryItemRepository = get() + private val transferSessionRepo: TransferSessionRepo = get() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -47,7 +47,7 @@ class MainActivity : ComponentActivity() { Modifier .padding(innerPadding), profileRepo = profileRepo, - transferHistoryItemRepository = transferHistoryItemRepository, + transferSessionRepo = transferSessionRepo, ) } } @@ -60,7 +60,7 @@ fun DropNavigation( modifier: Modifier = Modifier, navController: NavHostController = rememberNavController(), profileRepo: ProfileRepo, - transferHistoryItemRepository: TransferHistoryItemRepository, + transferSessionRepo: TransferSessionRepo, ) { NavHost( navController = navController, @@ -71,7 +71,7 @@ fun DropNavigation( Home( navController = navController, profileRepo = profileRepo, - transferHistoryItemRepository = transferHistoryItemRepository, + transferSessionRepo = transferSessionRepo, ) } composable(DropDestination.Send.route) { @@ -95,7 +95,7 @@ fun DropNavigation( composable(DropDestination.History.route) { History( navController = navController, - transferHistoryItemRepository = transferHistoryItemRepository, + transferSessionRepo = transferSessionRepo, ) } composable(DropDestination.EditProfile.route) { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt index 3d17589..a1259ce 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt @@ -35,17 +35,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.ClearAll import compose.icons.tablericons.FileDownload import compose.icons.tablericons.FileUpload import compose.icons.tablericons.History -import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.domain.model.TransferSession import dev.arkbuilders.drop.app.domain.model.TransferStatus import dev.arkbuilders.drop.app.domain.model.TransferType -import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo import dev.arkbuilders.drop.app.presentation.components.AvatarImageWithFallback import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack import org.koin.compose.koinInject @@ -59,7 +58,7 @@ import java.util.Locale @Composable fun History( navController: NavController, - transferHistoryItemRepository: TransferHistoryItemRepository, + transferSessionRepo: TransferSessionRepo, ) { val viewModel: HistoryViewModel = koinInject() @@ -189,7 +188,7 @@ fun History( @Composable private fun HistoryItemCard( state: HistoryScreenState, - item: TransferHistoryItem, + item: TransferSession, onShowDeleteDialog: () -> Unit, onDismissDeleteDialog: () -> Unit, onDelete: () -> Unit, @@ -258,7 +257,7 @@ private fun HistoryItemCard( Spacer(modifier = Modifier.height(4.dp)) Text( - text = item.fileName, + text = item.files.firstOrNull()?.name ?: "", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, maxLines = 1, @@ -273,7 +272,7 @@ private fun HistoryItemCard( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = formatFileSize(item.fileSize), + text = formatFileSize(item.files.sumOf { it.size }), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -290,7 +289,7 @@ private fun HistoryItemCard( color = MaterialTheme.colorScheme.onSurfaceVariant, ) - if (item.fileCount > 1) { + if (item.files.size > 1) { Text( text = "•", style = MaterialTheme.typography.bodyMedium, @@ -298,7 +297,7 @@ private fun HistoryItemCard( ) Text( - text = "${item.fileCount} files", + text = "${item.files.size} files", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Medium, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt index bb20a12..b9b6a70 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/HistoryViewModel.kt @@ -2,8 +2,8 @@ package dev.arkbuilders.drop.app.presentation.history import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem -import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.model.TransferSession +import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.orbitmvi.orbit.Container @@ -11,7 +11,7 @@ import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container data class HistoryScreenState( - val historyItems: List, + val historyItems: List, val showClearDialog: Boolean, val showDeleteDialog: Boolean, ) @@ -19,7 +19,7 @@ data class HistoryScreenState( sealed class HistoryScreenEffect class HistoryViewModel( - private val historyItemRepository: TransferHistoryItemRepository, + private val historyItemRepository: TransferSessionRepo, ) : ViewModel(), ContainerHost { override val container: Container = container( @@ -71,7 +71,7 @@ class HistoryViewModel( fun onDelete(id: Long) = intent { - historyItemRepository.deleteHistoryItem(id) + historyItemRepository.deleteSession(id) reduce { state.copy(showDeleteDialog = false) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt index 8bb7512..84f85f3 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import compose.icons.TablerIcons import compose.icons.tablericons.ArrowDownCircle @@ -44,11 +43,11 @@ import compose.icons.tablericons.CloudDownload import compose.icons.tablericons.CloudUpload import compose.icons.tablericons.History import dev.arkbuilders.drop.app.R -import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.domain.model.TransferSession import dev.arkbuilders.drop.app.domain.model.TransferType import dev.arkbuilders.drop.app.domain.model.UserProfile import dev.arkbuilders.drop.app.domain.repository.ProfileRepo -import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo import dev.arkbuilders.drop.app.presentation.components.AvatarImage import dev.arkbuilders.drop.app.presentation.components.AvatarImageWithFallback import dev.arkbuilders.drop.app.presentation.components.DropButton @@ -74,7 +73,7 @@ import java.util.Locale fun Home( navController: NavController, profileRepo: ProfileRepo, - transferHistoryItemRepository: TransferHistoryItemRepository, + transferSessionRepo: TransferSessionRepo, ) { val viewModel: HomeViewModel = koinInject() val state by viewModel.collectAsState() @@ -263,7 +262,7 @@ private fun QuickActionsSection( @Composable private fun RecentTransfersSection( - historyItems: List, + historyItems: List, onViewAllClick: () -> Unit, showViewAll: Boolean, ) { @@ -326,7 +325,7 @@ private fun EmptyTransfersSection() { } @Composable -private fun EnhancedTransferHistoryCard(item: TransferHistoryItem) { +private fun EnhancedTransferHistoryCard(item: TransferSession) { DropCard( variant = DropCardVariant.Elevated, size = DropCardSize.Medium, @@ -372,8 +371,8 @@ private fun EnhancedTransferHistoryCard(item: TransferHistoryItem) { ) Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xs)) Text( - text = "${item.fileCount} file${ - if (item.fileCount != 1) "s" else "" + text = "${item.files.size} file${ + if (item.files.size != 1) "s" else "" } • ${formatTimestamp( item.timestamp, )}", diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt index cc1fa95..e88adaa 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt @@ -2,10 +2,10 @@ package dev.arkbuilders.drop.app.presentation.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dev.arkbuilders.drop.app.domain.model.TransferHistoryItem +import dev.arkbuilders.drop.app.domain.model.TransferSession import dev.arkbuilders.drop.app.domain.model.UserProfile import dev.arkbuilders.drop.app.domain.repository.ProfileRepo -import dev.arkbuilders.drop.app.domain.repository.TransferHistoryItemRepository +import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -14,14 +14,14 @@ import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container data class HomeScreenState( - val historyItems: List, + val historyItems: List, val profile: UserProfile, ) sealed class HomeScreenEffect class HomeViewModel( - private val historyItemRepository: TransferHistoryItemRepository, + private val historyItemRepository: TransferSessionRepo, private val profileRepo: ProfileRepo, ) : ViewModel(), ContainerHost { override val container: Container = diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt index c99daf6..ca8c726 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.onEach import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container +import timber.log.Timber class ReceiveViewModel( private val receiveSessionRepo: ReceiveSessionRepo, @@ -299,6 +300,7 @@ class ReceiveViewModel( } } } catch (e: Exception) { + Timber.w("Save failed: ${e::class.simpleName} ${e.message}") val error = when { e.message?.contains("storage", ignoreCase = true) == true -> From c302655c868fc72871b4da5db17bc796273f625f Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 18 Nov 2025 02:13:09 +0600 Subject: [PATCH 22/33] Make Send screen scrollable, remove LazyColumn from phases --- .../drop/app/presentation/receive/Receive.kt | 5 +- .../drop/app/presentation/send/Send.kt | 6 +- .../components/phase/FileSelectionPhase.kt | 201 +++++++------- .../components/phase/TransferringPhase.kt | 255 +++++++++--------- .../phase/WaitingForReceiverPhase.kt | 110 ++++---- 5 files changed, 279 insertions(+), 298 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt index ce76e2b..1f07bbd 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt @@ -23,8 +23,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.Card @@ -125,7 +127,8 @@ fun Receive(navController: NavController) { Column( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .verticalScroll(rememberScrollState()), ) { DropTopBarBack( title = "Receive files", diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt index fa9d805..f98f8dd 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt @@ -17,7 +17,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -92,7 +94,8 @@ fun Send(navController: NavController) { Column( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .verticalScroll(rememberScrollState()), ) { DropTopBarBack( title = "Send files", @@ -115,7 +118,6 @@ fun Send(navController: NavController) { viewModel.onStartTransfer() }, canStartTransfer = sendScreenState.canStartTransfer, - listState = listState, ) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt index a539f96..1441a5b 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt @@ -2,10 +2,8 @@ package dev.arkbuilders.drop.app.presentation.send.components.phase import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn @@ -13,7 +11,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -43,129 +40,117 @@ fun FileSelectionPhase( onRemoveFile: (String) -> Unit, onStartTransfer: () -> Unit, canStartTransfer: Boolean, - listState: LazyListState, ) { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(20.dp), + Column( + modifier = Modifier.fillMaxWidth().padding(20.dp), verticalArrangement = Arrangement.spacedBy(24.dp), ) { - // File selection section - item { - SendCard { - Column( - modifier = Modifier.padding(20.dp), + SendCard { + Column( + modifier = Modifier.padding(20.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column { - Text( - text = "Selected Files", - style = - MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurface, - ) - - if (selectedFiles.isNotEmpty()) { - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "${selectedFiles.size} file${ - if (selectedFiles.size != 1) "s" else "" - } • ${ - formatBytes( - totalFileSize, - ) - }", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } + Column { + Text( + text = "Selected Files", + style = + MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) - SendButton( - onClick = onAddFiles, - variant = ButtonVariant.Secondary, - size = ButtonSize.Medium, - ) { - Icon( - TablerIcons.Plus, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(modifier = Modifier.width(8.dp)) + if (selectedFiles.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) Text( - "Add Files", - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Medium, - ), + text = "${selectedFiles.size} file${ + if (selectedFiles.size != 1) "s" else "" + } • ${ + formatBytes( + totalFileSize, + ) + }", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } - Spacer(modifier = Modifier.height(20.dp)) - - if (selectedFiles.isEmpty()) { - SendEmptyState( - title = "No Files Selected", - description = "Tap 'Add Files' to choose files you want to send.", - icon = TablerIcons.FileText, + SendButton( + onClick = onAddFiles, + variant = ButtonVariant.Secondary, + size = ButtonSize.Medium, + ) { + Icon( + TablerIcons.Plus, + contentDescription = null, + modifier = Modifier.size(18.dp), ) - } else { - LazyColumn( - modifier = Modifier.heightIn(max = 300.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - items(selectedFiles) { uri -> - SendFileItem( - uri = uri, - onRemove = { onRemoveFile(uri) }, - ) - } + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Add Files", + style = + MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Medium, + ), + ) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + if (selectedFiles.isEmpty()) { + SendEmptyState( + title = "No Files Selected", + description = "Tap 'Add Files' to choose files you want to send.", + icon = TablerIcons.FileText, + ) + } else { + LazyColumn( + modifier = Modifier.heightIn(max = 300.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items(selectedFiles) { uri -> + SendFileItem( + uri = uri, + onRemove = { onRemoveFile(uri) }, + ) } } } } } - // Transfer button - item { - SendButton( - onClick = onStartTransfer, - variant = ButtonVariant.Primary, - size = ButtonSize.Large, - enabled = canStartTransfer, - modifier = - Modifier - .fillMaxWidth() - .height(56.dp), - ) { - Text( - text = - when { - selectedFiles.isEmpty() -> "Select Files First" - else -> - "Send ${selectedFiles.size} File${ - if (selectedFiles.size != 1) "s" else "" - }" - }, - style = - MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - } + SendButton( + onClick = onStartTransfer, + variant = ButtonVariant.Primary, + size = ButtonSize.Large, + enabled = canStartTransfer, + modifier = + Modifier + .fillMaxWidth() + .height(56.dp), + ) { + Text( + text = + when { + selectedFiles.isEmpty() -> "Select Files First" + else -> + "Send ${selectedFiles.size} File${ + if (selectedFiles.size != 1) "s" else "" + }" + }, + style = + MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + ) } - // Instructions - item { - SendInstructionsCard() - } + SendInstructionsCard() } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt index 803c88b..47668d4 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferringPhase.kt @@ -2,15 +2,12 @@ package dev.arkbuilders.drop.app.presentation.send.components.phase import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close @@ -34,151 +31,153 @@ import dev.arkbuilders.drop.app.presentation.send.components.SendProgressBar @Composable fun TransferringPhase( - progress: SendScreenState.Transfer?, + progress: SendScreenState.Transfer, onCancel: () -> Unit, ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(20.dp), + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(20.dp), verticalArrangement = Arrangement.spacedBy(24.dp), ) { - item { - progress?.let { p -> - SendCard( - backgroundColor = MaterialTheme.colorScheme.primaryContainer, + SendCard( + backgroundColor = MaterialTheme.colorScheme.primaryContainer, + ) { + Column( + modifier = Modifier.padding(24.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { - Column( - modifier = Modifier.padding(24.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "Sending Files", - style = - MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold, - ), - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) + Text( + text = "Sending Files", + style = + MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) - IconButton( - onClick = onCancel, - modifier = - Modifier - .size(32.dp) - .clip(CircleShape), - ) { - Icon( - Icons.Default.Close, - contentDescription = "Cancel transfer", - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } - } + IconButton( + onClick = onCancel, + modifier = + Modifier + .size(32.dp) + .clip(CircleShape), + ) { + Icon( + Icons.Default.Close, + contentDescription = "Cancel transfer", + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + } - if (p.receiverName.isNotEmpty()) { - Spacer(modifier = Modifier.height(16.dp)) + if (progress.receiverName.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - AvatarImageWithFallback( - avatarB64 = p.receiverAvatar, - fallbackText = p.receiverName, - size = 32.dp, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + AvatarImageWithFallback( + avatarB64 = progress.receiverAvatar, + fallbackText = progress.receiverName, + size = 32.dp, + ) - Text( - text = "Connected to: ${p.receiverName}", - style = MaterialTheme.typography.bodyLarge, - color = - MaterialTheme.colorScheme.onPrimaryContainer.copy( - alpha = 0.8f, - ), - ) - } - } + Text( + text = "Connected to: ${progress.receiverName}", + style = MaterialTheme.typography.bodyLarge, + color = + MaterialTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.8f, + ), + ) + } + } - if (p.currentFileName.isNotEmpty()) { - Spacer(modifier = Modifier.height(20.dp)) + if (progress.currentFileName.isNotEmpty()) { + Spacer(modifier = Modifier.height(20.dp)) - Text( - text = "Sending: ${p.currentFileName}", - style = - MaterialTheme.typography.bodyLarge.copy( - fontWeight = FontWeight.Medium, - ), - color = MaterialTheme.colorScheme.onPrimaryContainer, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Text( + text = "Sending: ${progress.currentFileName}", + style = + MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.onPrimaryContainer, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) - val progressValue = - if (p.totalBytes > 0) { - (p.bytesTransferred.toFloat() / p.totalBytes.toFloat()) - .coerceIn( - 0f, - 1f, - ) - } else { - 0f - } + val progressValue = + if (progress.totalBytes > 0) { + (progress.bytesTransferred.toFloat() / progress.totalBytes.toFloat()) + .coerceIn( + 0f, + 1f, + ) + } else { + 0f + } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - SendProgressBar( - progress = progressValue, - modifier = Modifier.fillMaxWidth(), - ) + SendProgressBar( + progress = progressValue, + modifier = Modifier.fillMaxWidth(), + ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "${formatBytes( - p.bytesTransferred, - )} / ${formatBytes(p.totalBytes)}", - style = MaterialTheme.typography.bodyMedium, - color = - MaterialTheme.colorScheme.onPrimaryContainer.copy( - alpha = 0.7f, - ), + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "${ + formatBytes( + progress.bytesTransferred, ) + } / ${formatBytes(progress.totalBytes)}", + style = MaterialTheme.typography.bodyMedium, + color = + MaterialTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.7f, + ), + ) - if (p.transferSpeedBps > 0) { - Text( - text = "${formatBytes(p.transferSpeedBps)}/s", - style = MaterialTheme.typography.bodyMedium, - color = - MaterialTheme.colorScheme.onPrimaryContainer.copy( - alpha = 0.7f, - ), - ) - } - } + if (progress.transferSpeedBps > 0) { + Text( + text = "${formatBytes(progress.transferSpeedBps)}/s", + style = MaterialTheme.typography.bodyMedium, + color = + MaterialTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.7f, + ), + ) + } + } - if (p.estimatedTimeRemaining > 0) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Time remaining: ${formatDuration( - p.estimatedTimeRemaining, - )}", - style = MaterialTheme.typography.bodySmall, - color = - MaterialTheme.colorScheme.onPrimaryContainer.copy( - alpha = 0.6f, - ), + if (progress.estimatedTimeRemaining > 0) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Time remaining: ${ + formatDuration( + progress.estimatedTimeRemaining, ) - } - } + }", + style = MaterialTheme.typography.bodySmall, + color = + MaterialTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.6f, + ), + ) } } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt index 54e2780..0a9e2e3 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt @@ -2,14 +2,11 @@ package dev.arkbuilders.drop.app.presentation.send.components.phase import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -29,74 +26,69 @@ fun WaitingForReceiverPhase( fileCount: Int, onCancel: () -> Unit, ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(20.dp), + Column( + modifier = Modifier.fillMaxWidth().padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(24.dp), ) { - item { - SendCard { - Column( - modifier = Modifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, + SendCard { + Column( + modifier = Modifier.padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Ready to Send", + style = + MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = + "$fileCount file${if (fileCount != 1) "s" else ""}" + + " ready for transfer", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { + SendLoadingIndicator() Text( - text = "Ready to Send", + text = "Waiting for receiver to scan...", style = - MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.Bold, + MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, ), - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary, ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = - "$fileCount file${if (fileCount != 1) "s" else ""}" + - " ready for transfer", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - - Spacer(modifier = Modifier.height(32.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - SendLoadingIndicator() - Text( - text = "Waiting for receiver to scan...", - style = - MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium, - ), - color = MaterialTheme.colorScheme.primary, - ) - } } } } - item { - SendButton( - onClick = onCancel, - variant = ButtonVariant.Secondary, - size = ButtonSize.Large, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - "Cancel Transfer", - style = - MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Medium, - ), - ) - } + SendButton( + onClick = onCancel, + variant = ButtonVariant.Secondary, + size = ButtonSize.Large, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + "Cancel Transfer", + style = + MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Medium, + ), + ) } } } From 8a06cc14d0e4771a15dc843f3660f27abe9a21f0 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 18 Nov 2025 03:20:37 +0600 Subject: [PATCH 23/33] Extract DropInstructionsCard --- .../components/DropInstructionsCard.kt | 70 ++++++++++++++++ .../drop/app/presentation/receive/Receive.kt | 61 -------------- .../ReceivePermissionRequestCard.kt | 13 +++ .../components/ReceiveReadyToScanCard.kt | 13 +++ .../send/components/SendInstructionsCard.kt | 84 ------------------- .../components/phase/FileSelectionPhase.kt | 14 +++- 6 files changed, 108 insertions(+), 147 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropInstructionsCard.kt delete mode 100644 app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropInstructionsCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropInstructionsCard.kt new file mode 100644 index 0000000..e938e7f --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/components/DropInstructionsCard.kt @@ -0,0 +1,70 @@ +package dev.arkbuilders.drop.app.presentation.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import dev.arkbuilders.drop.app.presentation.theme.DesignTokens + +@Composable +fun DropInstructionsCard( + modifier: Modifier = Modifier, + title: String, + steps: List, +) { + Card( + modifier = modifier, + colors = + CardDefaults.cardColors( + containerColor = + MaterialTheme.colorScheme.surfaceVariant.copy( + alpha = 0.5f, + ), + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), + ) { + Column( + modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) + + steps.forEachIndexed { index, step -> + Row( + verticalAlignment = Alignment.Top, + modifier = Modifier.padding(vertical = 2.dp), + ) { + Text( + text = "${index + 1}.", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) + Text( + text = step, + style = MaterialTheme.typography.bodyMedium, + lineHeight = MaterialTheme.typography.bodyMedium.lineHeight * 1.2, + ) + } + } + } + } +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt index 1f07bbd..5a87933 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt @@ -15,21 +15,18 @@ import androidx.compose.animation.scaleOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api @@ -335,64 +332,6 @@ fun Receive(navController: NavController) { ) } } - - if (state !is ReceiveScreenState.Success && - state !is ReceiveScreenState.Error - ) { - Spacer(modifier = Modifier.weight(1f)) - - Card( - modifier = Modifier.padding(16.dp), - colors = - CardDefaults.cardColors( - containerColor = - MaterialTheme.colorScheme.surfaceVariant.copy( - alpha = 0.5f, - ), - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ), - shape = RoundedCornerShape(DesignTokens.CornerRadius.lg), - ) { - Column( - modifier = Modifier.Companion.padding(DesignTokens.Spacing.lg), - ) { - Text( - text = "How to receive files:", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.md)) - - val steps = - listOf( - "Ask the sender to start a transfer", - "Scan QR code OR enter transfer code manually", - "Accept the transfer", - "Files will be saved to your Downloads folder", - ) - - steps.forEachIndexed { index, step -> - Row( - verticalAlignment = Alignment.Top, - modifier = Modifier.padding(vertical = 2.dp), - ) { - Text( - text = "${index + 1}.", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.sm)) - Text( - text = step, - style = MaterialTheme.typography.bodyMedium, - lineHeight = MaterialTheme.typography.bodyMedium.lineHeight * 1.2, - ) - } - } - } - } - } } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt index e9a31a3..b939654 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceivePermissionRequestCard.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp import compose.icons.TablerIcons import compose.icons.tablericons.ArrowForward import compose.icons.tablericons.Camera +import dev.arkbuilders.drop.app.presentation.components.DropInstructionsCard import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable @@ -144,4 +145,16 @@ fun ReceivePermissionRequestCard( } } } + + DropInstructionsCard( + modifier = Modifier.fillMaxWidth().padding(16.dp), + title = "How to receive files:", + steps = + listOf( + "Ask the sender to start a transfer", + "Scan QR code OR enter transfer code manually", + "Accept the transfer", + "Files will be saved to your Downloads folder", + ), + ) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt index ed187c4..75e8284 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/components/ReceiveReadyToScanCard.kt @@ -30,6 +30,7 @@ import compose.icons.TablerIcons import compose.icons.tablericons.ArrowForward import compose.icons.tablericons.Camera import compose.icons.tablericons.Qrcode +import dev.arkbuilders.drop.app.presentation.components.DropInstructionsCard import dev.arkbuilders.drop.app.presentation.theme.DesignTokens @Composable @@ -158,4 +159,16 @@ fun ReceiveReadyToScanCard( } } } + + DropInstructionsCard( + modifier = Modifier.fillMaxWidth().padding(16.dp), + title = "How to receive files:", + steps = + listOf( + "Ask the sender to start a transfer", + "Scan QR code OR enter transfer code manually", + "Accept the transfer", + "Files will be saved to your Downloads folder", + ), + ) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt deleted file mode 100644 index 126b8c9..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/SendInstructionsCard.kt +++ /dev/null @@ -1,84 +0,0 @@ -package dev.arkbuilders.drop.app.presentation.send.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp - -@Composable -fun SendInstructionsCard() { - SendCard( - backgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), - ) { - Column( - modifier = Modifier.padding(20.dp), - ) { - Text( - text = "How to Send Files", - style = - MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Bold, - ), - color = MaterialTheme.colorScheme.onSurface, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - val instructions = - listOf( - "Select files you want to send", - "Tap 'Send Files' to generate QR code", - "Let the receiver scan the QR code", - "Files transfer automatically", - ) - - instructions.forEachIndexed { index, instruction -> - Row( - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Surface( - modifier = Modifier.size(20.dp), - shape = CircleShape, - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - ) { - Box(contentAlignment = Alignment.Center) { - Text( - text = "${index + 1}", - style = - MaterialTheme.typography.labelSmall.copy( - fontWeight = FontWeight.Bold, - ), - color = MaterialTheme.colorScheme.primary, - ) - } - } - - Text( - text = instruction, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 2.dp), - ) - } - - if (index < instructions.size - 1) { - Spacer(modifier = Modifier.height(12.dp)) - } - } - } - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt index 1441a5b..ce5d626 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/FileSelectionPhase.kt @@ -24,13 +24,13 @@ import compose.icons.TablerIcons import compose.icons.tablericons.FileText import compose.icons.tablericons.Plus import dev.arkbuilders.drop.app.presentation.DisplayUtils.formatBytes +import dev.arkbuilders.drop.app.presentation.components.DropInstructionsCard import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant import dev.arkbuilders.drop.app.presentation.send.components.SendButton import dev.arkbuilders.drop.app.presentation.send.components.SendCard import dev.arkbuilders.drop.app.presentation.send.components.SendEmptyState import dev.arkbuilders.drop.app.presentation.send.components.SendFileItem -import dev.arkbuilders.drop.app.presentation.send.components.SendInstructionsCard @Composable fun FileSelectionPhase( @@ -151,6 +151,16 @@ fun FileSelectionPhase( ) } - SendInstructionsCard() + DropInstructionsCard( + modifier = Modifier.fillMaxWidth(), + title = "How to send files:", + steps = + listOf( + "Select files you want to send", + "Tap 'Send Files' to generate QR code", + "Let the receiver scan the QR code", + "Files transfer automatically", + ), + ) } } From 9d110c47a79152cd1ac558360f67b126cd9f7041 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 18 Nov 2025 03:29:02 +0600 Subject: [PATCH 24/33] Remove duplicate Success card --- .../drop/app/presentation/receive/Receive.kt | 174 +----------------- .../receive/ReceiveScreenState.kt | 2 - .../presentation/receive/ReceiveViewModel.kt | 1 - .../components/phase/TransferCompletePhase.kt | 40 ---- 4 files changed, 9 insertions(+), 208 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt index 5a87933..318d041 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/Receive.kt @@ -3,53 +3,17 @@ package dev.arkbuilders.drop.app.presentation.receive import android.Manifest import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import dev.arkbuilders.drop.app.presentation.components.DropErrorCard @@ -62,8 +26,6 @@ import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveProgressC import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveQRCodeScannedCard import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveReadyToScanCard import dev.arkbuilders.drop.app.presentation.receive.components.ReceiveScanningCard -import dev.arkbuilders.drop.app.presentation.theme.DesignTokens -import kotlinx.coroutines.delay import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -96,31 +58,9 @@ fun Receive(navController: NavController) { ReceiveScreenEffect.RequestCameraPermission -> { requestPermissionLauncher.launch(Manifest.permission.CAMERA) } - - ReceiveScreenEffect.ShowSuccessAnimation -> { - } } } - var showSuccessAnimation by remember { mutableStateOf(false) } - - LaunchedEffect(showSuccessAnimation) { - if (showSuccessAnimation) { - delay(3000) - showSuccessAnimation = false - } - } - - val successScale by animateFloatAsState( - targetValue = if (showSuccessAnimation) 1f else 0f, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow, - ), - label = "successScale", - ) - Column( modifier = Modifier @@ -132,100 +72,6 @@ fun Receive(navController: NavController) { onBackClick = { navController.navigateUp() }, ) - Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xl)) - - AnimatedVisibility( - visible = showSuccessAnimation, - enter = - scaleIn( - animationSpec = - spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow, - ), - ) + fadeIn(), - exit = - scaleOut( - animationSpec = tween(DesignTokens.Animation.NORMAL), - ) + fadeOut(), - ) { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - ElevatedCard( - modifier = - Modifier - .fillMaxWidth() - .scale(successScale), - shape = RoundedCornerShape(DesignTokens.CornerRadius.xl), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - elevation = - CardDefaults.elevatedCardElevation( - defaultElevation = DesignTokens.Elevation.xl, - ), - ) { - Column( - modifier = Modifier.Companion.padding(DesignTokens.Spacing.xxl), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box( - modifier = - Modifier - .size(80.dp) - .background( - brush = - Brush.radialGradient( - colors = - listOf( - MaterialTheme.colorScheme.primary.copy( - alpha = 0.2f, - ), - Color.Transparent, - ), - ), - shape = CircleShape, - ) - .clip(CircleShape), - contentAlignment = Alignment.Center, - ) { - Icon( - Icons.Default.CheckCircle, - contentDescription = "Success", - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary, - ) - } - - Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.lg)) - - Text( - text = "Files Received!", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - ) - - Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.sm)) - - Text( - text = - "All files have been successfully saved to your" + - " Downloads folder.", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f), - lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2, - ) - } - } - } - } - val receiveScreenState = state when (receiveScreenState) { is ReceiveScreenState.Initial -> { @@ -307,17 +153,15 @@ fun Receive(navController: NavController) { } is ReceiveScreenState.Success -> { - if (!showSuccessAnimation) { - ReceiveCompleteCard( - receivedFiles = receiveScreenState.receivedFiles, - onReceiveMore = { - viewModel.onReceiveMore() - }, - onDone = { - viewModel.onDone() - }, - ) - } + ReceiveCompleteCard( + receivedFiles = receiveScreenState.receivedFiles, + onReceiveMore = { + viewModel.onReceiveMore() + }, + onDone = { + viewModel.onDone() + }, + ) } is ReceiveScreenState.Error -> { diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveScreenState.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveScreenState.kt index b80ef3f..999c3f5 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveScreenState.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveScreenState.kt @@ -37,8 +37,6 @@ sealed class ReceiveScreenEffect { data object NavigateBack : ReceiveScreenEffect() - data object ShowSuccessAnimation : ReceiveScreenEffect() - data object RequestCameraPermission : ReceiveScreenEffect() } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt index ca8c726..74cff33 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/receive/ReceiveViewModel.kt @@ -290,7 +290,6 @@ class ReceiveViewModel( receivedFiles = savedFiles, ) } - postSideEffect(ReceiveScreenEffect.ShowSuccessAnimation) } else { reduce { ReceiveScreenState.Error( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt index 3268e6c..912d0f5 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/TransferCompletePhase.kt @@ -46,46 +46,6 @@ fun TransferCompletePhase( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(24.dp), ) { - SendCard( - backgroundColor = MaterialTheme.colorScheme.primaryContainer, - modifier = Modifier, - ) { - Column( - modifier = Modifier.padding(40.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - Icons.Default.CheckCircle, - contentDescription = "Success", - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.primary, - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Text( - text = "Transfer Complete!", - style = - MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.Bold, - ), - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "$fileCount file${ - if (fileCount != 1) "s" else "" - } sent successfully", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f), - ) - } - } - SendCard( backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, ) { From b246fc2bc7ae3ef7db4ed9a84f3862f2c57ae112 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 18 Nov 2025 04:28:41 +0600 Subject: [PATCH 25/33] Add About screen --- app/build.gradle.kts | 2 + .../app/domain/model/BuildConfigFields.kt | 6 +++ .../drop/app/presentation/DropApplication.kt | 14 ++++++- .../drop/app/presentation/MainActivity.kt | 6 +++ .../navigation/DropDestination.kt | 2 + .../app/presentation/profile/AboutScreen.kt | 35 ++++++++++++++++ .../presentation/profile/EditProfileScreen.kt | 41 +++++++++++++++++++ gradle/libs.versions.toml | 4 ++ 8 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/domain/model/BuildConfigFields.kt create mode 100644 app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/AboutScreen.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5b4e4f0..bc55e52 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -178,6 +178,8 @@ dependencies { implementation(libs.androidx.compose.foundation) implementation(libs.kotlinx.serialization) + implementation(libs.ark.about) + // Koin Dependency Injection implementation(libs.io.koin.core) implementation(libs.io.koin.android) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/BuildConfigFields.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/BuildConfigFields.kt new file mode 100644 index 0000000..1e139a1 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/BuildConfigFields.kt @@ -0,0 +1,6 @@ +package dev.arkbuilders.drop.app.domain.model + +class BuildConfigFields( + val versionCode: Int, + val versionName: String, +) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt index 5a8ad48..a00c3a7 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt @@ -1,10 +1,13 @@ package dev.arkbuilders.drop.app.presentation import android.app.Application +import dev.arkbuilders.drop.app.BuildConfig import dev.arkbuilders.drop.app.di.appModule import dev.arkbuilders.drop.app.di.viewModelsModule +import dev.arkbuilders.drop.app.domain.model.BuildConfigFields import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin +import org.koin.dsl.module import timber.log.Timber class DropApplication : Application() { @@ -12,9 +15,18 @@ class DropApplication : Application() { super.onCreate() Timber.Forest.plant(Timber.DebugTree()) + val buildConfigFieldsModule = + module { + single { + BuildConfigFields( + BuildConfig.VERSION_CODE, + BuildConfig.VERSION_NAME, + ) + } + } startKoin { androidContext(this@DropApplication) - modules(appModule, viewModelsModule) + modules(appModule, viewModelsModule, buildConfigFieldsModule) } } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt index 12057e0..5ccdf87 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/MainActivity.kt @@ -20,6 +20,7 @@ import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo import dev.arkbuilders.drop.app.presentation.history.History import dev.arkbuilders.drop.app.presentation.home.Home import dev.arkbuilders.drop.app.presentation.navigation.DropDestination +import dev.arkbuilders.drop.app.presentation.profile.AboutScreen import dev.arkbuilders.drop.app.presentation.profile.EditProfileEnhanced import dev.arkbuilders.drop.app.presentation.receive.Receive import dev.arkbuilders.drop.app.presentation.send.Send @@ -103,5 +104,10 @@ fun DropNavigation( navController = navController, ) } + composable(DropDestination.About.route) { + AboutScreen( + navController = navController, + ) + } } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt index d9712ea..a7b0a86 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/navigation/DropDestination.kt @@ -9,6 +9,8 @@ sealed class DropDestination(val route: String) { object EditProfile : DropDestination("edit_profile") + object About : DropDestination("about") + object Receive : DropDestination("receive?ticket={ticket}&confirmation={confirmation}") { const val DEEP_LINK_PATTERN = "drop://receive?ticket={ticket}&confirmation={confirmation}" diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/AboutScreen.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/AboutScreen.kt new file mode 100644 index 0000000..41ea41c --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/AboutScreen.kt @@ -0,0 +1,35 @@ +package dev.arkbuilders.drop.app.presentation.profile + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController +import dev.arkbuilders.components.about.presentation.ArkAbout +import dev.arkbuilders.drop.app.R +import dev.arkbuilders.drop.app.domain.model.BuildConfigFields +import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack +import org.koin.compose.koinInject + +@Composable +fun AboutScreen(navController: NavController) { + val fields = koinInject() + + Scaffold( + topBar = { + DropTopBarBack( + title = "About", + onBackClick = { navController.popBackStack() }, + ) + }, + ) { + ArkAbout( + modifier = Modifier.padding(it), + appName = stringResource(id = R.string.app_name), + appLogoResId = R.drawable.ic_logo, + versionName = fields.versionName, + privacyPolicyUrl = "", + ) + } +} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt index dea4674..6f03098 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/profile/EditProfileScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -89,6 +90,7 @@ import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack import dev.arkbuilders.drop.app.presentation.components.ErrorState import dev.arkbuilders.drop.app.presentation.components.ErrorStateDisplay import dev.arkbuilders.drop.app.presentation.components.ErrorType +import dev.arkbuilders.drop.app.presentation.navigation.DropDestination import dev.arkbuilders.drop.app.presentation.theme.DesignTokens import kotlinx.coroutines.delay import org.koin.compose.koinInject @@ -221,6 +223,8 @@ fun EditProfileEnhanced(navController: NavController) { }, ) + About(navController = navController) + PrivacyNoticeSection() Spacer(modifier = Modifier.Companion.height(DesignTokens.Spacing.xxxl)) @@ -673,3 +677,40 @@ private fun PrivacyNoticeSection() { } } } + +@Composable +fun About(navController: NavController) { + DropCard( + variant = DropCardVariant.Filled, + size = DropCardSize.Medium, + colors = + CardDefaults.cardColors( + containerColor = colorScheme.surfaceVariant.copy(alpha = 0.5f), + contentColor = colorScheme.onSurfaceVariant, + ), + onClick = { + navController.navigate(DropDestination.About.route) + }, + ) { + Row(Modifier.padding(DesignTokens.Spacing.lg)) { + Icon( + Icons.Default.Info, + contentDescription = null, + modifier = + Modifier + .size(20.dp) + .padding(top = 2.dp), + tint = colorScheme.primary, + ) + + Spacer(modifier = Modifier.Companion.width(DesignTokens.Spacing.md)) + + Text( + text = "About", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onSurface, + ) + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 51f06e2..1741aef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,8 @@ agp = "8.12.1" kotlin = "2.2.0" ksp = "2.2.0-2.0.2" coreKtx = "1.15.0" + +arkAbout = "0.2.0" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" @@ -50,6 +52,8 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } +ark-about = { module = "dev.arkbuilders.components:about", version.ref = "arkAbout" } + androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } From cfac7c8046b9ef1ec983cfb44667795f30562a34 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 18 Nov 2025 04:58:15 +0600 Subject: [PATCH 26/33] Fix ui in History screen --- .../drop/app/presentation/history/History.kt | 65 ++++++------------- 1 file changed, 20 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt index a1259ce..df756d2 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/history/History.kt @@ -2,6 +2,7 @@ package dev.arkbuilders.drop.app.presentation.history import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -68,8 +69,7 @@ fun History( Column( modifier = Modifier - .fillMaxSize() - .padding(16.dp), + .fillMaxSize(), ) { DropTopBarBack( title = "Transfer history", @@ -85,12 +85,9 @@ fun History( }, ) - Spacer(modifier = Modifier.height(16.dp)) - if (state.historyItems.isEmpty()) { - // Empty state Card( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors( @@ -125,9 +122,10 @@ fun History( } } } else { - // History list LazyColumn( + modifier = Modifier, verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(16.dp), ) { items(state.historyItems) { item -> HistoryItemCard( @@ -144,7 +142,6 @@ fun History( } } - // Clear all confirmation dialog if (state.showClearDialog) { AlertDialog( onDismissRequest = { viewModel.onDismissClearDialog() }, @@ -176,7 +173,7 @@ fun History( } }, dismissButton = { - TextButton(onClick = { viewModel.onClear() }) { + TextButton(onClick = { viewModel.onDismissClearDialog() }) { Text("Cancel", fontWeight = FontWeight.Medium) } }, @@ -267,45 +264,23 @@ private fun HistoryItemCard( Spacer(modifier = Modifier.height(8.dp)) - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = formatFileSize(item.files.sumOf { it.size }), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Text( - text = "•", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Text( - text = formatTimestamp(item.timestamp), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + val size = formatFileSize(item.files.sumOf { it.size }) + val timestamp = formatTimestamp(item.timestamp) + val filesCount = + if (item.files.size > 1) + " • ${item.files.size} files" + else + "" - if (item.files.size > 1) { - Text( - text = "•", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + val info = "$size • $timestamp$filesCount" - Text( - text = "${item.files.size} files", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Medium, - ) - } - } + Text( + text = info, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(8.dp)) // Status Text( From 1940f966a92c62cf7ac0115f0874316b91b10668 Mon Sep 17 00:00:00 2001 From: Shubert Munthali Date: Sun, 30 Nov 2025 18:04:54 +0200 Subject: [PATCH 27/33] Remove unused code and replace Logs with Timbers (#16) * Remove obsolete SDK check in NetworkStatusImpl and Use Timber for logging in SendFileSubscriberImpl * Remove Hilt plugin from version catalogs * Remove unused code * Replace Log with Timber * Downgrade min SDK from 29 to 26 --- app/build.gradle.kts | 2 +- .../app/data/ReceiveFilesSubscriberImpl.kt | 12 ++-- .../drop/app/data/SendFilesSubscriberImpl.kt | 11 ++-- .../drop/app/data/SenderFileDataImpl.kt | 10 +-- .../data/datasource/ProfileLocalDataSource.kt | 7 +- .../drop/app/data/helper/AvatarHelperImpl.kt | 10 +-- .../app/data/helper/ResourcesHelperImpl.kt | 8 +-- .../app/data/repository/NetworkStatusImpl.kt | 28 ++++---- .../app/data/repository/ProfileRepoImpl.kt | 2 - .../dev/arkbuilders/drop/app/di/AppModule.kt | 65 +------------------ .../app/domain/usecase/ReceiveFilesUseCase.kt | 4 -- .../drop/app/presentation/DropApplication.kt | 2 +- gradle/libs.versions.toml | 3 - 13 files changed, 44 insertions(+), 120 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bc55e52..15edf56 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ android { defaultConfig { applicationId = "dev.arkbuilders.drop.app" - minSdk = 29 + minSdk = 26 targetSdk = 36 versionCode = 1 versionName = "1.0" diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/ReceiveFilesSubscriberImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/ReceiveFilesSubscriberImpl.kt index a06c50a..575a9eb 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/ReceiveFilesSubscriberImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/ReceiveFilesSubscriberImpl.kt @@ -1,12 +1,12 @@ package dev.arkbuilders.drop.app.data -import android.util.Log import dev.arkbuilders.drop.ReceiveFilesConnectingEvent import dev.arkbuilders.drop.ReceiveFilesReceivingEvent import dev.arkbuilders.drop.ReceiveFilesSubscriber import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber import java.io.ByteArrayOutputStream import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -46,11 +46,11 @@ class ReceiveFilesSubscriberImpl : ReceiveFilesSubscriber { override fun getId(): String = id override fun log(message: String) { - Log.d(TAG, message) + Timber.tag(TAG).d(message) } override fun notifyReceiving(event: ReceiveFilesReceivingEvent) { - Log.d(TAG, "Receiving data for file: ${event.id}, data size: ${event.data.size}") + Timber.tag(TAG).d("Receiving data for file: ${event.id}, data size: ${event.data.size}") // Get or create ByteArrayOutputStream for this file val stream = receivedDataStreams.getOrPut(event.id) { ByteArrayOutputStream() } @@ -83,13 +83,13 @@ class ReceiveFilesSubscriberImpl : ReceiveFilesSubscriber { ) if (isComplete) { - Log.d(TAG, "File ${fileInfo.name} completed: $receivedBytes bytes") + Timber.tag(TAG).d("File ${fileInfo.name} completed: $receivedBytes bytes") } } } override fun notifyConnecting(event: ReceiveFilesConnectingEvent) { - Log.d(TAG, "Connected to sender: ${event.sender.name}, files: ${event.files.size}") + Timber.tag(TAG).d("Connected to sender: ${event.sender.name}, files: ${event.files.size}") val fileInfos = event.files.map { file -> @@ -163,7 +163,7 @@ class ReceiveFilesSubscriberImpl : ReceiveFilesSubscriber { /** * Check if all files are complete */ - public fun areAllFilesComplete(): Boolean { + fun areAllFilesComplete(): Boolean { val currentProgress = _progress.value return currentProgress.files.isNotEmpty() && currentProgress.files.all { file -> diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/SendFilesSubscriberImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/SendFilesSubscriberImpl.kt index 12ad1b9..bbc79b4 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/SendFilesSubscriberImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/SendFilesSubscriberImpl.kt @@ -1,12 +1,12 @@ package dev.arkbuilders.drop.app.data -import android.util.Log import dev.arkbuilders.drop.SendFilesConnectingEvent import dev.arkbuilders.drop.SendFilesSendingEvent import dev.arkbuilders.drop.SendFilesSubscriber import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber import java.util.UUID data class SendingProgress( @@ -31,14 +31,11 @@ class SendFilesSubscriberImpl : SendFilesSubscriber { override fun getId(): String = id override fun log(message: String) { - Log.d(TAG, message) + Timber.tag(TAG).d(message) } override fun notifySending(event: SendFilesSendingEvent) { - Log.d( - TAG, - "Sending progress: ${event.name} - sent: ${event.sent}, remaining: ${event.remaining}", - ) + log("Sending progress: ${event.name} - sent: ${event.sent}, remaining: ${event.remaining}") _progress.value = _progress.value.copy( @@ -49,7 +46,7 @@ class SendFilesSubscriberImpl : SendFilesSubscriber { } override fun notifyConnecting(event: SendFilesConnectingEvent) { - Log.d(TAG, "Connected to receiver: ${event.receiver.name}") + log("Connected to receiver: ${event.receiver.name}") _progress.value = _progress.value.copy( diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/SenderFileDataImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/SenderFileDataImpl.kt index 5dffa1b..c933b80 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/SenderFileDataImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/SenderFileDataImpl.kt @@ -2,8 +2,8 @@ package dev.arkbuilders.drop.app.data import android.content.Context import android.net.Uri -import android.util.Log import dev.arkbuilders.drop.SenderFileData +import timber.log.Timber import java.io.InputStream class SenderFileDataImpl( @@ -36,9 +36,9 @@ class SenderFileDataImpl( inputStream = context.contentResolver.openInputStream(uri) isInitialized = true - Log.d(TAG, "Initialized SenderFileData for URI: $uri, size: $totalLength") + Timber.tag(TAG).d("Initialized SenderFileData for URI: $uri, size: $totalLength") } catch (e: Exception) { - Log.e(TAG, "Failed to initialize SenderFileData", e) + Timber.tag(TAG).e(e, "Failed to initialize SenderFileData") } } @@ -58,7 +58,7 @@ class SenderFileDataImpl( byte?.toUByte() } } catch (e: Exception) { - Log.e(TAG, "Error reading byte", e) + Timber.tag(TAG).e(e, "Error reading byte") null } } @@ -80,7 +80,7 @@ class SenderFileDataImpl( inputStream?.read(bytes) ?: 0 bytes } catch (e: Exception) { - Log.e(TAG, "Error reading chunk of size $size", e) + Timber.tag(TAG).e(e, "Error reading chunk of size $size") ByteArray(0) } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt index db56660..f0962da 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt @@ -11,7 +11,7 @@ import dev.arkbuilders.drop.app.domain.model.UserProfile import kotlinx.serialization.json.Json class ProfileLocalDataSource( - private val context: Context, + context: Context, private val avatarHelper: AvatarHelper, ) { private val prefs: SharedPreferences = @@ -20,10 +20,7 @@ class ProfileLocalDataSource( private val json = Json { ignoreUnknownKeys = true } fun loadProfile(): UserProfile { - val profileJson = prefs.getString(KEY_PROFILE, null) - if (profileJson == null) { - return createDefaultProfile() - } + val profileJson = prefs.getString(KEY_PROFILE, null) ?: return createDefaultProfile() return runCatching { json diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt index 9030ec7..3d72922 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt @@ -24,7 +24,7 @@ class AvatarHelperImpl( val bitmap = loadBitmapFromUri(uri.toUri()) ?: return null val optimizedBitmap = optimizeBitmap(bitmap) bitmapToBase64(optimizedBitmap) - } catch (e: Exception) { + } catch (_: Exception) { null } } @@ -33,9 +33,9 @@ class AvatarHelperImpl( return try { val source = ImageDecoder.createSource(context.contentResolver, uri) ImageDecoder.decodeBitmap(source) - } catch (e: IOException) { + } catch (_: IOException) { null - } catch (e: SecurityException) { + } catch (_: SecurityException) { null } } @@ -73,7 +73,7 @@ class AvatarHelperImpl( } Base64.encodeToString(byteArray, Base64.DEFAULT) - } catch (e: Exception) { + } catch (_: Exception) { null } } @@ -93,7 +93,7 @@ class AvatarHelperImpl( } else { "" } - } catch (e: Exception) { + } catch (_: Exception) { "" } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt index a8d7f0a..f344e2d 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt @@ -28,7 +28,7 @@ class ResourcesHelperImpl( } } } catch (e: Exception) { - Timber.Forest.e(e, "Error getting filename for URI: $uri") + Timber.e(e, "Error getting filename for URI: $uri") null } } @@ -40,12 +40,12 @@ class ResourcesHelperImpl( uris.forEach { uri -> try { val size = getFileSize(uri) - if (size > 0 && size <= 2_000_000_000L) { // 2GB limit + if (size in 1..2_000_000_000L) { // 2GB limit validFiles.add(uri) } else { skippedCount++ } - } catch (e: Exception) { + } catch (_: Exception) { skippedCount++ } } @@ -63,7 +63,7 @@ class ResourcesHelperImpl( 0L } } ?: 0L - } catch (e: Exception) { + } catch (_: Exception) { 0L } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt index 8655878..b86e1b3 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt @@ -11,9 +11,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class NetworkStatusImpl( - private val context: Context, + context: Context, ) : NetworkStatus { - private val cm = + private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager private val _onlineStatus = MutableStateFlow(checkIsOnline()) @@ -25,12 +25,13 @@ class NetworkStatusImpl( .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + } } .build() - cm.registerNetworkCallback( + connectivityManager.registerNetworkCallback( networkRequest, object : ConnectivityManager.NetworkCallback() { override fun onLost(network: Network) { @@ -45,17 +46,16 @@ class NetworkStatusImpl( } private fun checkIsOnline(): Boolean { - val network: Network = cm.activeNetwork ?: return false + val network: Network = connectivityManager.activeNetwork ?: return false val networkCapabilities: NetworkCapabilities = - cm.getNetworkCapabilities(network) ?: return false - - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) - } else { - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + connectivityManager.getNetworkCapabilities(network) ?: return false + + var isOnline = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + isOnline = isOnline && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) } + return isOnline } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt index 27bc088..165c968 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt @@ -1,6 +1,5 @@ package dev.arkbuilders.drop.app.data.repository -import android.content.Context import dev.arkbuilders.drop.app.data.datasource.ProfileLocalDataSource import dev.arkbuilders.drop.app.domain.model.UserAvatar import dev.arkbuilders.drop.app.domain.model.UserProfile @@ -11,7 +10,6 @@ import kotlinx.coroutines.flow.asStateFlow class ProfileRepoImpl( private val localDataSource: ProfileLocalDataSource, - private val context: Context, ) : ProfileRepo { private val _profile = MutableStateFlow(localDataSource.loadProfile()) override val profile: StateFlow = _profile.asStateFlow() diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt index 14b38eb..e5b71bc 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt @@ -24,7 +24,7 @@ import org.koin.dsl.module val appModule = module { - single { ProfileRepoImpl(get(), get()) } + single { ProfileRepoImpl(get()) } single { ResourcesHelperImpl(get()) } single { Database.build(get()) } single { TransferSessionRepoImpl(get()) } @@ -40,66 +40,5 @@ val appModule = db.transferHistoryDao() } factory { SendFilesUseCase(get(), get(), get()) } - factory { ReceiveFilesUseCase(get(), get(), get()) } + factory { ReceiveFilesUseCase(get()) } } - -/*@Module -@InstallIn(SingletonComponent::class) -object AppModule { - - @Provides - @Singleton - fun provideProfileRepo(impl: ProfileRepoImpl): ProfileRepo = impl - - @Provides - @Singleton - fun provideTransferManager( - @ApplicationContext context: Context, - profileRepo: ProfileRepo, - transferHistoryItemRepository: TransferHistoryItemRepository - ): TransferManager { - return TransferManager(context, profileRepo, transferHistoryItemRepository) - } - - @Provides - @Singleton - fun provideResourcesHelper( - impl: ResourcesHelperImpl - ): ResourcesHelper = impl - - @Provides - @Singleton - fun provideDatabase( - @ApplicationContext context: Context, - ) = Database.build(context) - - @Provides - fun provideTransferHistoryItemDao( - db: Database, - ) = db.transferHistoryDao() - - @Provides - @Singleton - fun provideTransferHistoryItemRepository( - impl: TransferHistoryItemRepositoryImpl - ): TransferHistoryItemRepository = impl - - @Provides - @Singleton - fun providePermissionHelper( - impl: PermissionsHelperImpl - ): PermissionsHelper = impl - - @Provides - @Singleton - fun provideNetworkStatus( - impl: NetworkStatusImpl - ): NetworkStatus = impl - - @Provides - @Singleton - fun provideAvatarHelper( - impl: AvatarHelperImpl - ): AvatarHelper = impl -} -*/ diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt index 036675e..732d522 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt @@ -1,11 +1,9 @@ package dev.arkbuilders.drop.app.domain.usecase -import android.content.Context import dev.arkbuilders.drop.ReceiveFilesBubble import dev.arkbuilders.drop.ReceiveFilesRequest import dev.arkbuilders.drop.ReceiverConfig import dev.arkbuilders.drop.ReceiverProfile -import dev.arkbuilders.drop.app.domain.ResourcesHelper import dev.arkbuilders.drop.app.domain.repository.ProfileRepo import dev.arkbuilders.drop.receiveFiles import kotlinx.coroutines.Dispatchers @@ -13,9 +11,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber class ReceiveFilesUseCase( - private val context: Context, private val profileRepo: ProfileRepo, - private val resourcesHelper: ResourcesHelper, ) { suspend operator fun invoke( ticket: String, diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt index a00c3a7..bee919f 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt @@ -13,7 +13,7 @@ import timber.log.Timber class DropApplication : Application() { override fun onCreate() { super.onCreate() - Timber.Forest.plant(Timber.DebugTree()) + Timber.plant(Timber.DebugTree()) val buildConfigFieldsModule = module { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1741aef..0a4a836 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,8 +24,6 @@ accompanistPermissions = "0.36.0" timber = "5.0.1" room = "2.8.1" orbitMVI = "9.0.0" -hilt = "2.57.1" -hiltNavCompose = "1.3.0" kotlinSerialization = "1.9.0" tripletPlay = "3.10.1" drop = "17348879247" @@ -95,7 +93,6 @@ android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"} -dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref="hilt" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } triplet-play = { id = "com.github.triplet.play", version.ref = "tripletPlay" } ktlint-gradle = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlintGradlePlugin" } From 322e46064a70d0700cf6432fd2ede32cbdc5d712 Mon Sep 17 00:00:00 2001 From: Shubert Munthali Date: Tue, 2 Dec 2025 17:04:23 +0200 Subject: [PATCH 28/33] Make sure build job is not skipped (#17) * Remove check job from build job dependencies * Fix lint --- .github/workflows/build.yml | 2 +- .../drop/app/data/repository/NetworkStatusImpl.kt | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6c0d64..10d3d33 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: run: ./gradlew assembleDebug build: - needs: [check, lint, ktlint] + needs: [lint, ktlint] environment: Development runs-on: ubuntu-latest steps: diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt index b86e1b3..e0321f9 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/NetworkStatusImpl.kt @@ -50,11 +50,13 @@ class NetworkStatusImpl( val networkCapabilities: NetworkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false - var isOnline = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + var isOnline = + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - isOnline = isOnline && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + isOnline = isOnline && + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) } return isOnline } From 430de54ddee04c548e9333081f7d74f8ddad7045 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 2 Dec 2025 20:33:31 +0600 Subject: [PATCH 29/33] typo fix --- .../arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt | 1 - .../drop/app/presentation/send/SendViewModel.kt | 9 ++------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt index 3d72922..b529325 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt @@ -78,7 +78,6 @@ class AvatarHelperImpl( } } - @SuppressLint("DiscouragedApi") override fun getDefaultAvatarBase64(avatarId: String): String { return try { val resourceId = diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt index fc44e32..b34813c 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/SendViewModel.kt @@ -8,7 +8,6 @@ import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.zxing.BarcodeFormat -import com.google.zxing.WriterException import com.google.zxing.common.BitMatrix import com.google.zxing.qrcode.QRCodeWriter import dev.arkbuilders.drop.app.data.repository.SendSessionRepo @@ -313,12 +312,8 @@ class SendViewModel( } } return bitmap - } catch (e: WriterException) { - // TODO - // throw RuntimeException("QR code generation failed: ${e.message}", e) - return null - } catch (e: Exception) { - // throw RuntimeException("Unexpected error during QR code generation: ${e.message}", e) + } catch (e: Throwable) { + Timber.e("Unexpected error during QR code generation: ${e.message}") return null } } From 31bd21761b34925edf9f68e56611718bc2050016 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 2 Dec 2025 20:34:10 +0600 Subject: [PATCH 30/33] Fix bitmap loading from uri on api 26-27 --- .../drop/app/data/helper/AvatarHelperImpl.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt index b529325..8b6b3c5 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/AvatarHelperImpl.kt @@ -1,11 +1,11 @@ package dev.arkbuilders.drop.app.data.helper -import android.annotation.SuppressLint import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.ImageDecoder import android.net.Uri +import android.os.Build import android.util.Base64 import androidx.core.graphics.scale import androidx.core.net.toUri @@ -14,7 +14,6 @@ import dev.arkbuilders.drop.app.domain.AvatarHelper.Companion.JPEG_QUALITY import dev.arkbuilders.drop.app.domain.AvatarHelper.Companion.MAX_FILE_SIZE import dev.arkbuilders.drop.app.domain.AvatarHelper.Companion.MAX_IMAGE_SIZE import java.io.ByteArrayOutputStream -import java.io.IOException class AvatarHelperImpl( private val context: Context, @@ -31,11 +30,15 @@ class AvatarHelperImpl( private fun loadBitmapFromUri(uri: Uri): Bitmap? { return try { - val source = ImageDecoder.createSource(context.contentResolver, uri) - ImageDecoder.decodeBitmap(source) - } catch (_: IOException) { - null - } catch (_: SecurityException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val source = ImageDecoder.createSource(context.contentResolver, uri) + ImageDecoder.decodeBitmap(source) + } else { + context.contentResolver.openInputStream(uri)?.use { input -> + BitmapFactory.decodeStream(input) + } + } + } catch (_: Throwable) { null } } From 9af5b360b375e2fe1d3a8a956bb643cf53b9a753 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Wed, 3 Dec 2025 06:20:14 +0600 Subject: [PATCH 31/33] Save files to downloads legacy (api 26-28) --- .../app/data/helper/ResourcesHelperImpl.kt | 115 +++++++++++++++--- 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt index f344e2d..3475e1a 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/ResourcesHelperImpl.kt @@ -2,13 +2,17 @@ package dev.arkbuilders.drop.app.data.helper import android.content.ContentValues import android.content.Context +import android.os.Build import android.os.Environment import android.provider.MediaStore import android.provider.OpenableColumns import android.webkit.MimeTypeMap +import androidx.annotation.RequiresApi import androidx.core.net.toUri import dev.arkbuilders.drop.app.domain.ResourcesHelper import timber.log.Timber +import java.io.File +import java.io.FileOutputStream import java.net.URLConnection class ResourcesHelperImpl( @@ -72,46 +76,73 @@ class ResourcesHelperImpl( fileName: String, data: ByteArray, ): String? { - val resolver = context.contentResolver - val uniqueName = getUniqueFileName(fileName) - val mime = getMimeType(uniqueName) + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + saveModern(uniqueName, data) + } else { + saveLegacy(uniqueName, data) + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun saveModern( + fileName: String, + data: ByteArray, + ): String? { + val resolver = context.contentResolver + val mime = getMimeType(fileName) + val relativePath = Environment.DIRECTORY_DOWNLOADS + "/" val values = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, uniqueName) + put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) put(MediaStore.MediaColumns.MIME_TYPE, mime) - put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) + put(MediaStore.MediaColumns.IS_PENDING, 1) } val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) ?: return null - resolver.openOutputStream(uri)?.use { it.write(data) } + try { + resolver.openOutputStream(uri)?.use { it.write(data) } + } catch (e: Exception) { + resolver.delete(uri, null, null) + return null + } + + values.clear() + values.put(MediaStore.MediaColumns.IS_PENDING, 0) + resolver.update(uri, values, null, null) - return uniqueName + return fileName } - private fun getUniqueFileName(originalName: String): String { - val resolver = context.contentResolver + private fun saveLegacy( + fileName: String, + data: ByteArray, + ): String { + val downloads = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + if (!downloads.exists()) downloads.mkdirs() + + val file = File(downloads, fileName) + + FileOutputStream(file).use { it.write(data) } + return fileName + } + + private fun getUniqueFileName(originalName: String): String { val baseName = originalName.substringBeforeLast(".") val ext = originalName.substringAfterLast(".", "") var candidateName = originalName var attempt = 1 while (true) { - val values = - ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, candidateName) - put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) - } - - val testUri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) - - if (testUri != null) { - resolver.delete(testUri, null, null) + if (doesFileExistInDownloads(candidateName).not()) { return candidateName } @@ -135,6 +166,52 @@ class ResourcesHelperImpl( } } + private fun doesFileExistInDownloads(name: String): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + doesFileExistModern(name) + } else { + doesFileExistLegacy(name) + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun doesFileExistModern(name: String): Boolean { + val resolver = context.contentResolver + + val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME) + + val selection = + "${MediaStore.MediaColumns.DISPLAY_NAME} = ? AND " + + "${MediaStore.MediaColumns.RELATIVE_PATH} = ?" + + val selectionArgs = + arrayOf( + name, + Environment.DIRECTORY_DOWNLOADS + "/", + ) + + resolver.query( + MediaStore.Downloads.EXTERNAL_CONTENT_URI, + projection, + selection, + selectionArgs, + null, + )?.use { cursor -> + return cursor.moveToFirst() + } + + return false + } + + private fun doesFileExistLegacy(name: String): Boolean { + val downloads = + Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + + val file = File(downloads, name) + return file.exists() + } + private fun getMimeType(fileName: String): String { val ext = fileName.substringAfterLast(".", "").lowercase() From cb496ce619dd4fb810ea2b2c800063fb7f606a01 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Wed, 3 Dec 2025 06:22:53 +0600 Subject: [PATCH 32/33] Implement write permission flow for file receive --- .../app/data/helper/PermissionsHelperImpl.kt | 12 +++++++ .../drop/app/di/ViewModelsModule.kt | 2 +- .../drop/app/domain/PermissionsHelper.kt | 2 ++ .../drop/app/presentation/home/Home.kt | 33 ++++++++++++++++++- .../app/presentation/home/HomeViewModel.kt | 17 +++++++++- 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt index 3b8d37a..e3869b7 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/data/helper/PermissionsHelperImpl.kt @@ -3,6 +3,7 @@ package dev.arkbuilders.drop.app.data.helper import android.Manifest import android.content.Context import android.content.pm.PackageManager +import android.os.Build import androidx.core.content.ContextCompat import dev.arkbuilders.drop.app.domain.PermissionsHelper @@ -15,4 +16,15 @@ class PermissionsHelperImpl( Manifest.permission.CAMERA, ) == PackageManager.PERMISSION_GRANTED } + + override fun isWritePermissionGranted(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + true + } else { + ContextCompat.checkSelfPermission( + ctx, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + ) == PackageManager.PERMISSION_GRANTED + } + } } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt index 1d1efc2..ab8f0e8 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt @@ -11,7 +11,7 @@ import org.koin.dsl.module val viewModelsModule = module { viewModel { HistoryViewModel(get()) } - viewModel { HomeViewModel(get(), get()) } + viewModel { HomeViewModel(get(), get(), get()) } viewModel { EditProfileViewModel(get(), get()) } viewModel { ReceiveViewModel(get(), get()) } viewModel { SendViewModel(get(), get(), get()) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt index ec0da00..79ff861 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/domain/PermissionsHelper.kt @@ -2,4 +2,6 @@ package dev.arkbuilders.drop.app.domain interface PermissionsHelper { fun isCameraGranted(): Boolean + + fun isWritePermissionGranted(): Boolean } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt index 84f85f3..e3da778 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/Home.kt @@ -1,5 +1,9 @@ package dev.arkbuilders.drop.app.presentation.home +import android.Manifest +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring @@ -29,6 +33,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -64,6 +69,7 @@ import dev.arkbuilders.drop.app.presentation.theme.DesignTokens import kotlinx.coroutines.delay import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect import java.time.Duration import java.time.OffsetDateTime import java.time.format.DateTimeFormatter @@ -77,6 +83,7 @@ fun Home( ) { val viewModel: HomeViewModel = koinInject() val state by viewModel.collectAsState() + val context = LocalContext.current var logoScale by remember { mutableStateOf(0f) } @@ -96,6 +103,30 @@ fun Home( label = "logoScale", ) + val requestWritePermissionLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { isGranted -> + if (isGranted) { + navController.navigate(DropDestination.Receive.route) + } else { + Toast + .makeText(context, "Write permission not granted", Toast.LENGTH_SHORT) + .show() + } + } + + viewModel.collectSideEffect { effect -> + when (effect) { + HomeScreenEffect.AskWritePermission -> { + requestWritePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + HomeScreenEffect.NavigateToReceiveScreen -> { + navController.navigate(DropDestination.Receive.route) + } + } + } + LazyColumn( modifier = Modifier @@ -116,7 +147,7 @@ fun Home( item { QuickActionsSection( onSendClick = { navController.navigate(DropDestination.Send.route) }, - onReceiveClick = { navController.navigate(DropDestination.Receive.route) }, + onReceiveClick = { viewModel.onReceiveClick() }, ) } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt index e88adaa..95ba5fe 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/home/HomeViewModel.kt @@ -2,6 +2,7 @@ package dev.arkbuilders.drop.app.presentation.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dev.arkbuilders.drop.app.domain.PermissionsHelper import dev.arkbuilders.drop.app.domain.model.TransferSession import dev.arkbuilders.drop.app.domain.model.UserProfile import dev.arkbuilders.drop.app.domain.repository.ProfileRepo @@ -18,11 +19,16 @@ data class HomeScreenState( val profile: UserProfile, ) -sealed class HomeScreenEffect +sealed class HomeScreenEffect { + object AskWritePermission : HomeScreenEffect() + + object NavigateToReceiveScreen : HomeScreenEffect() +} class HomeViewModel( private val historyItemRepository: TransferSessionRepo, private val profileRepo: ProfileRepo, + private val permissionsHelper: PermissionsHelper, ) : ViewModel(), ContainerHost { override val container: Container = container(HomeScreenState(emptyList(), UserProfile.empty())) @@ -47,4 +53,13 @@ class HomeViewModel( } } } + + fun onReceiveClick() = + intent { + if (permissionsHelper.isWritePermissionGranted()) { + postSideEffect(HomeScreenEffect.NavigateToReceiveScreen) + } else { + postSideEffect(HomeScreenEffect.AskWritePermission) + } + } } From 407ef28e82ea0bf473f6cb3916fbb7a20a97da78 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Sun, 14 Dec 2025 08:39:42 +0600 Subject: [PATCH 33/33] Replace SendQRDialog with WaitingForReceiver screen --- .../drop/app/presentation/send/Send.kt | 212 +----------------- .../phase/WaitingForReceiverPhase.kt | 154 +++++++++---- 2 files changed, 108 insertions(+), 258 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt index f98f8dd..76575ce 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/Send.kt @@ -1,64 +1,23 @@ package dev.arkbuilders.drop.app.presentation.send -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.graphics.Bitmap import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import compose.icons.TablerIcons -import compose.icons.tablericons.Copy -import compose.icons.tablericons.Qrcode import dev.arkbuilders.drop.app.presentation.components.DropErrorCard import dev.arkbuilders.drop.app.presentation.components.DropTopBarBack -import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize -import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant -import dev.arkbuilders.drop.app.presentation.send.components.SendButton -import dev.arkbuilders.drop.app.presentation.send.components.SendLoadingIndicator import dev.arkbuilders.drop.app.presentation.send.components.phase.FileSelectionPhase import dev.arkbuilders.drop.app.presentation.send.components.phase.GeneratingQRPhase import dev.arkbuilders.drop.app.presentation.send.components.phase.TransferCompletePhase import dev.arkbuilders.drop.app.presentation.send.components.phase.TransferringPhase import dev.arkbuilders.drop.app.presentation.send.components.phase.WaitingForReceiverPhase -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import org.koin.compose.koinInject import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -66,8 +25,6 @@ import org.orbitmvi.orbit.compose.collectSideEffect @OptIn(ExperimentalMaterial3Api::class) @Composable fun Send(navController: NavController) { - val haptic = LocalHapticFeedback.current - val listState = rememberLazyListState() val viewModel: SendViewModel = koinInject() val state by viewModel.collectAsState() @@ -127,6 +84,8 @@ fun Send(navController: NavController) { is SendScreenState.WaitingForReceiver -> { WaitingForReceiverPhase( + qrBitmap = sendScreenState.qrBitmap, + copyString = sendScreenState.copyString, fileCount = sendScreenState.files.size, onCancel = { viewModel.onCancelTransfer() }, ) @@ -163,176 +122,9 @@ fun Send(navController: NavController) { ) } } - - if (sendScreenState is SendScreenState.WaitingForReceiver) { - SendQRDialog( - qrBitmap = sendScreenState.qrBitmap, - fileCount = sendScreenState.files.size, - copyString = sendScreenState.copyString, - onDismiss = { }, - onCancel = {}, - ) - } } } -private fun copyToClipboard( - context: Context, - text: String, - label: String = "Transfer Info", -) { - val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clipData = ClipData.newPlainText(label, text) - clipboardManager.setPrimaryClip(clipData) -} - -@Composable -private fun SendQRDialog( - qrBitmap: Bitmap, - fileCount: Int, - copyString: String?, - onDismiss: () -> Unit, - onCancel: () -> Unit, -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - var showCopySuccess by remember { mutableStateOf(false) } - - AlertDialog( - onDismissRequest = onDismiss, - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - TablerIcons.Qrcode, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - Text( - "QR Code for Transfer", - style = - MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.Bold, - ), - ) - } - }, - text = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Surface( - shape = RoundedCornerShape(16.dp), - color = Color.White, - shadowElevation = 4.dp, - ) { - Image( - bitmap = qrBitmap.asImageBitmap(), - contentDescription = "QR code for file transfer", - modifier = - Modifier - .size(220.dp) - .padding(16.dp), - ) - } - - Spacer(modifier = Modifier.height(20.dp)) - - Text( - text = "Show this QR code to the receiver", - style = - MaterialTheme.typography.bodyLarge.copy( - fontWeight = FontWeight.Medium, - ), - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "$fileCount file${if (fileCount != 1) "s" else ""} ready to transfer", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f), - textAlign = TextAlign.Center, - ) - - // Copy functionality - if (!copyString.isNullOrEmpty()) { - Spacer(modifier = Modifier.height(16.dp)) - - SendButton( - onClick = { - copyToClipboard(context, copyString, "Transfer Code") - showCopySuccess = true - scope.launch { - delay(2000) - showCopySuccess = false - } - }, - variant = ButtonVariant.Secondary, - size = ButtonSize.Medium, - ) { - Icon( - TablerIcons.Copy, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - if (showCopySuccess) "Copied!" else "Copy Code", - fontWeight = FontWeight.Medium, - ) - } - - if (showCopySuccess) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Transfer code copied to clipboard", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.primary, - textAlign = TextAlign.Center, - ) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - SendLoadingIndicator() - Text( - text = "Waiting for receiver to scan...", - style = - MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium, - ), - color = MaterialTheme.colorScheme.primary, - ) - } - } - }, - confirmButton = {}, - dismissButton = { - TextButton( - onClick = onCancel, - shape = RoundedCornerShape(8.dp), - ) { - Text( - "Cancel Transfer", - fontWeight = FontWeight.Medium, - ) - } - }, - shape = RoundedCornerShape(20.dp), - ) -} - private fun SendException.toMessage() = when (this) { SendException.TransferInitializationFailed -> "Transfer initialization failed" diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt index 0a9e2e3..4e440ce 100644 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt +++ b/app/src/main/java/dev/arkbuilders/drop/app/presentation/send/components/phase/WaitingForReceiverPhase.kt @@ -1,5 +1,11 @@ package dev.arkbuilders.drop.app.presentation.send.components.phase +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.graphics.Bitmap +import android.widget.Toast +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -7,88 +13,140 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import compose.icons.TablerIcons +import compose.icons.tablericons.Copy import dev.arkbuilders.drop.app.presentation.send.components.ButtonSize import dev.arkbuilders.drop.app.presentation.send.components.ButtonVariant import dev.arkbuilders.drop.app.presentation.send.components.SendButton -import dev.arkbuilders.drop.app.presentation.send.components.SendCard import dev.arkbuilders.drop.app.presentation.send.components.SendLoadingIndicator @Composable fun WaitingForReceiverPhase( + qrBitmap: Bitmap, fileCount: Int, + copyString: String, onCancel: () -> Unit, ) { + val context = LocalContext.current + Column( - modifier = Modifier.fillMaxWidth().padding(20.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(24.dp), ) { - SendCard { - Column( - modifier = Modifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = "Ready to Send", - style = - MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.Bold, - ), - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - ) + Surface( + shape = RoundedCornerShape(16.dp), + color = Color.White, + shadowElevation = 4.dp, + ) { + Image( + bitmap = qrBitmap.asImageBitmap(), + contentDescription = "QR code for file transfer", + modifier = + Modifier + .size(220.dp) + .padding(16.dp), + ) + } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(20.dp)) - Text( - text = - "$fileCount file${if (fileCount != 1) "s" else ""}" + - " ready for transfer", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) + Text( + text = "Show this QR code to the receiver", + style = + MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - SendLoadingIndicator() - Text( - text = "Waiting for receiver to scan...", - style = - MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium, - ), - color = MaterialTheme.colorScheme.primary, - ) - } - } - } + Text( + text = "$fileCount file${if (fileCount != 1) "s" else ""} ready to transfer", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(16.dp)) SendButton( - onClick = onCancel, + onClick = { + copyToClipboard(context, copyString) + Toast.makeText(context, "Code copied!", Toast.LENGTH_SHORT).show() + }, variant = ButtonVariant.Secondary, - size = ButtonSize.Large, - modifier = Modifier.fillMaxWidth(), + size = ButtonSize.Medium, ) { + Icon( + TablerIcons.Copy, + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) Text( - "Cancel Transfer", + "Copy code", + fontWeight = FontWeight.Medium, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + SendLoadingIndicator() + Text( + text = "Waiting for receiver to scan...", style = - MaterialTheme.typography.titleMedium.copy( + MaterialTheme.typography.bodyMedium.copy( fontWeight = FontWeight.Medium, ), + color = MaterialTheme.colorScheme.primary, + ) + } + + TextButton( + onClick = onCancel, + shape = RoundedCornerShape(8.dp), + ) { + Text( + "Cancel Transfer", + fontWeight = FontWeight.Medium, ) } } } + +private fun copyToClipboard( + context: Context, + text: String, +) { + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText(null, text) + clipboardManager.setPrimaryClip(clipData) +}