From 36cdebf862eeaadf3c468da1e4bdda319d075515 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Minh Date: Sun, 15 Dec 2024 12:37:45 +0700 Subject: [PATCH] =?UTF-8?q?feat(flashcard):=20t=C3=ACm=20ki=E1=BA=BFm=20h?= =?UTF-8?q?=C3=ACnh=20=E1=BA=A3nh=20khi=20t=E1=BA=A1o=20flashcard=20-=20to?= =?UTF-8?q?do:=20t=C3=ACm=20ki=E1=BA=BFm=20h=C3=ACnh=20=E1=BA=A3nh=20khi?= =?UTF-8?q?=20ch=E1=BB=89nh=20s=E1=BB=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pwhs/quickmem/core/di/RepositoryModule.kt | 7 + .../data/dto/pixabay/PixaBayImageDto.kt | 10 + .../dto/pixabay/SearchImageResponseDto.kt | 12 + .../data/mapper/pixabay/PixaBayImageDto.kt | 14 ++ .../pixabay/SearchImageResponseModel.kt | 17 ++ .../pwhs/quickmem/data/remote/ApiService.kt | 8 + .../repository/PixaBayRepositoryImpl.kt | 31 +++ .../domain/model/pixabay/PixaBayImageModel.kt | 6 + .../model/pixabay/SearchImageResponseModel.kt | 7 + .../domain/repository/PixaBayRepository.kt | 12 + .../flashcard/component/CardSelectImage.kt | 17 +- .../FlashcardSelectImageBottomSheet.kt | 139 +++++++++++ .../flashcard/create/CreateFlashCardScreen.kt | 49 +++- .../create/CreateFlashCardUiAction.kt | 2 + .../create/CreateFlashCardUiState.kt | 4 + .../create/CreateFlashCardViewModel.kt | 68 +++++- .../app/flashcard/edit/EditFlashCardScreen.kt | 4 +- .../flashcard/edit/EditFlashCardUiAction.kt | 1 + .../flashcard/edit/EditFlashCardViewModel.kt | 15 +- .../presentation/app/home/HomeScreen.kt | 9 +- .../SearchStudySetBySubjectScreen.kt | 221 +++++++++--------- .../app/search_result/SearchResultScreen.kt | 2 +- .../presentation/component/ShowImageDialog.kt | 6 +- app/src/main/res/values-vi/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + 25 files changed, 524 insertions(+), 147 deletions(-) create mode 100644 app/src/main/java/com/pwhs/quickmem/data/dto/pixabay/PixaBayImageDto.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/data/dto/pixabay/SearchImageResponseDto.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/data/mapper/pixabay/PixaBayImageDto.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/data/mapper/pixabay/SearchImageResponseModel.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/data/remote/repository/PixaBayRepositoryImpl.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/domain/model/pixabay/PixaBayImageModel.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/domain/model/pixabay/SearchImageResponseModel.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/domain/repository/PixaBayRepository.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/component/FlashcardSelectImageBottomSheet.kt diff --git a/app/src/main/java/com/pwhs/quickmem/core/di/RepositoryModule.kt b/app/src/main/java/com/pwhs/quickmem/core/di/RepositoryModule.kt index 363e31cc..dffb85f7 100644 --- a/app/src/main/java/com/pwhs/quickmem/core/di/RepositoryModule.kt +++ b/app/src/main/java/com/pwhs/quickmem/core/di/RepositoryModule.kt @@ -7,6 +7,7 @@ import com.pwhs.quickmem.data.remote.repository.FirebaseRepositoryImpl import com.pwhs.quickmem.data.remote.repository.FlashCardRepositoryImpl import com.pwhs.quickmem.data.remote.repository.FolderRepositoryImpl import com.pwhs.quickmem.data.remote.repository.NotificationRepositoryImpl +import com.pwhs.quickmem.data.remote.repository.PixaBayRepositoryImpl import com.pwhs.quickmem.data.remote.repository.ReportRepositoryImpl import com.pwhs.quickmem.data.remote.repository.StreakRepositoryImpl import com.pwhs.quickmem.data.remote.repository.StudySetRepositoryImpl @@ -18,6 +19,7 @@ import com.pwhs.quickmem.domain.repository.FirebaseRepository import com.pwhs.quickmem.domain.repository.FlashCardRepository import com.pwhs.quickmem.domain.repository.FolderRepository import com.pwhs.quickmem.domain.repository.NotificationRepository +import com.pwhs.quickmem.domain.repository.PixaBayRepository import com.pwhs.quickmem.domain.repository.ReportRepository import com.pwhs.quickmem.domain.repository.SearchQueryRepository import com.pwhs.quickmem.domain.repository.StreakRepository @@ -91,4 +93,9 @@ abstract class RepositoryModule { abstract fun bindFirebaseRepository( firebaseRepositoryImpl: FirebaseRepositoryImpl ): FirebaseRepository + + @Binds + abstract fun bindPixaBayRepository( + pixaBayRepositoryImpl: PixaBayRepositoryImpl + ): PixaBayRepository } \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/data/dto/pixabay/PixaBayImageDto.kt b/app/src/main/java/com/pwhs/quickmem/data/dto/pixabay/PixaBayImageDto.kt new file mode 100644 index 00000000..e686e7a4 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/data/dto/pixabay/PixaBayImageDto.kt @@ -0,0 +1,10 @@ +package com.pwhs.quickmem.data.dto.pixabay + +import com.google.gson.annotations.SerializedName + +data class PixaBayImageDto( + @SerializedName("id") + val id: Int, + @SerializedName("imageUrl") + val imageUrl: String, +) diff --git a/app/src/main/java/com/pwhs/quickmem/data/dto/pixabay/SearchImageResponseDto.kt b/app/src/main/java/com/pwhs/quickmem/data/dto/pixabay/SearchImageResponseDto.kt new file mode 100644 index 00000000..6d649a54 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/data/dto/pixabay/SearchImageResponseDto.kt @@ -0,0 +1,12 @@ +package com.pwhs.quickmem.data.dto.pixabay + +import com.google.gson.annotations.SerializedName + +data class SearchImageResponseDto( + @SerializedName("total") + val total: Int, + @SerializedName("totalHits") + val totalHits: Int, + @SerializedName("images") + val images: List +) diff --git a/app/src/main/java/com/pwhs/quickmem/data/mapper/pixabay/PixaBayImageDto.kt b/app/src/main/java/com/pwhs/quickmem/data/mapper/pixabay/PixaBayImageDto.kt new file mode 100644 index 00000000..2442df1c --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/data/mapper/pixabay/PixaBayImageDto.kt @@ -0,0 +1,14 @@ +package com.pwhs.quickmem.data.mapper.pixabay + +import com.pwhs.quickmem.data.dto.pixabay.PixaBayImageDto +import com.pwhs.quickmem.domain.model.pixabay.PixaBayImageModel + +fun PixaBayImageDto.toModel() = PixaBayImageModel( + id = id, + imageUrl = imageUrl, +) + +fun PixaBayImageModel.toDto() = PixaBayImageDto( + id = id, + imageUrl = imageUrl, +) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/data/mapper/pixabay/SearchImageResponseModel.kt b/app/src/main/java/com/pwhs/quickmem/data/mapper/pixabay/SearchImageResponseModel.kt new file mode 100644 index 00000000..81ec7316 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/data/mapper/pixabay/SearchImageResponseModel.kt @@ -0,0 +1,17 @@ +package com.pwhs.quickmem.data.mapper.pixabay + +import com.pwhs.quickmem.data.dto.pixabay.SearchImageResponseDto +import com.pwhs.quickmem.domain.model.pixabay.SearchImageResponseModel + + +fun SearchImageResponseModel.toDto() = SearchImageResponseDto( + total = total, + totalHits = totalHits, + images = images.map { it.toDto() } +) + +fun SearchImageResponseDto.toModel() = SearchImageResponseModel( + total = total, + totalHits = totalHits, + images = images.map { it.toModel() } +) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/data/remote/ApiService.kt b/app/src/main/java/com/pwhs/quickmem/data/remote/ApiService.kt index 5a8b0dff..a8d3da59 100644 --- a/app/src/main/java/com/pwhs/quickmem/data/remote/ApiService.kt +++ b/app/src/main/java/com/pwhs/quickmem/data/remote/ApiService.kt @@ -61,6 +61,7 @@ import com.pwhs.quickmem.data.dto.notification.GetNotificationResponseDto import com.pwhs.quickmem.data.dto.notification.MarkNotificationReadRequestDto import com.pwhs.quickmem.data.dto.notification.DeviceTokenRequestDto import com.pwhs.quickmem.data.dto.flashcard.WriteStatusFlashCardDto +import com.pwhs.quickmem.data.dto.pixabay.SearchImageResponseDto import com.pwhs.quickmem.data.dto.report.CreateReportRequestDto import com.pwhs.quickmem.data.dto.streak.GetStreakDto import com.pwhs.quickmem.data.dto.streak.GetTopStreakResponseDto @@ -657,4 +658,11 @@ interface ApiService { @Body createReportRequestDto: CreateReportRequestDto ) + // PixaBay + @GET("pixabay/search") + suspend fun searchImage( + @Header("Authorization") token: String, + @Query("query") query: String, + ): SearchImageResponseDto + } \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/data/remote/repository/PixaBayRepositoryImpl.kt b/app/src/main/java/com/pwhs/quickmem/data/remote/repository/PixaBayRepositoryImpl.kt new file mode 100644 index 00000000..75210117 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/data/remote/repository/PixaBayRepositoryImpl.kt @@ -0,0 +1,31 @@ +package com.pwhs.quickmem.data.remote.repository + +import com.pwhs.quickmem.core.utils.Resources +import com.pwhs.quickmem.data.mapper.pixabay.toModel +import com.pwhs.quickmem.data.remote.ApiService +import com.pwhs.quickmem.domain.model.pixabay.SearchImageResponseModel +import com.pwhs.quickmem.domain.repository.PixaBayRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import timber.log.Timber +import javax.inject.Inject + +class PixaBayRepositoryImpl @Inject constructor( + private val apiService: ApiService +) : PixaBayRepository { + override suspend fun searchImages( + token: String, + query: String + ): Flow> { + return flow { + emit(Resources.Loading()) + try { + val response = apiService.searchImage(token = token, query = query) + emit(Resources.Success(response.toModel())) + } catch (e: Exception) { + Timber.e(e) + emit(Resources.Error(e.message ?: "An error occurred")) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/domain/model/pixabay/PixaBayImageModel.kt b/app/src/main/java/com/pwhs/quickmem/domain/model/pixabay/PixaBayImageModel.kt new file mode 100644 index 00000000..3a067d80 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/domain/model/pixabay/PixaBayImageModel.kt @@ -0,0 +1,6 @@ +package com.pwhs.quickmem.domain.model.pixabay + +data class PixaBayImageModel( + val id: Int, + val imageUrl: String, +) diff --git a/app/src/main/java/com/pwhs/quickmem/domain/model/pixabay/SearchImageResponseModel.kt b/app/src/main/java/com/pwhs/quickmem/domain/model/pixabay/SearchImageResponseModel.kt new file mode 100644 index 00000000..7598a18b --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/domain/model/pixabay/SearchImageResponseModel.kt @@ -0,0 +1,7 @@ +package com.pwhs.quickmem.domain.model.pixabay + +data class SearchImageResponseModel( + val total: Int, + val totalHits: Int, + val images: List +) diff --git a/app/src/main/java/com/pwhs/quickmem/domain/repository/PixaBayRepository.kt b/app/src/main/java/com/pwhs/quickmem/domain/repository/PixaBayRepository.kt new file mode 100644 index 00000000..454b62e2 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/domain/repository/PixaBayRepository.kt @@ -0,0 +1,12 @@ +package com.pwhs.quickmem.domain.repository + +import com.pwhs.quickmem.core.utils.Resources +import com.pwhs.quickmem.domain.model.pixabay.SearchImageResponseModel +import kotlinx.coroutines.flow.Flow + +interface PixaBayRepository { + suspend fun searchImages( + token: String, + query: String + ): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/component/CardSelectImage.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/component/CardSelectImage.kt index cf3ba608..e5614d47 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/component/CardSelectImage.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/component/CardSelectImage.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import coil.compose.AsyncImage -import com.mr0xf00.easycrop.ImagePicker import com.pwhs.quickmem.R @Composable @@ -43,7 +42,7 @@ fun CardSelectImage( definitionImageUri: Uri?, definitionImageUrl: String?, onDeleteImage: () -> Unit, - imagePicker: ImagePicker, + onChooseImage: () -> Unit ) { // State for showing the image viewer dialog var isImageViewerOpen by remember { mutableStateOf(false) } @@ -59,16 +58,16 @@ fun CardSelectImage( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), + onClick = { + if (definitionImageUri != null || !definitionImageUrl.isNullOrEmpty()) { + isImageViewerOpen = true // Open image viewer when clicked + } else { + onChooseImage() + } + } ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier.clickable { - if (definitionImageUri != null || !definitionImageUrl.isNullOrEmpty()) { - isImageViewerOpen = true // Open image viewer when clicked - } else { - imagePicker.pick(mimetype = "image/*") - } - } ) { Column( modifier = Modifier diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/component/FlashcardSelectImageBottomSheet.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/component/FlashcardSelectImageBottomSheet.kt new file mode 100644 index 00000000..a91fd1fc --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/component/FlashcardSelectImageBottomSheet.kt @@ -0,0 +1,139 @@ +package com.pwhs.quickmem.presentation.app.flashcard.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.material.icons.Icons +import androidx.compose.material.icons.filled.Image +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.MaterialTheme.colorScheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +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.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.mr0xf00.easycrop.ImagePicker +import com.pwhs.quickmem.R +import com.pwhs.quickmem.domain.model.pixabay.SearchImageResponseModel +import com.pwhs.quickmem.presentation.app.library.component.SearchTextField + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FlashcardSelectImageBottomSheet( + modifier: Modifier = Modifier, + searchImageBottomSheet: SheetState, + onQueryImageChanged: (String) -> Unit, + onDefinitionImageUrlChanged: (String) -> Unit, + queryImage: String, + isSearchImageLoading: Boolean, + searchImageResponseModel: SearchImageResponseModel?, + imagePicker: ImagePicker, + onDismissRequest: () -> Unit +) { + val context = LocalContext.current + ModalBottomSheet( + modifier = modifier, + sheetState = searchImageBottomSheet, + onDismissRequest = onDismissRequest, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.9f) + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.txt_search_image), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold + ) + ) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + SearchTextField( + searchQuery = queryImage, + onSearch = { + onQueryImageChanged(queryImage) + }, + onSearchQueryChange = { + onQueryImageChanged(it) + }, + placeholder = stringResource(R.string.txt_search_image), + modifier = Modifier + .weight(1f) + .padding(end = 8.dp), + ) + IconButton( + onClick = { + onDismissRequest() + imagePicker.pick(mimetype = "image/*") + } + ) { + Icon( + imageVector = Icons.Default.Image, + contentDescription = stringResource(R.string.txt_import_from_gallery), + modifier = Modifier.size(24.dp), + ) + } + } + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (queryImage.isEmpty()) { + Text(text = stringResource(R.string.txt_please_enter_a_keyword_to_search_images)) + } + } + if (isSearchImageLoading) { + LinearProgressIndicator( + color = colorScheme.primary, + modifier = Modifier.fillMaxWidth() + ) + } + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 100.dp), + modifier = Modifier.fillMaxSize() + ) { + items(searchImageResponseModel?.images?.size ?: 0) { index -> + val image = searchImageResponseModel?.images?.get(index) ?: return@items + AsyncImage( + model = ImageRequest.Builder(context) + .data(image.imageUrl) + .error(R.drawable.ic_image_error) + .build(), + contentDescription = null, + modifier = Modifier + .size(100.dp) + .padding(4.dp) + .clickable { + onDefinitionImageUrlChanged(image.imageUrl) + onDismissRequest() + } + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardScreen.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardScreen.kt index 222cbceb..17e51ad5 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardScreen.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardScreen.kt @@ -42,12 +42,14 @@ import com.mr0xf00.easycrop.rememberImageCropper import com.mr0xf00.easycrop.rememberImagePicker import com.mr0xf00.easycrop.ui.ImageCropperDialog import com.pwhs.quickmem.R +import com.pwhs.quickmem.domain.model.pixabay.SearchImageResponseModel import com.pwhs.quickmem.presentation.ads.BannerAds import com.pwhs.quickmem.presentation.app.flashcard.component.CardSelectImage import com.pwhs.quickmem.presentation.app.flashcard.component.FlashCardTextField import com.pwhs.quickmem.presentation.app.flashcard.component.FlashCardTextFieldContainer import com.pwhs.quickmem.presentation.app.flashcard.component.FlashCardTopAppBar import com.pwhs.quickmem.presentation.app.flashcard.component.FlashcardBottomSheet +import com.pwhs.quickmem.presentation.app.flashcard.component.FlashcardSelectImageBottomSheet import com.pwhs.quickmem.presentation.component.LoadingOverlay import com.pwhs.quickmem.ui.theme.QuickMemTheme import com.pwhs.quickmem.util.ImageCompressor @@ -75,7 +77,6 @@ fun CreateFlashCardScreen( viewModel.uiEvent.collect { event -> when (event) { CreateFlashCardUiEvent.FlashCardSaved -> { - Timber.d("Flashcard saved") Toast.makeText( context, context.getString(R.string.txt_flashcard_saved), Toast.LENGTH_SHORT @@ -83,7 +84,6 @@ fun CreateFlashCardScreen( } CreateFlashCardUiEvent.FlashCardSaveError -> { - Timber.d("Flashcard save error") Toast.makeText( context, context.getString(R.string.txt_flashcard_save_error), Toast.LENGTH_SHORT @@ -152,7 +152,14 @@ fun CreateFlashCardScreen( }, onSaveFlashCardClicked = { viewModel.onEvent(CreateFlashCardUiAction.SaveFlashCard) - } + }, + queryImage = uiState.queryImage, + searchImageResponseModel = uiState.searchImageResponseModel, + onQueryImageChanged = { viewModel.onEvent(CreateFlashCardUiAction.OnQueryImageChanged(it)) }, + onDefinitionImageUrlChanged = { + viewModel.onEvent(CreateFlashCardUiAction.OnDefinitionImageChanged(it)) + }, + isSearchImageLoading = uiState.isSearchImageLoading ) } @@ -182,6 +189,11 @@ fun CreateFlashCard( onDeleteImage: () -> Unit = {}, onNavigationBack: () -> Unit = {}, onSaveFlashCardClicked: () -> Unit = {}, + queryImage: String = "", + searchImageResponseModel: SearchImageResponseModel? = null, + onQueryImageChanged: (String) -> Unit = {}, + onDefinitionImageUrlChanged: (String) -> Unit = {}, + isSearchImageLoading: Boolean = false, ) { val bottomSheetSetting = rememberModalBottomSheetState() @@ -216,6 +228,12 @@ fun CreateFlashCard( ) } + var showSearchImageBottomSheet by remember { + mutableStateOf(false) + } + + val searchImageBottomSheet = rememberModalBottomSheetState() + Scaffold( topBar = { @@ -246,9 +264,11 @@ fun CreateFlashCard( .padding(16.dp), onUploadImage = onUploadImage, definitionImageUri = definitionImageUri, - imagePicker = imagePicker, definitionImageUrl = definitionImageURL, - onDeleteImage = onDeleteImage + onDeleteImage = onDeleteImage, + onChooseImage = { + showSearchImageBottomSheet = true + } ) } item { @@ -388,6 +408,25 @@ fun CreateFlashCard( onShowExplanationClicked = onShowExplanationClicked ) } + + if (showSearchImageBottomSheet) { + FlashcardSelectImageBottomSheet( + modifier = Modifier, + searchImageBottomSheet = searchImageBottomSheet, + onDismissRequest = { + showSearchImageBottomSheet = false + }, + queryImage = queryImage, + searchImageResponseModel = searchImageResponseModel, + onQueryImageChanged = onQueryImageChanged, + isSearchImageLoading = isSearchImageLoading, + onDefinitionImageUrlChanged = { + onDefinitionImageUrlChanged(it) + onDefinitionImageChanged(null) + }, + imagePicker = imagePicker + ) + } } } diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardUiAction.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardUiAction.kt index 23a9045d..ca3ca306 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardUiAction.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardUiAction.kt @@ -18,4 +18,6 @@ sealed class CreateFlashCardUiAction { data class ShowExplanationClicked(val showExplanation: Boolean) : CreateFlashCardUiAction() data class UploadImage(val imageUri: Uri) : CreateFlashCardUiAction() data class RemoveImage(val imageURL: String) : CreateFlashCardUiAction() + data class OnQueryImageChanged(val query: String) : CreateFlashCardUiAction() + data class OnDefinitionImageChanged(val definitionImageUrl: String) : CreateFlashCardUiAction() } \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardUiState.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardUiState.kt index f518621b..29cbfa25 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardUiState.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardUiState.kt @@ -2,6 +2,7 @@ package com.pwhs.quickmem.presentation.app.flashcard.create import android.net.Uri import com.pwhs.quickmem.domain.model.flashcard.CreateFlashCardModel +import com.pwhs.quickmem.domain.model.pixabay.SearchImageResponseModel data class CreateFlashCardUiState( val studySetId: String = "", @@ -15,6 +16,9 @@ data class CreateFlashCardUiState( val showExplanation: Boolean = false, val isCreated: Boolean = false, val isLoading: Boolean = false, + val queryImage: String = "", + val searchImageResponseModel: SearchImageResponseModel? = null, + val isSearchImageLoading: Boolean = false, ) fun CreateFlashCardUiState.toCreateFlashCardModel(): CreateFlashCardModel { diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardViewModel.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardViewModel.kt index cd4f59a1..86eee576 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardViewModel.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/create/CreateFlashCardViewModel.kt @@ -7,8 +7,10 @@ import androidx.lifecycle.viewModelScope import com.pwhs.quickmem.core.datastore.TokenManager import com.pwhs.quickmem.core.utils.Resources import com.pwhs.quickmem.domain.repository.FlashCardRepository +import com.pwhs.quickmem.domain.repository.PixaBayRepository import com.pwhs.quickmem.domain.repository.UploadImageRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -24,6 +26,7 @@ class CreateFlashCardViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val flashCardRepository: FlashCardRepository, private val uploadImageRepository: UploadImageRepository, + private val pixaBayRepository: PixaBayRepository, private val tokenManager: TokenManager, application: Application ) : AndroidViewModel(application) { @@ -33,6 +36,8 @@ class CreateFlashCardViewModel @Inject constructor( private val _uiEvent = Channel() val uiEvent = _uiEvent.receiveAsFlow() + private var job: Job? = null + init { val studySetId: String = savedStateHandle["studySetId"] ?: "" _uiState.update { it.copy(studySetId = studySetId) } @@ -45,7 +50,11 @@ class CreateFlashCardViewModel @Inject constructor( } is CreateFlashCardUiAction.FlashCardDefinitionImageChanged -> { - _uiState.update { it.copy(definitionImageUri = event.definitionImageUri) } + _uiState.update { + it.copy( + definitionImageUri = event.definitionImageUri, + ) + } } is CreateFlashCardUiAction.FlashCardExplanationChanged -> { @@ -139,6 +148,63 @@ class CreateFlashCardViewModel @Inject constructor( } } } + + is CreateFlashCardUiAction.OnQueryImageChanged -> { + _uiState.update { + it.copy( + queryImage = event.query, + isSearchImageLoading = true + ) + } + if (event.query.length < 3) { + return + } + + job?.cancel() + job = viewModelScope.launch { + pixaBayRepository.searchImages( + token = tokenManager.accessToken.firstOrNull() ?: "", + query = event.query + ).collect { resource -> + when (resource) { + is Resources.Success -> { + _uiState.update { + it.copy( + searchImageResponseModel = resource.data, + isSearchImageLoading = false + ) + } + } + + is Resources.Error -> { + Timber.e("Error: ${resource.message}") + _uiState.update { + it.copy( + searchImageResponseModel = null, + isSearchImageLoading = false + ) + } + } + + is Resources.Loading -> { + _uiState.update { + it.copy( + isSearchImageLoading = true + ) + } + } + } + } + } + } + + is CreateFlashCardUiAction.OnDefinitionImageChanged -> { + _uiState.update { + it.copy( + definitionImageURL = event.definitionImageUrl, + ) + } + } } } diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardScreen.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardScreen.kt index 239d016c..f2a34946 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardScreen.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardScreen.kt @@ -271,9 +271,9 @@ fun CreateFlashCard( .padding(16.dp), onUploadImage = onUploadImage, definitionImageUri = definitionImageUri, - imagePicker = imagePicker, definitionImageUrl = definitionImageURL, - onDeleteImage = onDeleteImage + onDeleteImage = onDeleteImage, + onChooseImage = {} ) } item { diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardUiAction.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardUiAction.kt index dd5152f6..5f45bc5a 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardUiAction.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardUiAction.kt @@ -18,4 +18,5 @@ sealed class EditFlashCardUiAction { data class ShowExplanationClicked(val showExplanation: Boolean) : EditFlashCardUiAction() data class UploadImage(val imageUri: Uri) : EditFlashCardUiAction() data class RemoveImage(val imageURL: String) : EditFlashCardUiAction() + data class OnQueryImageChanged(val query: String) : EditFlashCardUiAction() } \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardViewModel.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardViewModel.kt index d9e7fc6a..2d0941c0 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardViewModel.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/flashcard/edit/EditFlashCardViewModel.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -63,12 +62,10 @@ class EditFlashCardViewModel @Inject constructor( } is EditFlashCardUiAction.FlashCardExplanationChanged -> { - Timber.d("Explanation: ${event.explanation}") _uiState.update { it.copy(explanation = event.explanation) } } is EditFlashCardUiAction.FlashCardHintChanged -> { - Timber.d("Hint: ${event.hint}") _uiState.update { it.copy(hint = event.hint) } } @@ -102,7 +99,6 @@ class EditFlashCardViewModel @Inject constructor( .collect { resource -> when (resource) { is Resources.Success -> { - Timber.d("Success: ${resource.data}") _uiState.update { it.copy( definitionImageURL = resource.data!!.url, @@ -112,7 +108,6 @@ class EditFlashCardViewModel @Inject constructor( } is Resources.Error -> { - Timber.e("Error: ${resource.message}") _uiState.update { it.copy(isLoading = false) } } @@ -133,7 +128,6 @@ class EditFlashCardViewModel @Inject constructor( .collect { resource -> when (resource) { is Resources.Success -> { - Timber.d("Success: ${resource.data}") _uiState.update { it.copy( definitionImageURL = "", @@ -144,7 +138,6 @@ class EditFlashCardViewModel @Inject constructor( } is Resources.Error -> { - Timber.e("Error: ${resource.message}") _uiState.update { it.copy(isLoading = false) } } @@ -157,15 +150,16 @@ class EditFlashCardViewModel @Inject constructor( } } } + + is EditFlashCardUiAction.OnQueryImageChanged -> { + } } } private fun saveFlashCard() { - Timber.d("Saving flashcard: url: ${_uiState.value.definitionImageURL}") viewModelScope.launch { val token = tokenManager.accessToken.firstOrNull() ?: "" val editFlashCardModel = _uiState.value.toEditFlashCardModel() - Timber.d("EditFlashCardModel: $editFlashCardModel") flashCardRepository.updateFlashCard( token, _uiState.value.flashcardId, @@ -173,17 +167,14 @@ class EditFlashCardViewModel @Inject constructor( ).collect { resource -> when (resource) { is Resources.Error -> { - Timber.e("Error: ${resource.message}") _uiState.update { it.copy(isLoading = false) } } is Resources.Loading -> { - Timber.d("Loading") _uiState.update { it.copy(isLoading = true) } } is Resources.Success -> { - Timber.d("FlashCard saved: ${resource.data}") _uiState.update { it.copy( term = "", diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/home/HomeScreen.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/home/HomeScreen.kt index e4044110..020c96fe 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/home/HomeScreen.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/home/HomeScreen.kt @@ -10,7 +10,6 @@ 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -589,17 +588,14 @@ private fun Home( sheetState = streakBottomSheet, ) { Column( - modifier = Modifier - .padding(16.dp) - .fillMaxHeight(0.7f), horizontalAlignment = Alignment.CenterHorizontally ) { LottieAnimation( composition = composition, progress = { progress }, modifier = Modifier - .width(150.dp) - .height(150.dp) + .width(100.dp) + .height(100.dp) ) Text( text = streakCount.toString(), @@ -622,7 +618,6 @@ private fun Home( ) Text( text = stringResource(R.string.txt_practice_every_day), - modifier = Modifier.padding(top = 16.dp) ) StreakCalendar( currentDate = currentDate, diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/home/search_by_subject/SearchStudySetBySubjectScreen.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/home/search_by_subject/SearchStudySetBySubjectScreen.kt index ea5e2987..5e6c4e74 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/home/search_by_subject/SearchStudySetBySubjectScreen.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/home/search_by_subject/SearchStudySetBySubjectScreen.kt @@ -5,8 +5,10 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes 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.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -137,136 +139,139 @@ fun SearchStudySetBySubject( ) } ) { innerPadding -> - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(innerPadding), - horizontalAlignment = Alignment.CenterHorizontally + Box( + modifier = Modifier.padding(innerPadding), ) { - item { - SearchTextField( - searchQuery = searchQuery, - onSearchQueryChange = { searchQuery = it }, - placeholder = stringResource(R.string.txt_search_study_sets), - modifier = Modifier.padding(horizontal = 16.dp) - ) - } - item { - BannerAds( - modifier = Modifier.padding(8.dp) - ) - } - items(studySets?.itemCount ?: 0, key = { it }) { index -> - val studySet = studySets?.get(index) - if (studySet != null && studySet.title.contains( - searchQuery, - ignoreCase = true - ) - ) { - StudySetItem( - modifier = Modifier.padding(horizontal = 16.dp), - studySet = studySet, - onStudySetClick = { onStudySetClick(studySet) } + LazyColumn( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + SearchTextField( + searchQuery = searchQuery, + onSearchQueryChange = { searchQuery = it }, + placeholder = stringResource(R.string.txt_search_study_sets), + modifier = Modifier.padding(horizontal = 16.dp) ) } - } - item { - if (studySets?.itemCount == 0 && !isLoading) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(innerPadding) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally + items(studySets?.itemCount ?: 0, key = { it }) { index -> + val studySet = studySets?.get(index) + if (studySet != null && studySet.title.contains( + searchQuery, + ignoreCase = true + ) ) { - Text( - text = stringResource(R.string.txt_no_study_sets_found), - style = typography.bodyLarge, - textAlign = TextAlign.Center + StudySetItem( + modifier = Modifier.padding(horizontal = 16.dp), + studySet = studySet, + onStudySetClick = { onStudySetClick(studySet) } ) } } - } - item { - studySets?.apply { - when { - loadState.refresh is LoadState.Loading -> { - CircularProgressIndicator( - modifier = Modifier - .size(36.dp) - .padding(innerPadding), - color = colorScheme.primary + item { + if (studySets?.itemCount == 0 && !isLoading) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(innerPadding) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.txt_no_study_sets_found), + style = typography.bodyLarge, + textAlign = TextAlign.Center ) } - - loadState.refresh is LoadState.Error -> { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(innerPadding) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - Image( - imageVector = Icons.Default.Error, - contentDescription = stringResource(R.string.txt_error), - ) - Text( - text = stringResource(R.string.txt_error_occurred), - style = typography.titleLarge, - textAlign = TextAlign.Center + } + } + item { + studySets?.apply { + when { + loadState.refresh is LoadState.Loading -> { + CircularProgressIndicator( + modifier = Modifier + .size(36.dp) + .padding(innerPadding), + color = colorScheme.primary ) - Button( - onClick = onStudySetRefresh, - modifier = Modifier.padding(top = 16.dp) + } + + loadState.refresh is LoadState.Error -> { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(innerPadding) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) ) { - Text(text = stringResource(R.string.txt_retry)) + Image( + imageVector = Icons.Default.Error, + contentDescription = stringResource(R.string.txt_error), + ) + Text( + text = stringResource(R.string.txt_error_occurred), + style = typography.titleLarge, + textAlign = TextAlign.Center + ) + Button( + onClick = onStudySetRefresh, + modifier = Modifier.padding(top = 16.dp) + ) { + Text(text = stringResource(R.string.txt_retry)) + } } } - } - - loadState.append is LoadState.Loading -> { - CircularProgressIndicator( - modifier = Modifier - .size(36.dp) - .padding(top = 16.dp), - color = colorScheme.primary - ) - } - loadState.append is LoadState.Error -> { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(innerPadding) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - Image( - imageVector = Icons.Default.Error, - contentDescription = stringResource(R.string.txt_error), + loadState.append is LoadState.Loading -> { + CircularProgressIndicator( + modifier = Modifier + .size(36.dp) + .padding(top = 16.dp), + color = colorScheme.primary ) - Text( - text = stringResource(R.string.txt_error_occurred), - style = typography.titleLarge, - textAlign = TextAlign.Center - ) - Button( - onClick = { retry() }, - modifier = Modifier.padding(top = 16.dp) + } + + loadState.append is LoadState.Error -> { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(innerPadding) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) ) { - Text(text = stringResource(R.string.txt_retry)) + Image( + imageVector = Icons.Default.Error, + contentDescription = stringResource(R.string.txt_error), + ) + Text( + text = stringResource(R.string.txt_error_occurred), + style = typography.titleLarge, + textAlign = TextAlign.Center + ) + Button( + onClick = { retry() }, + modifier = Modifier.padding(top = 16.dp) + ) { + Text(text = stringResource(R.string.txt_retry)) + } } } } } } + item { + Spacer(modifier = Modifier.padding(60.dp)) + } } - item { - Spacer(modifier = Modifier.padding(60.dp)) - } + BannerAds( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/search_result/SearchResultScreen.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/search_result/SearchResultScreen.kt index 1058913c..628fd6ca 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/search_result/SearchResultScreen.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/search_result/SearchResultScreen.kt @@ -183,9 +183,9 @@ fun SearchResultScreen( @Composable fun SearchResult( + modifier: Modifier = Modifier, isLoading: Boolean = false, query: String = "", - modifier: Modifier = Modifier, tabIndex: Int, onTabSelected: (Int) -> Unit, colorModel: ColorModel? = ColorModel.defaultColors.first(), diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/component/ShowImageDialog.kt b/app/src/main/java/com/pwhs/quickmem/presentation/component/ShowImageDialog.kt index 43a57160..08ba08f9 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/component/ShowImageDialog.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/component/ShowImageDialog.kt @@ -18,16 +18,18 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import coil.compose.AsyncImage +import com.pwhs.quickmem.R @Composable fun ShowImageDialog( modifier: Modifier = Modifier, definitionImageUri: String, onDismissRequest: () -> Unit, - title: String = "Close", + title: String = stringResource(id = R.string.txt_close), color: Color = MaterialTheme.colorScheme.primary ) { Dialog(onDismissRequest = { @@ -48,7 +50,7 @@ fun ShowImageDialog( Column(horizontalAlignment = Alignment.CenterHorizontally) { AsyncImage( model = definitionImageUri, - contentDescription = "Full Image", + contentDescription = stringResource(id = R.string.image_description), modifier = Modifier .fillMaxWidth() .padding(16.dp), diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index d9b8f179..e96c9752 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -618,4 +618,9 @@ Tải lại Xu Tạo (-%1$s) + Hình ảnh + Thử \"Mèo\" + Vui lòng nhập từ khóa để tìm kiếm hình ảnh + Nhập từ thư viện + Tìm kiếm hình ảnh \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f998068d..eeca2994 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -621,4 +621,9 @@ Refresh Coin Create (-%1$s) + Image + Try \"Cats\" + Please enter a keyword to search images + Import from gallery + Search image \ No newline at end of file