From 1ba06d4dba26bddccbbe15dd0fdcfcc2d3e06cfe Mon Sep 17 00:00:00 2001 From: krzys Date: Tue, 7 Nov 2023 15:58:52 +0100 Subject: [PATCH] Added some animation. Corrected ui.Simple Firebase with Room Sync works. --- .idea/misc.xml | 1 - app/src/main/AndroidManifest.xml | 3 +- .../com/eltescode/warhbook/MainActivity.kt | 9 +- buildSrc/src/main/java/DataStore.kt | 4 + core_data/build.gradle.kts | 2 + core_data/src/main/AndroidManifest.xml | 4 +- .../core_data/utils/exception/CallOrThrow.kt | 8 + .../core_data/utils/exception/ErrorWrapper.kt | 5 + .../utils/exception/ErrorWrapperImpl.kt | 30 ++++ .../utils/exception/ServerException.kt | 12 ++ .../utils/network/NetworkStateProvider.kt | 5 + .../utils/network/NetworkStateProviderImpl.kt | 20 +++ .../eltescode/core_ui/components/Brushes.kt | 3 +- .../java/com/eltescode/core_ui/ui/Colors.kt | 20 +++ notes/notes_data/build.gradle.kts | 2 + .../notes_data/di/NotesDataModule.kt | 84 ++++++++- .../eltescode/notes_data/local/NoteChached.kt | 6 +- .../com/eltescode/notes_data/local/NoteDao.kt | 15 +- .../notes_data/mappers/NotesMapper.kt | 5 +- .../repository/NoteRepositoryImpl.kt | 170 +++++++++++++++++- .../notes_data/repository/SyncHelper.kt | 9 + .../notes_data/repository/SyncHelperImpl.kt | 38 ++++ .../notes_data/utils/CustomPreferences.kt | 8 + .../notes_data/utils/CustomPreferencesImpl.kt | 27 +++ .../notes_domain/di/NotesDomainModule.kt | 10 +- .../com/eltescode/notes_domain/model/Note.kt | 12 +- .../notes_domain/repository/NoteRepository.kt | 19 +- .../notes_domain/use_cases/AddNoteUseCase.kt | 5 +- .../use_cases/CheckSyncNeedUseCase.kt | 10 ++ .../use_cases/DeleteNoteUseCase.kt | 5 +- .../notes_domain/use_cases/GetNoteUseCase.kt | 3 +- .../notes_domain/use_cases/GetNotesUseCase.kt | 28 +-- .../notes_domain/use_cases/NoteUseCases.kt | 5 +- .../use_cases/SortNotesUseCase.kt | 33 ++++ .../notes_domain/use_cases/SyncDataUseCase.kt | 10 ++ .../add_edit_note/AddEditNoteScreen.kt | 1 - .../add_edit_note/AddEditNoteViewModel.kt | 32 +++- .../add_edit_note/UiEvent.kt | 2 +- .../notes_presentation/mappers/NotesMapper.kt | 5 +- .../model/NoteDisplayable.kt | 5 +- .../notes_presentation/notes/NotesScreen.kt | 31 ++-- .../notes/NotesViewModel.kt | 74 ++++++-- .../notes_presentation/util/NotesEvent.kt | 3 +- .../user_presentation/components/BaseCard.kt | 52 ------ .../components/CustomBackground.kt | 138 ++++++++++++++ .../components/CustomCard.kt | 55 ++++++ .../components/CustomCircularProgressBar.kt | 103 +++++++++++ .../components/CustomVerticalGrid.kt | 48 +++++ .../components/UserPicture.kt | 72 ++++---- .../user_screen/UserDataScreen.kt | 40 ++--- .../user_screen/UserDataViewModel.kt | 15 +- .../utils/PhotoCompressionWorker.kt | 2 - .../utils/UserDataScreenEvent.kt | 3 + .../utils/UserScreenState.kt | 1 + 54 files changed, 1092 insertions(+), 220 deletions(-) create mode 100644 buildSrc/src/main/java/DataStore.kt create mode 100644 core_data/src/main/java/com/eltescode/core_data/utils/exception/CallOrThrow.kt create mode 100644 core_data/src/main/java/com/eltescode/core_data/utils/exception/ErrorWrapper.kt create mode 100644 core_data/src/main/java/com/eltescode/core_data/utils/exception/ErrorWrapperImpl.kt create mode 100644 core_data/src/main/java/com/eltescode/core_data/utils/exception/ServerException.kt create mode 100644 core_data/src/main/java/com/eltescode/core_data/utils/network/NetworkStateProvider.kt create mode 100644 core_data/src/main/java/com/eltescode/core_data/utils/network/NetworkStateProviderImpl.kt create mode 100644 notes/notes_data/src/main/java/com/eltescode/notes_data/repository/SyncHelper.kt create mode 100644 notes/notes_data/src/main/java/com/eltescode/notes_data/repository/SyncHelperImpl.kt create mode 100644 notes/notes_data/src/main/java/com/eltescode/notes_data/utils/CustomPreferences.kt create mode 100644 notes/notes_data/src/main/java/com/eltescode/notes_data/utils/CustomPreferencesImpl.kt create mode 100644 notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/CheckSyncNeedUseCase.kt create mode 100644 notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/SortNotesUseCase.kt create mode 100644 notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/SyncDataUseCase.kt delete mode 100644 user_presentation/src/main/java/com/eltescode/user_presentation/components/BaseCard.kt create mode 100644 user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomBackground.kt create mode 100644 user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomCard.kt create mode 100644 user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomCircularProgressBar.kt create mode 100644 user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomVerticalGrid.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index e9e9db5..54b4442 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 62ef7d0..7925518 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ - - + + + \ No newline at end of file diff --git a/core_data/src/main/java/com/eltescode/core_data/utils/exception/CallOrThrow.kt b/core_data/src/main/java/com/eltescode/core_data/utils/exception/CallOrThrow.kt new file mode 100644 index 0000000..abd9b9d --- /dev/null +++ b/core_data/src/main/java/com/eltescode/core_data/utils/exception/CallOrThrow.kt @@ -0,0 +1,8 @@ +package com.eltescode.core_data.utils.exception + +suspend fun callOrThrow( + errorWrapper: ErrorWrapper, + apiCall: suspend () -> T +): T { + return runCatching { apiCall() }.getOrElse { throw errorWrapper.wrap(it) } +} \ No newline at end of file diff --git a/core_data/src/main/java/com/eltescode/core_data/utils/exception/ErrorWrapper.kt b/core_data/src/main/java/com/eltescode/core_data/utils/exception/ErrorWrapper.kt new file mode 100644 index 0000000..1dd2842 --- /dev/null +++ b/core_data/src/main/java/com/eltescode/core_data/utils/exception/ErrorWrapper.kt @@ -0,0 +1,5 @@ +package com.eltescode.core_data.utils.exception + +interface ErrorWrapper { + fun wrap(throwable: Throwable): Throwable +} \ No newline at end of file diff --git a/core_data/src/main/java/com/eltescode/core_data/utils/exception/ErrorWrapperImpl.kt b/core_data/src/main/java/com/eltescode/core_data/utils/exception/ErrorWrapperImpl.kt new file mode 100644 index 0000000..069d1b8 --- /dev/null +++ b/core_data/src/main/java/com/eltescode/core_data/utils/exception/ErrorWrapperImpl.kt @@ -0,0 +1,30 @@ +package com.eltescode.core_data.utils.exception + +import retrofit2.HttpException + + +class ErrorWrapperImpl : ErrorWrapper { + override fun wrap(throwable: Throwable): Throwable { + return when (throwable) { + is HttpException -> { + wrapServerException(throwable) + } + + else -> throwable + } + } + + private fun wrapServerException(httpException: HttpException): Throwable { + return with(httpException) { + when (httpException.code()) { + 500 -> ServerException.Internal(message) + 400 -> ServerException.BadRequest(message) + 401 -> ServerException.Unauthorized(message) + 402 -> ServerException.PaymentRequired(message) + 403 -> ServerException.Forbidden(message) + 404 -> ServerException.NotFound(message) + else -> ServerException.Unknown(message) + } + } + } +} diff --git a/core_data/src/main/java/com/eltescode/core_data/utils/exception/ServerException.kt b/core_data/src/main/java/com/eltescode/core_data/utils/exception/ServerException.kt new file mode 100644 index 0000000..d39fe88 --- /dev/null +++ b/core_data/src/main/java/com/eltescode/core_data/utils/exception/ServerException.kt @@ -0,0 +1,12 @@ +package com.eltescode.core_data.utils.exception + +sealed class ServerException(message: String?) : Throwable(message) { + class Internal(message: String?) : ServerException(message) + class NotFound(message: String?) : ServerException(message) + class BadRequest(message: String?) : ServerException(message) + class Unauthorized(message: String?) : ServerException(message) + class PaymentRequired(message: String?) : ServerException(message) + class Forbidden(message: String?) : ServerException(message) + class Unknown(message: String?) : ServerException(message) + +} \ No newline at end of file diff --git a/core_data/src/main/java/com/eltescode/core_data/utils/network/NetworkStateProvider.kt b/core_data/src/main/java/com/eltescode/core_data/utils/network/NetworkStateProvider.kt new file mode 100644 index 0000000..84cd7d8 --- /dev/null +++ b/core_data/src/main/java/com/eltescode/core_data/utils/network/NetworkStateProvider.kt @@ -0,0 +1,5 @@ +package com.eltescode.core_data.utils.network + +interface NetworkStateProvider { + fun isNetworkAvailable(): Boolean +} \ No newline at end of file diff --git a/core_data/src/main/java/com/eltescode/core_data/utils/network/NetworkStateProviderImpl.kt b/core_data/src/main/java/com/eltescode/core_data/utils/network/NetworkStateProviderImpl.kt new file mode 100644 index 0000000..fac11f4 --- /dev/null +++ b/core_data/src/main/java/com/eltescode/core_data/utils/network/NetworkStateProviderImpl.kt @@ -0,0 +1,20 @@ +package com.eltescode.core_data.utils.network + +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import androidx.annotation.RequiresApi + +class NetworkStateProviderImpl(private val connectivityManager: ConnectivityManager) : + NetworkStateProvider { + @RequiresApi(Build.VERSION_CODES.M) + override fun isNetworkAvailable(): Boolean { + val capabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + ?: return false + + return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) + || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + } +} \ No newline at end of file diff --git a/core_ui/src/main/java/com/eltescode/core_ui/components/Brushes.kt b/core_ui/src/main/java/com/eltescode/core_ui/components/Brushes.kt index 1d08c50..13e4d4f 100644 --- a/core_ui/src/main/java/com/eltescode/core_ui/components/Brushes.kt +++ b/core_ui/src/main/java/com/eltescode/core_ui/components/Brushes.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.res.imageResource import com.eltescode.core_ui.R +import com.eltescode.core_ui.ui.SilverColors.Companion.color979c9f @Composable fun backgroundShaderBrush(): ShaderBrush { @@ -25,7 +26,7 @@ fun backgroundShaderBrush(): ShaderBrush { @Composable fun silverBackgroundBrush(): Brush { return Brush.linearGradient( - colors = listOf(Color.White, Color(0XFF979c9f)), + colors = listOf(Color.White, color979c9f), start = Offset.Zero, end = Offset.Infinite ) diff --git a/core_ui/src/main/java/com/eltescode/core_ui/ui/Colors.kt b/core_ui/src/main/java/com/eltescode/core_ui/ui/Colors.kt index 2d1d70f..114431a 100644 --- a/core_ui/src/main/java/com/eltescode/core_ui/ui/Colors.kt +++ b/core_ui/src/main/java/com/eltescode/core_ui/ui/Colors.kt @@ -15,5 +15,25 @@ class SilverColors { val color84898b = Color(0XFF84898b) val color979c9f = Color(0XFF979c9f) val colorAab0b3 = Color(0XFFaab0b3) + val colorffffff = Color(0XFFffffff) } } + +class GreenColors { + companion object { + + val colorCfea75 = Color(0XFFcfea75) + val color008c15 = Color(0XFF008c15) + + } +} + +class BlueColors { + companion object { + + val color91b1fd = Color(0XFF91b1fd) + val color4e6db1 = Color(0XFF4e6db1) + + + } +} \ No newline at end of file diff --git a/notes/notes_data/build.gradle.kts b/notes/notes_data/build.gradle.kts index 6b1d265..88ad145 100644 --- a/notes/notes_data/build.gradle.kts +++ b/notes/notes_data/build.gradle.kts @@ -23,4 +23,6 @@ dependencies { implementation(Firebase.firebaseAuth) implementation(Firebase.firebaseFirestore) implementation(Firebase.firebaseStorage) + + implementation(DataStore.dataStore) } \ No newline at end of file diff --git a/notes/notes_data/src/main/java/com/eltescode/notes_data/di/NotesDataModule.kt b/notes/notes_data/src/main/java/com/eltescode/notes_data/di/NotesDataModule.kt index 5c3501f..221a250 100644 --- a/notes/notes_data/src/main/java/com/eltescode/notes_data/di/NotesDataModule.kt +++ b/notes/notes_data/src/main/java/com/eltescode/notes_data/di/NotesDataModule.kt @@ -1,14 +1,33 @@ package com.eltescode.notes_data.di import android.app.Application +import android.content.Context +import android.net.ConnectivityManager +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile import androidx.room.Room +import com.eltescode.core_data.utils.exception.ErrorWrapper +import com.eltescode.core_data.utils.exception.ErrorWrapperImpl +import com.eltescode.core_data.utils.network.NetworkStateProvider +import com.eltescode.core_data.utils.network.NetworkStateProviderImpl import com.eltescode.notes_data.local.NoteDatabase import com.eltescode.notes_data.repository.NoteRepositoryImpl +import com.eltescode.notes_data.repository.SyncHelper +import com.eltescode.notes_data.repository.SyncHelperImpl +import com.eltescode.notes_data.utils.CustomPreferences +import com.eltescode.notes_data.utils.CustomPreferencesImpl import com.eltescode.notes_domain.repository.NoteRepository +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -27,8 +46,69 @@ object NotesDataModule { @Provides @Singleton - fun providesNoteRepository(db: NoteDatabase): NoteRepository { - return NoteRepositoryImpl(db.provideDao()) + fun providesConnectivityManager(@ApplicationContext context: Context): ConnectivityManager { + return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + } + + @Provides + @Singleton + fun providesSyncHelper(): SyncHelper { + return SyncHelperImpl + } + + @Provides + @Singleton + fun providesNetworkStateProvider(connectivityManager: ConnectivityManager): NetworkStateProvider { + return NetworkStateProviderImpl(connectivityManager) + } + + + @Provides + @Singleton + fun providesErrorWrapper(): ErrorWrapper { + return ErrorWrapperImpl() + } + + @Provides + @Singleton + fun providesErrorCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob()) + } + + @Provides + @Singleton + fun providesDataStore(@ApplicationContext context: Context): DataStore { + return PreferenceDataStoreFactory.create { context.preferencesDataStoreFile("data_store_pref") } + } + + @Provides + @Singleton + fun providesCustomPreferences(dataStore: DataStore): CustomPreferences { + return CustomPreferencesImpl(dataStore) + } + + @Provides + @Singleton + fun providesNoteRepository( + db: NoteDatabase, + auth: FirebaseAuth, + fireStore: FirebaseFirestore, + errorWrapper: ErrorWrapper, + networkStateProvider: NetworkStateProvider, + preferences: CustomPreferences, + syncHelper: SyncHelper, + coroutineScope: CoroutineScope + ): NoteRepository { + return NoteRepositoryImpl( + db.provideDao(), + errorWrapper, + auth, + fireStore, + networkStateProvider, + syncHelper, + preferences, + coroutineScope, + ) } diff --git a/notes/notes_data/src/main/java/com/eltescode/notes_data/local/NoteChached.kt b/notes/notes_data/src/main/java/com/eltescode/notes_data/local/NoteChached.kt index 31198c4..49d4454 100644 --- a/notes/notes_data/src/main/java/com/eltescode/notes_data/local/NoteChached.kt +++ b/notes/notes_data/src/main/java/com/eltescode/notes_data/local/NoteChached.kt @@ -2,16 +2,18 @@ package com.eltescode.notes_data.local import androidx.room.Entity import androidx.room.PrimaryKey +import java.util.UUID @Entity(tableName = "note_table") data class NoteCached( @PrimaryKey - val id: Int? = null, + val noteId: UUID, val title: String, val content: String, val timestamp: Long, val color: Int, - val attachment: String = "" + val attachment: String = "", + val userId: UUID? = null, ) { diff --git a/notes/notes_data/src/main/java/com/eltescode/notes_data/local/NoteDao.kt b/notes/notes_data/src/main/java/com/eltescode/notes_data/local/NoteDao.kt index 4b4c8d2..d9dcaa2 100644 --- a/notes/notes_data/src/main/java/com/eltescode/notes_data/local/NoteDao.kt +++ b/notes/notes_data/src/main/java/com/eltescode/notes_data/local/NoteDao.kt @@ -2,19 +2,26 @@ package com.eltescode.notes_data.local import androidx.room.* import kotlinx.coroutines.flow.Flow +import java.util.UUID @Dao interface NoteDao { @Query("SELECT * FROM note_table") - fun getAllNotes(): Flow> + fun getAllNotesFlow(): Flow> - @Query("SELECT * FROM note_table WHERE id = :id") - suspend fun getNoteById(id: Int): NoteCached? + @Query("SELECT * FROM note_table") + suspend fun getAllNotes(): List + + @Query("SELECT * FROM note_table WHERE noteId = :id") + suspend fun getNoteById(id: UUID): NoteCached? @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertNote(noteDomain: NoteCached) + suspend fun insertNote(vararg noteDomain: NoteCached) @Delete suspend fun deleteNote(noteDomain: NoteCached) + + @Query("DELETE FROM note_table") + suspend fun dropDataBase() } \ No newline at end of file diff --git a/notes/notes_data/src/main/java/com/eltescode/notes_data/mappers/NotesMapper.kt b/notes/notes_data/src/main/java/com/eltescode/notes_data/mappers/NotesMapper.kt index 1935804..ff62a73 100644 --- a/notes/notes_data/src/main/java/com/eltescode/notes_data/mappers/NotesMapper.kt +++ b/notes/notes_data/src/main/java/com/eltescode/notes_data/mappers/NotesMapper.kt @@ -2,9 +2,10 @@ package com.eltescode.notes_data.mappers import com.eltescode.notes_data.local.NoteCached import com.eltescode.notes_domain.model.Note +import java.util.UUID fun Note.mapToNoteCached() = NoteCached( - id = id, + noteId = UUID.fromString(noteId), title = title, content = content, timestamp = timestamp, @@ -13,7 +14,7 @@ fun Note.mapToNoteCached() = NoteCached( ) fun NoteCached.mapToNote() = Note( - id = id, + noteId = noteId.toString(), title = title, content = content, timestamp = timestamp, diff --git a/notes/notes_data/src/main/java/com/eltescode/notes_data/repository/NoteRepositoryImpl.kt b/notes/notes_data/src/main/java/com/eltescode/notes_data/repository/NoteRepositoryImpl.kt index b78a44d..7a64929 100644 --- a/notes/notes_data/src/main/java/com/eltescode/notes_data/repository/NoteRepositoryImpl.kt +++ b/notes/notes_data/src/main/java/com/eltescode/notes_data/repository/NoteRepositoryImpl.kt @@ -1,28 +1,184 @@ package com.eltescode.notes_data.repository +import com.eltescode.core_data.utils.exception.ErrorWrapper +import com.eltescode.core_data.utils.exception.callOrThrow +import com.eltescode.core_data.utils.network.NetworkStateProvider import com.eltescode.notes_data.local.NoteDao import com.eltescode.notes_data.mappers.mapToNote import com.eltescode.notes_data.mappers.mapToNoteCached +import com.eltescode.notes_data.utils.CustomPreferences import com.eltescode.notes_domain.model.Note import com.eltescode.notes_domain.repository.NoteRepository +import com.eltescode.notes_domain.repository.Result +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.tasks.await +import java.util.UUID -class NoteRepositoryImpl(private val dao: NoteDao) : NoteRepository { +class NoteRepositoryImpl( + private val dao: NoteDao, + private val errorWrapper: ErrorWrapper, + private val auth: FirebaseAuth, + private val fireStore: FirebaseFirestore, + private val networkStateProvider: NetworkStateProvider, + private val syncHelper: SyncHelper, + private val preferences: CustomPreferences, + private val externalScope: CoroutineScope, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, +) : NoteRepository { - override fun getAllNotes(): Flow> { - return dao.getAllNotes().map { list -> list.map { it.mapToNote() } } + override suspend fun syncNotes() { + if (networkStateProvider.isNetworkAvailable()) { + val remoteData = getAllNotesFromRemote() + val localData = getAllNotesFromLocal() + val areTheSame = + remoteData.size == localData.size + && remoteData.containsAll(localData) + && localData.containsAll(remoteData) + if (!areTheSame) { + val dataToSync = + syncHelper.compareData(remoteData = remoteData, localData = localData) + clearDataBase() + insertNotesToLocal(dataToSync) + insertNotesToRemote(dataToSync) + } + preferences.setSyncInfo(false) + } + + } + + override suspend fun getAllNotes(): Flow> { + return if (networkStateProvider.isNetworkAvailable()) { + callOrThrow(errorWrapper) { + val remoteData = getAllNotesFromRemote() + flow { emit(remoteData) } + } + } else { + getAllNotesFromLocalFlow() + } + } + + override suspend fun insertNote(note: Note): Result { + return if (networkStateProvider.isNetworkAvailable()) { + insertNoteToRemote(note).also { result -> + if (result == Result.SuccessRemote) insertNoteToLocal(note) + } + } else { + preferences.setSyncInfo(true) + insertNoteToLocal(note) + Result.SuccessLocal + } + } + + override suspend fun getNoteById(id: UUID): Note? { + return getNoteByIdFromLocal(id) + } + + override suspend fun deleteNote(note: Note): Result { + + return if (networkStateProvider.isNetworkAvailable()) { + deleteNoteFromRemote(note).also { result -> + if (result == Result.SuccessRemote) deleteNoteFromLocal(note) + } + + } else { + preferences.setSyncInfo(true) + deleteNoteFromLocal(note) + Result.SuccessLocal + } + } + + override suspend fun isNeededToSync(): Boolean { + return preferences.getSyncInfo() + } + + private suspend fun getAllNotesFromRemote(): List { + + val currentUserId = auth.currentUser?.uid ?: throw Exception("No such user") + val result = fireStore.collection("users") + .document(currentUserId) + .collection("notes") + .get() + .await() + + return result.toObjects(Note::class.java) + + } + + private fun getAllNotesFromLocalFlow(): Flow> { + return dao.getAllNotesFlow().map { list -> list.map { it.mapToNote() } } + } + + private suspend fun getAllNotesFromLocal(): List { + return dao.getAllNotes().map { list -> list.mapToNote() } } - override suspend fun getNoteById(id: Int): Note? { + private suspend fun getNoteByIdFromLocal(id: UUID): Note? { return dao.getNoteById(id)?.mapToNote() } - override suspend fun insertNote(noteDomain: Note) { - dao.insertNote(noteDomain.mapToNoteCached()) + private suspend fun insertNoteToRemote(note: Note): Result { + return try { + val currentUserId = auth.currentUser?.uid ?: throw Exception("No such user") + val currentNoteId = note.noteId + fireStore.collection("users") + .document(currentUserId) + .collection("notes") + .document("Note $currentNoteId") + .set(note) + .await() + + Result.SuccessRemote + } catch (e: Exception) { + Result.Error(e.message) + } + } + + private suspend fun insertNotesToRemote(notes: List) { + notes.forEach { + insertNoteToRemote(it) + } } - override suspend fun deleteNote(noteDomain: Note) { + private suspend fun insertNoteToLocal(note: Note) { + dao.insertNote(note.mapToNoteCached()) + } + + private suspend fun insertNotesToLocal(notes: List) { + notes + .map { it.mapToNoteCached() } + .toTypedArray() + .let { dao.insertNote(*it) } + } + + private suspend fun deleteNoteFromRemote(note: Note): Result { + return try { + val currentUserId = auth.currentUser?.uid ?: throw Exception("No such user") + val currentNoteId = note.noteId + fireStore.collection("users") + .document(currentUserId) + .collection("notes") + .document("Note $currentNoteId") + .delete() + .await() + Result.SuccessRemote + } catch (e: Exception) { + Result.Error(e.message) + } + } + + private suspend fun deleteNoteFromLocal(noteDomain: Note) { dao.deleteNote(noteDomain.mapToNoteCached()) } + + private suspend fun clearDataBase() { + dao.dropDataBase() + } + } \ No newline at end of file diff --git a/notes/notes_data/src/main/java/com/eltescode/notes_data/repository/SyncHelper.kt b/notes/notes_data/src/main/java/com/eltescode/notes_data/repository/SyncHelper.kt new file mode 100644 index 0000000..92979bb --- /dev/null +++ b/notes/notes_data/src/main/java/com/eltescode/notes_data/repository/SyncHelper.kt @@ -0,0 +1,9 @@ +package com.eltescode.notes_data.repository + +import com.eltescode.notes_domain.model.Note + +interface SyncHelper { + fun compareData(localData: List, remoteData: List): List + +} + diff --git a/notes/notes_data/src/main/java/com/eltescode/notes_data/repository/SyncHelperImpl.kt b/notes/notes_data/src/main/java/com/eltescode/notes_data/repository/SyncHelperImpl.kt new file mode 100644 index 0000000..8c86fe6 --- /dev/null +++ b/notes/notes_data/src/main/java/com/eltescode/notes_data/repository/SyncHelperImpl.kt @@ -0,0 +1,38 @@ +package com.eltescode.notes_data.repository + +import com.eltescode.notes_domain.model.Note + +object SyncHelperImpl : SyncHelper { + + override fun compareData(localData: List, remoteData: List): List { + val newList: MutableList = mutableListOf() + + localData.forEach { localNote -> + val remoteNote = remoteData.firstOrNull { localNote.noteId == it.noteId } + if (remoteNote != null) { + if (localNote.timestamp > remoteNote.timestamp) { + newList.add(localNote) + } else { + newList.add(remoteNote) + } + } else { + newList.add(localNote) + } + } + remoteData.forEach { remoteNote -> + val localNote = localData.firstOrNull { remoteNote.noteId == it.noteId } + if (localNote != null) { + if (remoteNote.timestamp > localNote.timestamp) { + newList.add(remoteNote) + } else { + newList.add(localNote) + } + } else { + newList.add(remoteNote) + } + } + + return newList.distinct() + } + +} \ No newline at end of file diff --git a/notes/notes_data/src/main/java/com/eltescode/notes_data/utils/CustomPreferences.kt b/notes/notes_data/src/main/java/com/eltescode/notes_data/utils/CustomPreferences.kt new file mode 100644 index 0000000..3a5a897 --- /dev/null +++ b/notes/notes_data/src/main/java/com/eltescode/notes_data/utils/CustomPreferences.kt @@ -0,0 +1,8 @@ +package com.eltescode.notes_data.utils + +interface CustomPreferences { + + suspend fun setSyncInfo(syncInfo: Boolean) + + suspend fun getSyncInfo(): Boolean +} \ No newline at end of file diff --git a/notes/notes_data/src/main/java/com/eltescode/notes_data/utils/CustomPreferencesImpl.kt b/notes/notes_data/src/main/java/com/eltescode/notes_data/utils/CustomPreferencesImpl.kt new file mode 100644 index 0000000..246a377 --- /dev/null +++ b/notes/notes_data/src/main/java/com/eltescode/notes_data/utils/CustomPreferencesImpl.kt @@ -0,0 +1,27 @@ +package com.eltescode.notes_data.utils + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.first + + +class CustomPreferencesImpl( + private val dataStore: DataStore +) : CustomPreferences { + override suspend fun setSyncInfo(syncInfo: Boolean) { + dataStore.edit { + it[SYNC_INFO] = syncInfo + } + } + + override suspend fun getSyncInfo(): Boolean { + + return dataStore.data.first()[SYNC_INFO] ?: true + } + + companion object { + private val SYNC_INFO = booleanPreferencesKey("SYNC_INFO") + } +} \ No newline at end of file diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/di/NotesDomainModule.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/di/NotesDomainModule.kt index 9884c34..bf38393 100644 --- a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/di/NotesDomainModule.kt +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/di/NotesDomainModule.kt @@ -2,10 +2,13 @@ package com.eltescode.notes_domain.di import com.eltescode.notes_domain.repository.NoteRepository import com.eltescode.notes_domain.use_cases.AddNoteUseCase +import com.eltescode.notes_domain.use_cases.CheckSyncNeedUseCase import com.eltescode.notes_domain.use_cases.DeleteNoteUseCase import com.eltescode.notes_domain.use_cases.GetNoteUseCase import com.eltescode.notes_domain.use_cases.GetNotesUseCase import com.eltescode.notes_domain.use_cases.NoteUseCases +import com.eltescode.notes_domain.use_cases.SortNotesUseCase +import com.eltescode.notes_domain.use_cases.SyncDataUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,10 +22,13 @@ object NotesDomainModule { @Singleton fun providesNoteUseCases(repository: NoteRepository): NoteUseCases { return NoteUseCases( - getNotesUseCase = GetNotesUseCase(repository), deleteNoteUseCase = DeleteNoteUseCase(repository), getNoteUseCase = GetNoteUseCase(repository), - addNoteUseCase = AddNoteUseCase(repository) + addNoteUseCase = AddNoteUseCase(repository), + sortNotesUseCase = SortNotesUseCase(), + getNotesUseCase = GetNotesUseCase(repository), + syncDataUseCase = SyncDataUseCase(repository), + checkSyncNeedUseCase = CheckSyncNeedUseCase(repository) ) } } \ No newline at end of file diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/model/Note.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/model/Note.kt index 78836a3..6be3ae3 100644 --- a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/model/Note.kt +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/model/Note.kt @@ -2,10 +2,10 @@ package com.eltescode.notes_domain.model data class Note( - val title: String, - val content: String, - val timestamp: Long, - val color: Int, - val id: Int? = null, - val attachment: String + val title: String = "", + val content: String = "", + val timestamp: Long = -1, + val color: Int = -1, + val noteId: String = "", + val attachment: String = "", ) \ No newline at end of file diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/repository/NoteRepository.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/repository/NoteRepository.kt index 2dfcc63..a58627d 100644 --- a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/repository/NoteRepository.kt +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/repository/NoteRepository.kt @@ -2,14 +2,25 @@ package com.eltescode.notes_domain.repository import com.eltescode.notes_domain.model.Note import kotlinx.coroutines.flow.Flow +import java.util.UUID interface NoteRepository { - fun getAllNotes(): Flow> + suspend fun getAllNotes(): Flow> - suspend fun getNoteById(id: Int): Note? + suspend fun getNoteById(id: UUID): Note? - suspend fun insertNote(noteDomain: Note) + suspend fun insertNote(note: Note): Result - suspend fun deleteNote(noteDomain: Note) + suspend fun deleteNote(note: Note): Result + + suspend fun isNeededToSync(): Boolean + + suspend fun syncNotes() +} + +sealed interface Result { + object SuccessRemote : Result + object SuccessLocal : Result + data class Error(val message: String?) : Result } \ No newline at end of file diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/AddNoteUseCase.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/AddNoteUseCase.kt index 0917c32..39b46fe 100644 --- a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/AddNoteUseCase.kt +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/AddNoteUseCase.kt @@ -2,14 +2,15 @@ package com.eltescode.notes_domain.use_cases import com.eltescode.notes_domain.model.Note import com.eltescode.notes_domain.repository.NoteRepository +import com.eltescode.notes_domain.repository.Result import com.eltescode.notes_domain.utils.InvalidNoteException class AddNoteUseCase(private val repository: NoteRepository) { - suspend operator fun invoke(note: Note) { + suspend operator fun invoke(note: Note): Result { if (note.title.isBlank()) { throw InvalidNoteException("Title can not be blank") } - repository.insertNote(note) + return repository.insertNote(note) } } \ No newline at end of file diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/CheckSyncNeedUseCase.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/CheckSyncNeedUseCase.kt new file mode 100644 index 0000000..9fc593a --- /dev/null +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/CheckSyncNeedUseCase.kt @@ -0,0 +1,10 @@ +package com.eltescode.notes_domain.use_cases + +import com.eltescode.notes_domain.repository.NoteRepository + +class CheckSyncNeedUseCase(private val repository: NoteRepository) { + + suspend operator fun invoke(): Boolean { + return repository.isNeededToSync() + } +} \ No newline at end of file diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/DeleteNoteUseCase.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/DeleteNoteUseCase.kt index e9e3197..39c08d6 100644 --- a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/DeleteNoteUseCase.kt +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/DeleteNoteUseCase.kt @@ -2,10 +2,11 @@ package com.eltescode.notes_domain.use_cases import com.eltescode.notes_domain.model.Note import com.eltescode.notes_domain.repository.NoteRepository +import com.eltescode.notes_domain.repository.Result class DeleteNoteUseCase(private val repository: NoteRepository) { - suspend operator fun invoke(note: Note) { - repository.deleteNote(note) + suspend operator fun invoke(note: Note): Result { + return repository.deleteNote(note) } } \ No newline at end of file diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/GetNoteUseCase.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/GetNoteUseCase.kt index 70f857f..dec6cee 100644 --- a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/GetNoteUseCase.kt +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/GetNoteUseCase.kt @@ -2,10 +2,11 @@ package com.eltescode.notes_domain.use_cases import com.eltescode.notes_domain.model.Note import com.eltescode.notes_domain.repository.NoteRepository +import java.util.UUID class GetNoteUseCase(private val repository: NoteRepository) { - suspend operator fun invoke(id: Int): Note? { + suspend operator fun invoke(id: UUID): Note? { return repository.getNoteById(id) } } \ No newline at end of file diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/GetNotesUseCase.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/GetNotesUseCase.kt index 988c09e..493dd08 100644 --- a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/GetNotesUseCase.kt +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/GetNotesUseCase.kt @@ -2,32 +2,12 @@ package com.eltescode.notes_domain.use_cases import com.eltescode.notes_domain.model.Note import com.eltescode.notes_domain.repository.NoteRepository -import com.eltescode.notes_domain.utils.NoteOrder -import com.eltescode.notes_domain.utils.OrderType import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -class GetNotesUseCase(private val repository: NoteRepository) { - operator fun invoke(noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending)): Flow> { - return repository.getAllNotes().map { notes -> - when (noteOrder.orderType) { - is OrderType.Descending -> { - when (noteOrder) { - is NoteOrder.Title -> notes.sortedByDescending { it.title.lowercase() } - is NoteOrder.Date -> notes.sortedByDescending { it.timestamp } - is NoteOrder.Color -> notes.sortedByDescending { it.color } - } - } +class GetNotesUseCase(private val repository: NoteRepository) { - is OrderType.Ascending -> { - when (noteOrder) { - is NoteOrder.Title -> notes.sortedBy { it.title.lowercase() } - is NoteOrder.Date -> notes.sortedBy { it.timestamp } - is NoteOrder.Color -> notes.sortedBy { it.color } - } - } - } - } + suspend operator fun invoke(): Flow> { + return repository.getAllNotes() } -} \ No newline at end of file +} diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/NoteUseCases.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/NoteUseCases.kt index 20ed957..1374c0b 100644 --- a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/NoteUseCases.kt +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/NoteUseCases.kt @@ -4,5 +4,8 @@ data class NoteUseCases( val getNotesUseCase: GetNotesUseCase, val getNoteUseCase: GetNoteUseCase, val deleteNoteUseCase: DeleteNoteUseCase, - val addNoteUseCase: AddNoteUseCase + val addNoteUseCase: AddNoteUseCase, + val sortNotesUseCase: SortNotesUseCase, + val checkSyncNeedUseCase: CheckSyncNeedUseCase, + val syncDataUseCase: SyncDataUseCase ) diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/SortNotesUseCase.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/SortNotesUseCase.kt new file mode 100644 index 0000000..8d7da27 --- /dev/null +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/SortNotesUseCase.kt @@ -0,0 +1,33 @@ +package com.eltescode.notes_domain.use_cases + +import com.eltescode.notes_domain.model.Note +import com.eltescode.notes_domain.utils.NoteOrder +import com.eltescode.notes_domain.utils.OrderType + + +class SortNotesUseCase() { + + operator fun invoke( + notes: List, + noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending) + ): List { + return when (noteOrder.orderType) { + is OrderType.Descending -> { + when (noteOrder) { + is NoteOrder.Title -> notes.sortedByDescending { it.title.lowercase() } + is NoteOrder.Date -> notes.sortedByDescending { it.timestamp } + is NoteOrder.Color -> notes.sortedByDescending { it.color } + } + } + + is OrderType.Ascending -> { + when (noteOrder) { + is NoteOrder.Title -> notes.sortedBy { it.title.lowercase() } + is NoteOrder.Date -> notes.sortedBy { it.timestamp } + is NoteOrder.Color -> notes.sortedBy { it.color } + } + } + } + + } +} \ No newline at end of file diff --git a/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/SyncDataUseCase.kt b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/SyncDataUseCase.kt new file mode 100644 index 0000000..e26c3fb --- /dev/null +++ b/notes/notes_domain/src/main/java/com/eltescode/notes_domain/use_cases/SyncDataUseCase.kt @@ -0,0 +1,10 @@ +package com.eltescode.notes_domain.use_cases + +import com.eltescode.notes_domain.repository.NoteRepository + +class SyncDataUseCase(private val repository: NoteRepository) { + + suspend operator fun invoke() { + return repository.syncNotes() + } +} \ No newline at end of file diff --git a/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/AddEditNoteScreen.kt b/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/AddEditNoteScreen.kt index 41ea883..62503a9 100644 --- a/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/AddEditNoteScreen.kt +++ b/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/AddEditNoteScreen.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.eltescode.notes.features.note.presentation.add_edit_note.UiEvent import com.eltescode.notes_presentation.add_edit_note.components.TransparentTextField import com.eltescode.notes_presentation.model.NoteDisplayable import kotlinx.coroutines.flow.collectLatest diff --git a/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/AddEditNoteViewModel.kt b/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/AddEditNoteViewModel.kt index 9f2aea4..e4bded7 100644 --- a/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/AddEditNoteViewModel.kt +++ b/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/AddEditNoteViewModel.kt @@ -8,8 +8,8 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.eltescode.notes.features.note.presentation.add_edit_note.NoteTextFieldState -import com.eltescode.notes.features.note.presentation.add_edit_note.UiEvent import com.eltescode.notes_domain.model.Note +import com.eltescode.notes_domain.repository.Result import com.eltescode.notes_domain.use_cases.NoteUseCases import com.eltescode.notes_domain.utils.InvalidNoteException import com.eltescode.notes_presentation.mappers.mapToNote @@ -18,6 +18,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -47,14 +48,14 @@ class AddEditNoteViewModel @Inject constructor( private val _eventFlow = MutableSharedFlow() val eventFlow: SharedFlow = _eventFlow - private var currentNoteId: Int? = null + private var currentNoteId: UUID = UUID.randomUUID() init { - savedStateHandle.get("noteId")?.let { noteId -> - if (noteId != -1) { + savedStateHandle.get("noteId")?.let { noteId -> + if (noteId != "-1") { viewModelScope.launch { - noteUseCases.getNoteUseCase(noteId)?.also { note: Note -> - currentNoteId = note.id + noteUseCases.getNoteUseCase(UUID.fromString(noteId))?.also { note: Note -> + currentNoteId = UUID.fromString(note.noteId) _noteTitle.value = noteTitle.value.copy(text = note.title, isHintVisible = false) _noteContent.value = @@ -98,16 +99,29 @@ class AddEditNoteViewModel @Inject constructor( AddEditNoteEvent.SaveNote -> { viewModelScope.launch { try { - noteUseCases.addNoteUseCase( + val result = noteUseCases.addNoteUseCase( NoteDisplayable( title = noteTitle.value.text, content = noteContent.value.text, timestamp = System.currentTimeMillis(), color = noteColor.value, - id = currentNoteId, + noteId = currentNoteId, ).mapToNote() ) - _eventFlow.emit(UiEvent.SaveNote) + when (result) { + is Result.Error -> { + throw Exception(result.message) + } + + Result.SuccessLocal -> { + _eventFlow.emit(UiEvent.SaveNote) + } + + Result.SuccessRemote -> { + _eventFlow.emit(UiEvent.SaveNote) + } + } + } catch (e: InvalidNoteException) { _eventFlow.emit(UiEvent.ShowSnackBar("Error: $e")) } diff --git a/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/UiEvent.kt b/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/UiEvent.kt index 4fc63bf..a8c3e26 100644 --- a/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/UiEvent.kt +++ b/notes_presentation/src/main/java/com/eltescode/notes_presentation/add_edit_note/UiEvent.kt @@ -1,4 +1,4 @@ -package com.eltescode.notes.features.note.presentation.add_edit_note +package com.eltescode.notes_presentation.add_edit_note sealed class UiEvent { data class ShowSnackBar(val message: String) : UiEvent() diff --git a/notes_presentation/src/main/java/com/eltescode/notes_presentation/mappers/NotesMapper.kt b/notes_presentation/src/main/java/com/eltescode/notes_presentation/mappers/NotesMapper.kt index 3c602ce..95f09b2 100644 --- a/notes_presentation/src/main/java/com/eltescode/notes_presentation/mappers/NotesMapper.kt +++ b/notes_presentation/src/main/java/com/eltescode/notes_presentation/mappers/NotesMapper.kt @@ -2,9 +2,10 @@ package com.eltescode.notes_presentation.mappers import com.eltescode.notes_domain.model.Note import com.eltescode.notes_presentation.model.NoteDisplayable +import java.util.UUID fun Note.mapToNoteDisplayable() = NoteDisplayable( - id = id, + noteId = UUID.fromString(noteId), title = title, content = content, timestamp = timestamp, @@ -13,7 +14,7 @@ fun Note.mapToNoteDisplayable() = NoteDisplayable( ) fun NoteDisplayable.mapToNote() = Note( - id = id, + noteId = noteId.toString(), title = title, content = content, timestamp = timestamp, diff --git a/notes_presentation/src/main/java/com/eltescode/notes_presentation/model/NoteDisplayable.kt b/notes_presentation/src/main/java/com/eltescode/notes_presentation/model/NoteDisplayable.kt index a8e619c..24bef87 100644 --- a/notes_presentation/src/main/java/com/eltescode/notes_presentation/model/NoteDisplayable.kt +++ b/notes_presentation/src/main/java/com/eltescode/notes_presentation/model/NoteDisplayable.kt @@ -1,15 +1,16 @@ package com.eltescode.notes_presentation.model import androidx.compose.ui.graphics.Color +import java.util.UUID data class NoteDisplayable( - val id: Int? = null, + val noteId: UUID, val title: String, val content: String, val timestamp: Long, val color: Int, - val attachment: String = "" + val attachment: String = "", ) { companion object { diff --git a/notes_presentation/src/main/java/com/eltescode/notes_presentation/notes/NotesScreen.kt b/notes_presentation/src/main/java/com/eltescode/notes_presentation/notes/NotesScreen.kt index e9faa99..d29ab67 100644 --- a/notes_presentation/src/main/java/com/eltescode/notes_presentation/notes/NotesScreen.kt +++ b/notes_presentation/src/main/java/com/eltescode/notes_presentation/notes/NotesScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -47,6 +48,7 @@ import com.eltescode.notes_presentation.notes.comopnents.NoteItem import com.eltescode.notes_presentation.notes.comopnents.OrderSection import com.eltescode.notes_presentation.util.NotesEvent import com.eltescode.notes_presentation.util.NotesState +import kotlinx.coroutines.launch @Composable @@ -58,6 +60,7 @@ fun NotesScreen( val state = viewModel.state.value val context = LocalContext.current + val scope = rememberCoroutineScope() LaunchedEffect(key1 = true) { viewModel.uiEvent.collect { event -> @@ -65,22 +68,21 @@ fun NotesScreen( is UiEvent.OnNextScreen -> { onNextScreen(event.route) } - is UiEvent.ShowSnackBar -> { - snackBarHostState.currentSnackbarData?.dismiss() + scope.launch { + snackBarHostState.currentSnackbarData?.dismiss() - val result = snackBarHostState.showSnackbar( - message = event.message.asString(context), - actionLabel = context.getString(R.string.undo), - duration = SnackbarDuration.Long - ) + val result = snackBarHostState.showSnackbar( + message = event.message.asString(context), + actionLabel = context.getString(R.string.undo), + duration = SnackbarDuration.Long + ) - if (result == SnackbarResult.ActionPerformed) { - viewModel.onEvent(NotesEvent.RestoreNote) + if (result == SnackbarResult.ActionPerformed) { + viewModel.onEvent(NotesEvent.RestoreNote) + } } - } - else -> Unit } } @@ -138,7 +140,12 @@ fun NotesScreen(state: NotesState, onEvent: (NotesEvent) -> Unit) { modifier = Modifier .fillMaxWidth() .clickable { - onEvent(NotesEvent.OnAddEditNote(id = note.id, color = note.color)) + onEvent( + NotesEvent.OnAddEditNote( + id = note.noteId, + color = note.color + ) + ) }, onDeleteClick = { onEvent(NotesEvent.DeleteNote(note)) diff --git a/notes_presentation/src/main/java/com/eltescode/notes_presentation/notes/NotesViewModel.kt b/notes_presentation/src/main/java/com/eltescode/notes_presentation/notes/NotesViewModel.kt index 225ffab..a80127d 100644 --- a/notes_presentation/src/main/java/com/eltescode/notes_presentation/notes/NotesViewModel.kt +++ b/notes_presentation/src/main/java/com/eltescode/notes_presentation/notes/NotesViewModel.kt @@ -1,5 +1,6 @@ package com.eltescode.notes_presentation.notes +import android.util.Log import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel @@ -7,9 +8,10 @@ import androidx.lifecycle.viewModelScope import com.eltescode.core_ui.R import com.eltescode.core_ui.utils.UiEvent import com.eltescode.core_ui.utils.UiText +import com.eltescode.notes_domain.model.Note +import com.eltescode.notes_domain.repository.Result import com.eltescode.notes_domain.use_cases.NoteUseCases import com.eltescode.notes_domain.utils.NoteOrder -import com.eltescode.notes_domain.utils.OrderType import com.eltescode.notes_presentation.mappers.mapToNote import com.eltescode.notes_presentation.mappers.mapToNoteDisplayable import com.eltescode.notes_presentation.model.NoteDisplayable @@ -40,19 +42,52 @@ class NotesViewModel @Inject constructor(private val noteUseCases: NoteUseCases) private var job: Job? = null + private var job2: Job? = null + init { - getNotes(NoteOrder.Date(OrderType.Descending)) + + + viewModelScope.launch { + Log.d("SYNC", "${noteUseCases.checkSyncNeedUseCase()}") + if (noteUseCases.checkSyncNeedUseCase()) { + syncData() + getNotes() + } else { + getNotes() + } + } } fun onEvent(event: NotesEvent) { when (event) { is NotesEvent.DeleteNote -> { job = null - job = viewModelScope.launch { - noteUseCases.deleteNoteUseCase(event.note.mapToNote()) - recentlyDeletedNote = event.note - _uiEvent.send(UiEvent.ShowSnackBar(UiText.StringResource(R.string.note_deleted))) - } + job = + viewModelScope.launch { + try { + val result = noteUseCases.deleteNoteUseCase(event.note.mapToNote()) + when (result) { + is Result.Error -> { + throw Exception(result.message) + } + + Result.SuccessLocal -> { + recentlyDeletedNote = event.note + _uiEvent.send(UiEvent.ShowSnackBar(UiText.StringResource(R.string.note_deleted))) + getNotes() + } + + Result.SuccessRemote -> { + recentlyDeletedNote = event.note + _uiEvent.send(UiEvent.ShowSnackBar(UiText.StringResource(R.string.note_deleted))) + getNotes() + } + + } + } catch (e: Exception) { + _uiEvent.send(UiEvent.ShowSnackBar(UiText.StringResource(R.string.error_basic))) + } + } } is NotesEvent.Order -> { @@ -61,7 +96,8 @@ class NotesViewModel @Inject constructor(private val noteUseCases: NoteUseCases) ) { return } - getNotes(event.noteOrder) + _state.value = _state.value.copy(noteOrder = event.noteOrder) + sortNotes(state.value.notes.map { it.mapToNote() }, event.noteOrder) } is NotesEvent.RestoreNote -> { @@ -69,6 +105,7 @@ class NotesViewModel @Inject constructor(private val noteUseCases: NoteUseCases) job = viewModelScope.launch { noteUseCases.addNoteUseCase(recentlyDeletedNote?.mapToNote() ?: return@launch) recentlyDeletedNote = null + getNotes() } } @@ -97,13 +134,22 @@ class NotesViewModel @Inject constructor(private val noteUseCases: NoteUseCases) } } - private fun getNotes(noteOrder: NoteOrder) { + private suspend fun getNotes() { getNotesJob?.cancel() - getNotesJob = noteUseCases.getNotesUseCase(noteOrder).onEach { notes -> - _state.value = state.value.copy( - notes = notes.map { it.mapToNoteDisplayable() }, - noteOrder = noteOrder - ) + getNotesJob = noteUseCases.getNotesUseCase().onEach { notes -> + sortNotes(notes, state.value.noteOrder) }.launchIn(viewModelScope) } + + private fun sortNotes(notes: List, noteOrder: NoteOrder) { + val sortedNotes = + noteUseCases.sortNotesUseCase(notes, noteOrder).map { it.mapToNoteDisplayable() } + _state.value = state.value.copy( + notes = sortedNotes, + ) + } + + private suspend fun syncData() { + noteUseCases.syncDataUseCase() + } } \ No newline at end of file diff --git a/notes_presentation/src/main/java/com/eltescode/notes_presentation/util/NotesEvent.kt b/notes_presentation/src/main/java/com/eltescode/notes_presentation/util/NotesEvent.kt index 902a61f..4bd5912 100644 --- a/notes_presentation/src/main/java/com/eltescode/notes_presentation/util/NotesEvent.kt +++ b/notes_presentation/src/main/java/com/eltescode/notes_presentation/util/NotesEvent.kt @@ -2,13 +2,14 @@ package com.eltescode.notes_presentation.util import com.eltescode.notes_domain.utils.NoteOrder import com.eltescode.notes_presentation.model.NoteDisplayable +import java.util.UUID sealed interface NotesEvent { data class Order(val noteOrder: NoteOrder) : NotesEvent data class DeleteNote(val note: NoteDisplayable) : NotesEvent object RestoreNote : NotesEvent object ToggleOrderSection : NotesEvent - data class OnAddEditNote(val id: Int?, val color: Int?) : NotesEvent + data class OnAddEditNote(val id: UUID?, val color: Int?) : NotesEvent } diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/components/BaseCard.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/components/BaseCard.kt deleted file mode 100644 index 582ff14..0000000 --- a/user_presentation/src/main/java/com/eltescode/user_presentation/components/BaseCard.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.eltescode.user_presentation.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp - -@Composable -fun BaseCard( - text: String, modifier: Modifier = Modifier, fontSize: TextUnit = 32.sp, - containerColor: Color = Color.LightGray -) { - Card( - modifier = modifier, - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - colors = CardDefaults.cardColors(containerColor = containerColor.copy(alpha = 0.5f)) - ) - { - Column( - modifier = Modifier - .fillMaxSize() - .padding(4.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - CustomText( - text = text, - fontSize = fontSize, - ) - } - } -} - -@Preview(showSystemUi = true) -@Composable -fun BaseCard() { - Box(contentAlignment = Alignment.Center) { - BaseCard(text = "Notes", modifier = Modifier.size(300.dp)) - } -} \ No newline at end of file diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomBackground.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomBackground.kt new file mode 100644 index 0000000..7eea786 --- /dev/null +++ b/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomBackground.kt @@ -0,0 +1,138 @@ +package com.eltescode.user_presentation.components + +import android.util.Log +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.StartOffset +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.eltescode.core_ui.ui.BlueColors +import com.eltescode.core_ui.ui.SilverColors +import kotlin.math.abs + +@Composable +fun CustomBackground( + modifier: Modifier = Modifier, + backgroundColor: Brush = Brush.linearGradient( + listOf( + BlueColors.color4e6db1, + BlueColors.color91b1fd + ) + ), + animatedColor: Brush = Brush.linearGradient( + listOf( + BlueColors.color91b1fd, + BlueColors.color4e6db1, + SilverColors.color5f6264 + ) + ) +) { + val infinityTrans = rememberInfiniteTransition(label = "") + + val initialStartOffset = remember { StartOffset((0..8000).shuffled().first()) } + + Log.d("OFFSET", "${initialStartOffset.offsetMillis}") + + val point1 by infinityTrans.animateFloat( + initialValue = 0.3f, targetValue = 0.7f, animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 10000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, initialStartOffset = initialStartOffset + ), label = "" + ) + + val point2 by infinityTrans.animateFloat( + initialValue = 0.35f, targetValue = 0.7f, animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 10000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, initialStartOffset = initialStartOffset + ), label = "" + ) + + val point3 by infinityTrans.animateFloat( + initialValue = 0.05f, targetValue = 0.7f, animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 10000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, initialStartOffset = initialStartOffset + ), label = "" + ) + + val point4 by infinityTrans.animateFloat( + initialValue = 0.7f, targetValue = 0.05f, animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 10000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, initialStartOffset = initialStartOffset + ), label = "" + ) + + val point5 by infinityTrans.animateFloat( + initialValue = 1f, targetValue = 1.2f, animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 10000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, initialStartOffset = initialStartOffset + ), label = "" + ) + + BoxWithConstraints( + modifier = modifier + .background(backgroundColor) + ) { + val width = constraints.maxWidth + val height = constraints.maxHeight + + + val pathPoint1 = Offset(0f, height * point1) + val pathPoint2 = Offset(width * 0.1f, height * point2) + val pathPoint3 = Offset(width * 0.4f, height * point3) + val pathPoint4 = Offset(width * 0.75f, height * point4) + val pathPoint5 = Offset(width * 1.4f, -height.toFloat() * point5) + + val mediumColoredPath = Path().apply { + moveTo(pathPoint1.x, pathPoint1.y) + standardQuadFromTo(pathPoint1, pathPoint2) + standardQuadFromTo(pathPoint2, pathPoint3) + standardQuadFromTo(pathPoint3, pathPoint4) + standardQuadFromTo(pathPoint4, pathPoint5) + lineTo(width.toFloat() + 100f, height.toFloat() + 100f) + lineTo(-100f, height.toFloat() + 100f) + close() + } + Canvas( + modifier = Modifier.fillMaxSize() + ) { + drawPath( + path = mediumColoredPath, brush = animatedColor + ) + } + } +} + +private fun Path.standardQuadFromTo(from: Offset, to: Offset) { + quadraticBezierTo( + from.x, + from.y, + abs(from.x + to.x) / 2f, + abs(from.y + to.y) / 2f + ) +} + +@Preview +@Composable +private fun CustomBackground() { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CustomBackground(modifier = Modifier.size(300.dp)) + } +} \ No newline at end of file diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomCard.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomCard.kt new file mode 100644 index 0000000..200b401 --- /dev/null +++ b/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomCard.kt @@ -0,0 +1,55 @@ +package com.eltescode.user_presentation.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun CustomCard( + text: String, + modifier: Modifier = Modifier, + fontSize: TextUnit = 32.sp, +) { + Box( + modifier = modifier + .clip( + RoundedCornerShape( + topStart = 25.dp, + topEnd = 40.dp, + bottomEnd = 25.dp + ) + ), + contentAlignment = Alignment.Center + ) + + { + CustomBackground( + modifier = Modifier.fillMaxSize(), + ) + CustomText( + text = text, + fontSize = fontSize, + color = Color.White, + modifier = Modifier.padding(8.dp) + ) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun CustomCard() { + Box(contentAlignment = Alignment.Center) { + CustomCard(text = "Notes", modifier = Modifier.size(300.dp)) + } +} \ No newline at end of file diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomCircularProgressBar.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomCircularProgressBar.kt new file mode 100644 index 0000000..5d0513f --- /dev/null +++ b/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomCircularProgressBar.kt @@ -0,0 +1,103 @@ +package com.eltescode.user_presentation.components + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + + +@Composable +fun CustomCircularProgressBar( + modifier: Modifier = Modifier, + durationMillis: Int = 1000, + border: Dp = 15.dp, + loadingColor: Color = Color.Red, + backgroundColor: Color = Color.White +) { + + val infinityTrans = rememberInfiniteTransition(label = "") + + val angle by infinityTrans.animateFloat( + initialValue = 0F, targetValue = 359F, animationSpec = infiniteRepeatable( + animation = tween(durationMillis = durationMillis, easing = FastOutSlowInEasing) + ), label = "" + ) + + val color1 by infinityTrans.animateColor( + initialValue = Color.Transparent, + targetValue = loadingColor, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = durationMillis, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse + ), + label = "" + ) + val color2 by infinityTrans.animateColor( + initialValue = backgroundColor, + targetValue = loadingColor, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = durationMillis, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "" + ) + + val horizontalBrush = Brush.sweepGradient(listOf(color1, color2)) + + + BoxWithConstraints( + modifier = modifier + .clip(CircleShape) + .background(backgroundColor), + contentAlignment = Alignment.Center + ) { + val boxWithConstraintsScope = this + val size = boxWithConstraintsScope.maxHeight + Box( + modifier = Modifier + .rotate(angle) + .size(size) + .background(horizontalBrush) + ) + + Box( + modifier = Modifier + .size(size - border) + .clip(CircleShape) + .background(backgroundColor), + ) + } + +} + +@Preview(showSystemUi = true) +@Composable +private fun CustomCircularProgressBar() { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CustomCircularProgressBar(modifier = Modifier.size(100.dp)) + } +} + + + diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomVerticalGrid.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomVerticalGrid.kt new file mode 100644 index 0000000..0490e2d --- /dev/null +++ b/user_presentation/src/main/java/com/eltescode/user_presentation/components/CustomVerticalGrid.kt @@ -0,0 +1,48 @@ +package com.eltescode.user_presentation.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import com.eltescode.user_presentation.utils.UserScreens + + +@Composable +fun CustomLazyVerticalGrid( + modifier: Modifier = Modifier, + cardData: Array, + content: @Composable (Int, String) -> Unit +) { + LazyVerticalGrid( + columns = object : GridCells { + override fun Density.calculateCrossAxisCellSizes( + availableSize: Int, + spacing: Int + ): List { + val firstColumn = (availableSize - spacing) * 1 / 2 + val secondColumn = availableSize - spacing - firstColumn + return listOf(firstColumn, secondColumn) + } + }, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier, + ) { + cardData.forEachIndexed { index, item -> + if (index == 0) { + item(span = { GridItemSpan(maxLineSpan) }) { + content(item.screenNameRes, item.route) + } + } else { + item(span = { GridItemSpan(1) }) { + content(item.screenNameRes, item.route) + } + } + } + } +} + diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/components/UserPicture.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/components/UserPicture.kt index 55cb1a9..60ec829 100644 --- a/user_presentation/src/main/java/com/eltescode/user_presentation/components/UserPicture.kt +++ b/user_presentation/src/main/java/com/eltescode/user_presentation/components/UserPicture.kt @@ -21,24 +21,33 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage +import com.eltescode.core_ui.ui.SilverColors @Composable fun UserPicture( userPhoto: String, userName: String, userSurname: String, + isPhotoLoading: Boolean, modifier: Modifier = Modifier, size: Dp = 150.dp, - onClick: () -> Unit -) { + onClick: () -> Unit, - Box( - modifier = modifier - .size(size) - .clip(RoundedCornerShape(100.dp)) - .background(Color.LightGray), - contentAlignment = Alignment.Center - ) { + ) { + + Box( + modifier = modifier + .size(size) + .clip(RoundedCornerShape(100.dp)) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + if (isPhotoLoading) { + CustomCircularProgressBar( + modifier = Modifier.size(100.dp), + loadingColor = SilverColors.color979c9f + ) + } else { AsyncImage( model = userPhoto, contentDescription = null, @@ -49,33 +58,34 @@ fun UserPicture( onClick() } ) - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.verticalGradient( - colors = listOf(Color.Transparent, Color.Black), - startY = size.value * 2f, - tileMode = TileMode.Clamp - ) - ) - ) - CustomText( - text = "$userName $userSurname".trim(), - fontSize = (size.value / 15f).sp, - color = Color.White, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(bottom = 16.dp) - ) } + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf(Color.Transparent, Color.Black), + startY = size.value * 2f, + tileMode = TileMode.Clamp + ) + ) + ) + CustomText( + text = "$userName $userSurname".trim(), + fontSize = (size.value / 15f).sp, + color = Color.White, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp) + ) + } } @Preview @Composable -fun UserPicture() { - UserPicture("", "Admin", "Admin") {} +private fun UserPicture() { + UserPicture("", "Admin", "Admin", false) {} } \ No newline at end of file diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/user_screen/UserDataScreen.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/user_screen/UserDataScreen.kt index a886542..f3df3bf 100644 --- a/user_presentation/src/main/java/com/eltescode/user_presentation/user_screen/UserDataScreen.kt +++ b/user_presentation/src/main/java/com/eltescode/user_presentation/user_screen/UserDataScreen.kt @@ -11,15 +11,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -29,7 +25,8 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.work.WorkManager import com.eltescode.core_ui.components.silverBackgroundBrush -import com.eltescode.user_presentation.components.BaseCard +import com.eltescode.user_presentation.components.CustomCard +import com.eltescode.user_presentation.components.CustomLazyVerticalGrid import com.eltescode.user_presentation.components.PhotoChooserDialog import com.eltescode.user_presentation.components.SettingsDialog import com.eltescode.user_presentation.components.SettingsIcon @@ -63,6 +60,7 @@ fun UserDataScreen( if (isPhotoTaken) { if (state.photoUri != null) { val request = photoOneTimeWorkRequestBuilder(state.photoUri) + viewModel.onEvent(UserDataScreenEvent.OnStarPhotoLoading) viewModel.updateWorkId(request.id) viewModel.onEvent( UserDataScreenEvent.PhotoDialogEvents.OnWorkManagerEnqueue( @@ -77,6 +75,7 @@ fun UserDataScreen( contract = ActivityResultContracts.OpenDocument(), onResult = { photoUri -> if (photoUri != null) { + viewModel.onEvent(UserDataScreenEvent.OnStarPhotoLoading) val request = photoOneTimeWorkRequestBuilder(photoUri) viewModel.updateWorkId(request.id) viewModel.onEvent(UserDataScreenEvent.PhotoDialogEvents.OnWorkManagerEnqueue(request)) @@ -163,31 +162,26 @@ fun UserDataScreen(state: UserScreenState, onEvent: (UserDataScreenEvent) -> Uni userPhoto = state.userData?.photo ?: "", userName = state.userData?.name ?: "", userSurname = state.userData?.surname ?: "", + isPhotoLoading = state.isPhotoLoading, onClick = { onEvent(UserDataScreenEvent.OnPhotoClick) } ) - LazyVerticalGrid( - columns = GridCells.Fixed(2), + CustomLazyVerticalGrid( + modifier = Modifier .fillMaxSize() .padding(top = 16.dp, bottom = 35.dp, start = 8.dp, end = 8.dp), - verticalArrangement = Arrangement.SpaceBetween, - horizontalArrangement = Arrangement.SpaceEvenly - ) { - UserScreens.values().forEach { - item { - BaseCard( - text = stringResource(id = it.screenNameRes), - modifier = Modifier - .size(125.dp) - .padding(3.dp) - .clip(RoundedCornerShape(8.dp)) - .clickable { onEvent(UserDataScreenEvent.OnNextScreenClick(it.route)) }, - fontSize = 18.sp - ) - } - } + cardData = UserScreens.values(), + ) { resource, route -> + CustomCard( + text = stringResource(id = resource), + modifier = Modifier + .size(150.dp) + .padding(4.dp) + .clickable { onEvent(UserDataScreenEvent.OnNextScreenClick(route)) }, + fontSize = 18.sp + ) } } diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/user_screen/UserDataViewModel.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/user_screen/UserDataViewModel.kt index c0b7329..9fea53d 100644 --- a/user_presentation/src/main/java/com/eltescode/user_presentation/user_screen/UserDataViewModel.kt +++ b/user_presentation/src/main/java/com/eltescode/user_presentation/user_screen/UserDataViewModel.kt @@ -46,9 +46,7 @@ class UserDataViewModel @Inject constructor( private var job: Job? = null init { - Log.d("InitVm VM", "${uriHelper.oldUri}") refreshUserData() - viewModelScope.launch { uriHelper.uriFlow.collect { doWork(it) @@ -57,7 +55,10 @@ class UserDataViewModel @Inject constructor( } private fun doWork(photoUri: Uri?) { + Log.d("VIEWMODEL", "${state.isPhotoLoading}") photoUri?.let { uri -> + onEvent(UserDataScreenEvent.OnStarPhotoLoading) + Log.d("VIEWMODEL", "${state.isPhotoLoading}") job = null job = viewModelScope.launch { val request = photoOneTimeWorkRequestBuilder(uri) @@ -134,10 +135,14 @@ class UserDataViewModel @Inject constructor( uploadUserPhoto(event.bytes) .onSuccess { photoUrl -> state = - state.copy(userData = userData.copy(photo = photoUrl.toString())) + state.copy( + userData = userData.copy(photo = photoUrl.toString()), + isPhotoLoading = false + ) handleEditUserDataResult(editUserData()) } .onFailure { throwable -> + state = state.copy(isPhotoLoading = false) val message = throwable.message.toString() _uiEvent.send(UiEvent.ShowSnackBar(UiText.DynamicString(message))) } @@ -184,6 +189,10 @@ class UserDataViewModel @Inject constructor( _photoEvent.send(event) } } + + UserDataScreenEvent.OnStarPhotoLoading -> { + state = state.copy(isPhotoLoading = true) + } } } diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/utils/PhotoCompressionWorker.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/utils/PhotoCompressionWorker.kt index e8547cb..2cebc6e 100644 --- a/user_presentation/src/main/java/com/eltescode/user_presentation/utils/PhotoCompressionWorker.kt +++ b/user_presentation/src/main/java/com/eltescode/user_presentation/utils/PhotoCompressionWorker.kt @@ -7,7 +7,6 @@ import android.graphics.Matrix import android.media.ExifInterface import android.net.Uri import android.os.Build -import android.util.Log import androidx.annotation.RequiresApi import androidx.core.net.toUri import androidx.work.Constraints @@ -48,7 +47,6 @@ class PhotoCompressionWorker(private val context: Context, private val params: W } stringUri = file.toUri().toString() } - Log.d("WORKER", "$stringUri") val compressionThresholdInBytes = params.inputData.getLong(KEY_PHOTO_COMPRESSION_THRESHOLD, 0L) diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/utils/UserDataScreenEvent.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/utils/UserDataScreenEvent.kt index 446cd7a..02003d5 100644 --- a/user_presentation/src/main/java/com/eltescode/user_presentation/utils/UserDataScreenEvent.kt +++ b/user_presentation/src/main/java/com/eltescode/user_presentation/utils/UserDataScreenEvent.kt @@ -9,6 +9,9 @@ sealed interface UserDataScreenEvent { object OnSettingsDialogDismiss : UserDataScreenEvent object OnSettingsSave : UserDataScreenEvent object OnPhotoClick : UserDataScreenEvent + + object OnStarPhotoLoading : UserDataScreenEvent + sealed interface PhotoDialogEvents : UserDataScreenEvent { object OnTakePhotoClick : PhotoDialogEvents diff --git a/user_presentation/src/main/java/com/eltescode/user_presentation/utils/UserScreenState.kt b/user_presentation/src/main/java/com/eltescode/user_presentation/utils/UserScreenState.kt index 2915790..5d8cfdb 100644 --- a/user_presentation/src/main/java/com/eltescode/user_presentation/utils/UserScreenState.kt +++ b/user_presentation/src/main/java/com/eltescode/user_presentation/utils/UserScreenState.kt @@ -7,6 +7,7 @@ data class UserScreenState( val userData: CustomUserData?, val isSettingsDialogVisible: Boolean = false, val isChoosePhotoDialogVisible: Boolean = false, + val isPhotoLoading: Boolean = false, val photoUri: Uri? = null, )