From 8fcab621e9dfa493237de58ff8a9b5b9a781144a Mon Sep 17 00:00:00 2001 From: "Wei.He" Date: Fri, 1 Dec 2023 14:15:30 +0800 Subject: [PATCH 1/2] Initial photolibray module --- app/build.gradle.kts | 2 +- .../com/wei/picquest/navigation/PqNavHost.kt | 4 + .../java/com/wei/picquest/ui/PqAppState.kt | 11 +- .../picquest/feature/home/home/HomeScreen.kt | 5 +- feature/photolibrary/.gitignore | 1 + feature/photolibrary/build.gradle.kts | 12 ++ .../photolibrary/src/main/AndroidManifest.xml | 4 + .../photolibrary/PhotoLibraryScreen.kt | 112 ++++++++++++++++++ .../navigation/PhotoLibraryNavigation.kt | 23 ++++ settings.gradle.kts | 1 + 10 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 feature/photolibrary/.gitignore create mode 100644 feature/photolibrary/build.gradle.kts create mode 100644 feature/photolibrary/src/main/AndroidManifest.xml create mode 100644 feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryScreen.kt create mode 100644 feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/navigation/PhotoLibraryNavigation.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f921f5..72fd56f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,9 +67,9 @@ android { dependencies { implementation(project(":feature:home")) + implementation(project(":feature:photolibrary")) // TODO Wei -// implementation(project(":feature:login")) // implementation(project(":feature:contactme")) implementation(project(":core:designsystem")) diff --git a/app/src/main/java/com/wei/picquest/navigation/PqNavHost.kt b/app/src/main/java/com/wei/picquest/navigation/PqNavHost.kt index d7db6db..4062567 100644 --- a/app/src/main/java/com/wei/picquest/navigation/PqNavHost.kt +++ b/app/src/main/java/com/wei/picquest/navigation/PqNavHost.kt @@ -7,6 +7,7 @@ import androidx.window.layout.DisplayFeature import com.wei.picquest.core.designsystem.ui.DeviceOrientation import com.wei.picquest.feature.home.home.navigation.homeGraph import com.wei.picquest.feature.home.home.navigation.homeRoute +import com.wei.picquest.feature.photolibrary.photolibrary.navigation.photoLibraryGraph import com.wei.picquest.ui.PqAppState /** @@ -36,5 +37,8 @@ fun PqNavHost( homeGraph( navController = navController, ) + photoLibraryGraph( + navController = navController, + ) } } diff --git a/app/src/main/java/com/wei/picquest/ui/PqAppState.kt b/app/src/main/java/com/wei/picquest/ui/PqAppState.kt index 9ce1550..15698f1 100644 --- a/app/src/main/java/com/wei/picquest/ui/PqAppState.kt +++ b/app/src/main/java/com/wei/picquest/ui/PqAppState.kt @@ -27,6 +27,8 @@ import com.wei.picquest.core.designsystem.ui.isBookPosture import com.wei.picquest.core.designsystem.ui.isSeparating import com.wei.picquest.feature.home.home.navigation.homeRoute import com.wei.picquest.feature.home.home.navigation.navigateToHome +import com.wei.picquest.feature.photolibrary.photolibrary.navigation.navigateToPhotoLibrary +import com.wei.picquest.feature.photolibrary.photolibrary.navigation.photoLibraryRoute import com.wei.picquest.navigation.TopLevelDestination import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -149,6 +151,7 @@ class PqAppState( val currentTopLevelDestination: TopLevelDestination? @Composable get() = when (currentDestination?.route) { homeRoute -> TopLevelDestination.HOME + photoLibraryRoute -> TopLevelDestination.PHOTO_LIBRARY else -> null } @@ -196,10 +199,10 @@ class PqAppState( topLevelNavOptions, ) -// TopLevelDestination.PHOTO_LIBRARY -> navController.navigateToPhotoLibrary( -// topLevelNavOptions, -// ) -// + TopLevelDestination.PHOTO_LIBRARY -> navController.navigateToPhotoLibrary( + topLevelNavOptions, + ) + // TopLevelDestination.CONTACT_ME -> navController.navigateToContactMe( // topLevelNavOptions, // ) diff --git a/feature/home/src/main/java/com/wei/picquest/feature/home/home/HomeScreen.kt b/feature/home/src/main/java/com/wei/picquest/feature/home/home/HomeScreen.kt index 6e49664..73ea884 100644 --- a/feature/home/src/main/java/com/wei/picquest/feature/home/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/wei/picquest/feature/home/home/HomeScreen.kt @@ -62,7 +62,6 @@ internal fun HomeRoute( internal fun HomeScreen( withTopSpacer: Boolean = true, withBottomSpacer: Boolean = true, - isPreview: Boolean = false, ) { val showPopup = remember { mutableStateOf(false) } @@ -108,8 +107,6 @@ internal fun HomeScreen( @Composable fun HomeScreenPreview() { PqTheme { - HomeScreen( - isPreview = true, - ) + HomeScreen() } } diff --git a/feature/photolibrary/.gitignore b/feature/photolibrary/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/feature/photolibrary/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/photolibrary/build.gradle.kts b/feature/photolibrary/build.gradle.kts new file mode 100644 index 0000000..85b8308 --- /dev/null +++ b/feature/photolibrary/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.pq.android.feature) + alias(libs.plugins.pq.android.library.compose) + alias(libs.plugins.pq.android.hilt) +} + +android { + namespace = "com.wei.picquest.feature.photolibrary" +} + +dependencies { +} \ No newline at end of file diff --git a/feature/photolibrary/src/main/AndroidManifest.xml b/feature/photolibrary/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/feature/photolibrary/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryScreen.kt b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryScreen.kt new file mode 100644 index 0000000..ff7a691 --- /dev/null +++ b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryScreen.kt @@ -0,0 +1,112 @@ +package com.wei.picquest.feature.photolibrary.photolibrary + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsTopHeight +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.navigation.NavController +import com.wei.picquest.core.designsystem.component.FunctionalityNotAvailablePopup +import com.wei.picquest.core.designsystem.component.ThemePreviews +import com.wei.picquest.core.designsystem.theme.PqTheme + +/** + * + * UI 事件決策樹 + * 下圖顯示了一個決策樹,用於查找處理特定事件用例的最佳方法。 + * + * ┌───────┐ + * │ Start │ + * └───┬───┘ + * ↓ + * ┌───────────────────────────────────┐ + * │ Where is event originated? │ + * └──────┬─────────────────────┬──────┘ + * ↓ ↓ + * UI ViewModel + * │ │ + * ┌─────────────────────────┐ ┌───────────────┐ + * │ When the event requires │ │ Update the UI │ + * │ ... │ │ State │ + * └─┬─────────────────────┬─┘ └───────────────┘ + * ↓ ↓ + * Business logic UI behavior logic + * │ │ + * ┌─────────────────────────────────┐ ┌──────────────────────────────────────┐ + * │ Delegate the business logic to │ │ Modify the UI element state in the │ + * │ the ViewModel │ │ UI directly │ + * └─────────────────────────────────┘ └──────────────────────────────────────┘ + * + * + */ +@Composable +internal fun PhotoLibraryRoute( + navController: NavController, +) { + PhotoLibraryScreen() +} + +@Composable +internal fun PhotoLibraryScreen( + withTopSpacer: Boolean = true, + withBottomSpacer: Boolean = true, +) { + val showPopup = remember { mutableStateOf(false) } + + if (showPopup.value) { + FunctionalityNotAvailablePopup( + onDismiss = { + showPopup.value = false + }, + ) + } + + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (withTopSpacer) { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) + } + + Column { + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "Photo Library Screen not available \uD83D\uDE48", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier + .semantics { contentDescription = "" }, + ) + Spacer(modifier = Modifier.weight(1f)) + } + + if (withBottomSpacer) { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + } + } + } +} + +@ThemePreviews +@Composable +fun HomeScreenPreview() { + PqTheme { + PhotoLibraryScreen() + } +} diff --git a/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/navigation/PhotoLibraryNavigation.kt b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/navigation/PhotoLibraryNavigation.kt new file mode 100644 index 0000000..910d257 --- /dev/null +++ b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/navigation/PhotoLibraryNavigation.kt @@ -0,0 +1,23 @@ +package com.wei.picquest.feature.photolibrary.photolibrary.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.wei.picquest.feature.photolibrary.photolibrary.PhotoLibraryRoute + +const val photoLibraryRoute = "photo_library_route" + +fun NavController.navigateToPhotoLibrary(navOptions: NavOptions? = null) { + this.navigate(photoLibraryRoute, navOptions) +} + +fun NavGraphBuilder.photoLibraryGraph( + navController: NavController, +) { + composable(route = photoLibraryRoute) { + PhotoLibraryRoute( + navController = navController, + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index baf125c..d130eb7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,3 +29,4 @@ include(":core:network") include(":ui-test-hilt-manifest") include(":feature:home") +include(":feature:photolibrary") From 3b0fc10f43617f8b719e98bd352184c313925928 Mon Sep 17 00:00:00 2001 From: WeiHe Date: Sat, 2 Dec 2023 05:12:15 +0800 Subject: [PATCH 2/2] feat: Implement functionality of the photolibrary module --- .../kotlin/AndroidLibraryConventionPlugin.kt | 5 + .../picquest/core/data/model/ImageDetail.kt | 53 +++ .../picquest/core/data/model/SearchImages.kt | 63 ---- .../DefaultSearchImagesRepository.kt | 42 +-- .../data/repository/SearchImagesRepository.kt | 5 +- .../core/designsystem/icon/PqIcons.kt | 4 + .../core/network/PqNetworkDataSource.kt | 2 +- .../pagingsource/PixabayPagingSource.kt | 37 ++ .../network/retrofit/RetrofitPqNetwork.kt | 6 +- .../photolibrary/PhotoLibraryScreen.kt | 334 ++++++++++++++++-- .../photolibrary/PhotoLibraryViewModel.kt | 57 +++ .../photolibrary/PhotoLibraryViewState.kt | 20 ++ .../component/LayoutSwitchWarningDialog.kt | 38 ++ .../main/res/drawable/placeholder_image.png | Bin 0 -> 8806 bytes .../src/main/res/values-zh-rTW/strings.xml | 7 + .../src/main/res/values/strings.xml | 7 + gradle/libs.versions.toml | 7 + 17 files changed, 568 insertions(+), 119 deletions(-) create mode 100644 core/data/src/main/java/com/wei/picquest/core/data/model/ImageDetail.kt delete mode 100644 core/data/src/main/java/com/wei/picquest/core/data/model/SearchImages.kt create mode 100644 core/network/src/main/java/com/wei/picquest/core/network/pagingsource/PixabayPagingSource.kt create mode 100644 feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryViewModel.kt create mode 100644 feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryViewState.kt create mode 100644 feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/component/LayoutSwitchWarningDialog.kt create mode 100644 feature/photolibrary/src/main/res/drawable/placeholder_image.png create mode 100644 feature/photolibrary/src/main/res/values-zh-rTW/strings.xml create mode 100644 feature/photolibrary/src/main/res/values/strings.xml diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 9cf2027..d230e7a 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -36,6 +36,11 @@ class AndroidLibraryConventionPlugin : Plugin { add("androidTestImplementation", kotlin("test")) add("androidTestImplementation", project(":core:testing")) + // Paging 3 + add("implementation", libs.findLibrary("paging-runtime").get()) + add("implementation", libs.findLibrary("paging-compose").get()) + add("testImplementation", libs.findLibrary("paging-common").get()) + // Timber add("implementation", libs.findLibrary("timber").get()) } diff --git a/core/data/src/main/java/com/wei/picquest/core/data/model/ImageDetail.kt b/core/data/src/main/java/com/wei/picquest/core/data/model/ImageDetail.kt new file mode 100644 index 0000000..c188c9c --- /dev/null +++ b/core/data/src/main/java/com/wei/picquest/core/data/model/ImageDetail.kt @@ -0,0 +1,53 @@ +package com.wei.picquest.core.data.model + +import com.wei.picquest.core.network.model.NetworkImageDetail + +data class ImageDetail( + val id: Int, + val pageURL: String, + val type: String, + val tags: String, + val previewURL: String, + val previewWidth: Int, + val previewHeight: Int, + val webformatURL: String, + val webformatWidth: Int, + val webformatHeight: Int, + val largeImageURL: String, + val imageWidth: Int, + val imageHeight: Int, + val imageSize: Long, + val views: Int, + val downloads: Int, + val likes: Int, + val comments: Int, + val userId: Int, + val user: String, + val userImageURL: String, +) { + val aspectRatio get() = imageWidth.toFloat() / imageHeight.toFloat() +} + +fun NetworkImageDetail.asExternalModel() = ImageDetail( + id = this.id, + pageURL = this.pageURL, + type = this.type, + tags = this.tags, + previewURL = this.previewURL, + previewWidth = this.previewWidth, + previewHeight = this.previewHeight, + webformatURL = this.webformatURL, + webformatWidth = this.webformatWidth, + webformatHeight = this.webformatHeight, + largeImageURL = this.largeImageURL, + imageWidth = this.imageWidth, + imageHeight = this.imageHeight, + imageSize = this.imageSize, + views = this.views, + downloads = this.downloads, + likes = this.likes, + comments = this.comments, + userId = this.userId, + user = this.user, + userImageURL = this.userImageURL, +) diff --git a/core/data/src/main/java/com/wei/picquest/core/data/model/SearchImages.kt b/core/data/src/main/java/com/wei/picquest/core/data/model/SearchImages.kt deleted file mode 100644 index df45fee..0000000 --- a/core/data/src/main/java/com/wei/picquest/core/data/model/SearchImages.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.wei.picquest.core.data.model - -import com.wei.picquest.core.network.model.NetworkSearchImages - -data class SearchImages( - val total: Int, - val totalHits: Int, - val images: List, -) - -data class ImageDetail( - val id: Int, - val pageURL: String, - val type: String, - val tags: String, - val previewURL: String, - val previewWidth: Int, - val previewHeight: Int, - val webformatURL: String, - val webformatWidth: Int, - val webformatHeight: Int, - val largeImageURL: String, - val imageWidth: Int, - val imageHeight: Int, - val imageSize: Long, - val views: Int, - val downloads: Int, - val likes: Int, - val comments: Int, - val userId: Int, - val user: String, - val userImageURL: String, -) - -fun NetworkSearchImages.asExternalModel() = SearchImages( - total = this.total, - totalHits = this.totalHits, - images = this.hits.map { networkImageDetail -> - ImageDetail( - id = networkImageDetail.id, - pageURL = networkImageDetail.pageURL, - type = networkImageDetail.type, - tags = networkImageDetail.tags, - previewURL = networkImageDetail.previewURL, - previewWidth = networkImageDetail.previewWidth, - previewHeight = networkImageDetail.previewHeight, - webformatURL = networkImageDetail.webformatURL, - webformatWidth = networkImageDetail.webformatWidth, - webformatHeight = networkImageDetail.webformatHeight, - largeImageURL = networkImageDetail.largeImageURL, - imageWidth = networkImageDetail.imageWidth, - imageHeight = networkImageDetail.imageHeight, - imageSize = networkImageDetail.imageSize, - views = networkImageDetail.views, - downloads = networkImageDetail.downloads, - likes = networkImageDetail.likes, - comments = networkImageDetail.comments, - userId = networkImageDetail.userId, - user = networkImageDetail.user, - userImageURL = networkImageDetail.userImageURL, - ) - }, -) diff --git a/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchImagesRepository.kt b/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchImagesRepository.kt index f20cfdf..3f922cd 100644 --- a/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchImagesRepository.kt +++ b/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchImagesRepository.kt @@ -1,35 +1,31 @@ package com.wei.picquest.core.data.repository -import com.wei.picquest.core.network.Dispatcher -import com.wei.picquest.core.network.PqDispatchers +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.map +import com.wei.picquest.core.data.model.ImageDetail +import com.wei.picquest.core.data.model.asExternalModel import com.wei.picquest.core.network.PqNetworkDataSource -import com.wei.picquest.core.network.model.NetworkSearchImages -import kotlinx.coroutines.CoroutineDispatcher +import com.wei.picquest.core.network.pagingsource.PixabayPagingSource import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.map import javax.inject.Inject -/** - * Implementation of the [SearchImagesRepository]. - * @param ioDispatcher 用於執行 IO 相關操作的 CoroutineDispatcher。 - * @param network 數據源的網路接口。 - */ class DefaultSearchImagesRepository @Inject constructor( - @Dispatcher(PqDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, - private val network: PqNetworkDataSource, + private val pqNetworkDataSource: PqNetworkDataSource, ) : SearchImagesRepository { - /** - * @param query。A URL encoded search term. If omitted, all images are returned. This value may not exceed 100 characters. - * Example: "yellow+flower" - * @return 一個 Flow,內容為 Search Images 的數據。 - */ - override suspend fun getSearchImages( - query: String, - ): Flow = withContext(ioDispatcher) { - flow { - emit(network.searchImages(query)) + override suspend fun getSearchImages(query: String): Flow> { + return Pager( + config = PagingConfig( + pageSize = 20, + prefetchDistance = 5, + enablePlaceholders = false, + ), + pagingSourceFactory = { PixabayPagingSource(pqNetworkDataSource, query) }, + ).flow.map { pagingData -> + pagingData.map { it.asExternalModel() } } } } diff --git a/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchImagesRepository.kt b/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchImagesRepository.kt index 5e1a293..436a903 100644 --- a/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchImagesRepository.kt +++ b/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchImagesRepository.kt @@ -1,9 +1,10 @@ package com.wei.picquest.core.data.repository -import com.wei.picquest.core.network.model.NetworkSearchImages +import androidx.paging.PagingData +import com.wei.picquest.core.data.model.ImageDetail import kotlinx.coroutines.flow.Flow interface SearchImagesRepository { - suspend fun getSearchImages(query: String): Flow + suspend fun getSearchImages(query: String): Flow> } diff --git a/core/designsystem/src/main/java/com/wei/picquest/core/designsystem/icon/PqIcons.kt b/core/designsystem/src/main/java/com/wei/picquest/core/designsystem/icon/PqIcons.kt index 624842e..3ef3b6f 100644 --- a/core/designsystem/src/main/java/com/wei/picquest/core/designsystem/icon/PqIcons.kt +++ b/core/designsystem/src/main/java/com/wei/picquest/core/designsystem/icon/PqIcons.kt @@ -14,8 +14,10 @@ import androidx.compose.material.icons.rounded.ArrowForward import androidx.compose.material.icons.rounded.ArrowForwardIos import androidx.compose.material.icons.rounded.CalendarMonth import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.GridView import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.List import androidx.compose.material.icons.rounded.Menu import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.Phone @@ -55,4 +57,6 @@ object PqIcons { val Person = Icons.Rounded.Person val Menu = Icons.Rounded.Menu val Add = Icons.Rounded.Add + val ListView = Icons.Rounded.List + val GridView = Icons.Rounded.GridView } diff --git a/core/network/src/main/java/com/wei/picquest/core/network/PqNetworkDataSource.kt b/core/network/src/main/java/com/wei/picquest/core/network/PqNetworkDataSource.kt index 16dc2ff..2ddc85c 100644 --- a/core/network/src/main/java/com/wei/picquest/core/network/PqNetworkDataSource.kt +++ b/core/network/src/main/java/com/wei/picquest/core/network/PqNetworkDataSource.kt @@ -7,5 +7,5 @@ import com.wei.picquest.core.network.model.NetworkSearchImages */ interface PqNetworkDataSource { - suspend fun searchImages(query: String): NetworkSearchImages + suspend fun searchImages(query: String, page: Int, perPage: Int): NetworkSearchImages } diff --git a/core/network/src/main/java/com/wei/picquest/core/network/pagingsource/PixabayPagingSource.kt b/core/network/src/main/java/com/wei/picquest/core/network/pagingsource/PixabayPagingSource.kt new file mode 100644 index 0000000..a77c907 --- /dev/null +++ b/core/network/src/main/java/com/wei/picquest/core/network/pagingsource/PixabayPagingSource.kt @@ -0,0 +1,37 @@ +package com.wei.picquest.core.network.pagingsource + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.wei.picquest.core.network.PqNetworkDataSource +import com.wei.picquest.core.network.model.NetworkImageDetail + +class PixabayPagingSource( + private val pqNetworkDataSource: PqNetworkDataSource, + private val query: String, +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + try { + val currentPage = params.key ?: 1 + val response = pqNetworkDataSource.searchImages( + query = query, + page = currentPage, + perPage = 20, + ) + + val endOfPaginationReached = response.hits.isEmpty() + + return LoadResult.Page( + data = response.hits, + prevKey = if (currentPage == 1) null else currentPage - 1, + nextKey = if (endOfPaginationReached) null else currentPage + 1, + ) + } catch (exception: Exception) { + return LoadResult.Error(exception) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition + } +} diff --git a/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt b/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt index e18a633..6315504 100644 --- a/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt +++ b/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt @@ -28,6 +28,8 @@ interface RetrofitPixabayApi { @Query("key") apiKey: String = API_KEY, @Query("q") query: String, @Query("image_type") imageType: String = "photo", + @Query("page") page: Int, + @Query("perPage") perPage: Int, // Add more parameters as needed ): NetworkSearchImages } @@ -50,7 +52,7 @@ class RetrofitPqNetwork @Inject constructor( .build() .create(RetrofitPixabayApi::class.java) - override suspend fun searchImages(query: String): NetworkSearchImages { - return pixabayApi.searchImages(query = query) + override suspend fun searchImages(query: String, page: Int, perPage: Int): NetworkSearchImages { + return pixabayApi.searchImages(query = query, page = page, perPage = perPage) } } diff --git a/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryScreen.kt b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryScreen.kt index ff7a691..3d95f13 100644 --- a/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryScreen.kt +++ b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryScreen.kt @@ -1,26 +1,66 @@ package com.wei.picquest.feature.photolibrary.photolibrary +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.ScrollableDefaults +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsTopHeight +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import coil.compose.AsyncImagePainter +import coil.compose.SubcomposeAsyncImage +import coil.compose.SubcomposeAsyncImageContent +import com.wei.picquest.core.data.model.ImageDetail import com.wei.picquest.core.designsystem.component.FunctionalityNotAvailablePopup -import com.wei.picquest.core.designsystem.component.ThemePreviews -import com.wei.picquest.core.designsystem.theme.PqTheme +import com.wei.picquest.core.designsystem.icon.PqIcons +import com.wei.picquest.core.designsystem.theme.SPACING_LARGE +import com.wei.picquest.core.designsystem.theme.SPACING_MEDIUM +import com.wei.picquest.core.designsystem.theme.SPACING_SMALL +import com.wei.picquest.feature.photolibrary.R +import com.wei.picquest.feature.photolibrary.photolibrary.component.LayoutSwitchWarningDialog /** * @@ -54,15 +94,49 @@ import com.wei.picquest.core.designsystem.theme.PqTheme @Composable internal fun PhotoLibraryRoute( navController: NavController, + viewModel: PhotoLibraryViewModel = hiltViewModel(), ) { - PhotoLibraryScreen() + val query = "your_search_query" // 這應該是動態查詢字串 + val lazyPagingItems = viewModel.imagesState.collectAsLazyPagingItems() + val uiStates: PhotoLibraryViewState by viewModel.states.collectAsStateWithLifecycle() + + Surface(modifier = Modifier.fillMaxSize()) { + Box { + when (uiStates.layoutType) { + LayoutType.LIST -> PhotoLibraryListScreen(lazyPagingItems = lazyPagingItems) + LayoutType.GRID -> PhotoLibraryGridScreen(lazyPagingItems = lazyPagingItems) + } + + TopBarActions( + layoutType = uiStates.layoutType, + onSwitchLayoutClick = { + viewModel.dispatch(PhotoLibraryViewAction.SwitchLayoutType) + }, + ) + } + } } @Composable -internal fun PhotoLibraryScreen( - withTopSpacer: Boolean = true, - withBottomSpacer: Boolean = true, +fun TopBarActions( + layoutType: LayoutType, + onSwitchLayoutClick: () -> Unit, ) { + Column { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) + Row(modifier = Modifier.padding(SPACING_MEDIUM.dp)) { + BackButton() + Spacer(modifier = Modifier.weight(1f)) + SwitchLayoutButton( + layoutType = layoutType, + onClick = onSwitchLayoutClick, + ) + } + } +} + +@Composable +fun BackButton() { val showPopup = remember { mutableStateOf(false) } if (showPopup.value) { @@ -73,40 +147,244 @@ internal fun PhotoLibraryScreen( ) } - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, + IconButton( + onClick = { + /* TODO: Implement back button action */ + showPopup.value = true + }, + modifier = Modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceVariant) + .semantics { contentDescription = "Back" }, ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (withTopSpacer) { - Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) + Icon( + imageVector = PqIcons.ArrowBack, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } +} + +@Composable +fun SwitchLayoutButton( + layoutType: LayoutType, + onClick: () -> Unit, +) { + val showPopup = remember { mutableStateOf(false) } + + if (showPopup.value) { + LayoutSwitchWarningDialog( + onDismiss = { + showPopup.value = false + onClick() + }, + ) + } + + IconButton( + onClick = { + showPopup.value = true + }, + modifier = Modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceVariant) + .semantics { contentDescription = "Switch Layout" }, + ) { + Icon( + imageVector = if (layoutType == LayoutType.LIST) PqIcons.GridView else PqIcons.ListView, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } +} + +@Composable +fun PhotoLibraryListScreen( + lazyPagingItems: LazyPagingItems, + withTopSpacer: Boolean = true, + withBottomSpacer: Boolean = true, +) { + Surface { + Box(modifier = Modifier.fillMaxSize()) { + Column { + LazyColumn(modifier = Modifier.weight(1f)) { + if (withTopSpacer) { + item { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) + } + } + + items(lazyPagingItems.itemCount) { index -> + lazyPagingItems[index]?.let { + ImageDetailItem( + layoutType = LayoutType.LIST, + imageDetail = it, + ) + } + } + + if (withBottomSpacer) { + item { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + } + } + } + + PagingStateHandling(lazyPagingItems) } + } + } +} +@Composable +fun PhotoLibraryGridScreen( + lazyPagingItems: LazyPagingItems, + withTopSpacer: Boolean = true, + withBottomSpacer: Boolean = true, +) { + Surface { + Box(modifier = Modifier.fillMaxSize()) { Column { - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "Photo Library Screen not available \uD83D\uDE48", - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.headlineMedium, + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(150.dp), + modifier = Modifier.weight(1f), + state = rememberLazyStaggeredGridState(), + contentPadding = PaddingValues(0.dp), + horizontalArrangement = Arrangement.spacedBy(0.dp), + verticalItemSpacing = 0.dp, + flingBehavior = ScrollableDefaults.flingBehavior(), + ) { + if (withTopSpacer) { + item { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) + } + } + + items(lazyPagingItems.itemCount) { index -> + lazyPagingItems[index]?.let { + ImageDetailItem( + layoutType = LayoutType.GRID, + imageDetail = it, + ) + } + } + + if (withBottomSpacer) { + item { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + } + } + } + + PagingStateHandling(lazyPagingItems) + } + } + } +} + +@Composable +fun ImageDetailItem( + layoutType: LayoutType, + imageDetail: ImageDetail, +) { + val imageAspectRatio = imageDetail.aspectRatio + + SubcomposeAsyncImage( + model = imageDetail.webformatURL, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(imageAspectRatio), + ) { + when (painter.state) { + is AsyncImagePainter.State.Loading, is AsyncImagePainter.State.Error -> { + Box( modifier = Modifier - .semantics { contentDescription = "" }, - ) - Spacer(modifier = Modifier.weight(1f)) + .fillMaxWidth() + .aspectRatio(imageAspectRatio), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = R.drawable.placeholder_image), + contentDescription = null, + modifier = if (layoutType == LayoutType.LIST) { + Modifier.size(300.dp) + } else { + Modifier.size( + 80.dp, + ) + }, + ) + } } - if (withBottomSpacer) { - Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + else -> { + SubcomposeAsyncImageContent() } } } } -@ThemePreviews @Composable -fun HomeScreenPreview() { - PqTheme { - PhotoLibraryScreen() +fun PagingStateHandling(lazyPagingItems: LazyPagingItems) { + lazyPagingItems.apply { + when { + loadState.refresh is LoadState.Loading -> PageLoader() + loadState.refresh is LoadState.Error -> PageLoaderError { retry() } + loadState.append is LoadState.Loading -> LoadingNextPageItem() + loadState.append is LoadState.Error -> ErrorMessage { retry() } + } + } +} + +@Composable +fun PageLoaderError(onClickRetry: () -> Unit) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + OutlinedButton(onClick = onClickRetry) { + Text(text = stringResource(R.string.retry)) + } + } +} + +@Composable +fun PageLoader() { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + CircularProgressIndicator() + } +} + +@Composable +fun ErrorMessage( + onClickRetry: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(text = stringResource(R.string.error_message), color = MaterialTheme.colorScheme.error) + Spacer(modifier = Modifier.height(SPACING_SMALL.dp)) + OutlinedButton(onClick = onClickRetry) { + Text(text = stringResource(R.string.retry)) + } + } +} + +@Composable +fun LoadingNextPageItem(modifier: Modifier = Modifier) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .fillMaxWidth() + .padding(SPACING_LARGE.dp), + ) { + CircularProgressIndicator(modifier = Modifier.size(30.dp)) } } diff --git a/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryViewModel.kt b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryViewModel.kt new file mode 100644 index 0000000..7dee194 --- /dev/null +++ b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryViewModel.kt @@ -0,0 +1,57 @@ +package com.wei.picquest.feature.photolibrary.photolibrary + +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.wei.picquest.core.base.BaseViewModel +import com.wei.picquest.core.data.model.ImageDetail +import com.wei.picquest.core.data.repository.SearchImagesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PhotoLibraryViewModel @Inject constructor( + private val searchImagesRepository: SearchImagesRepository, +) : BaseViewModel< + PhotoLibraryViewAction, + PhotoLibraryViewState, + >(PhotoLibraryViewState()) { + + private val _imagesState: MutableStateFlow> = + MutableStateFlow(value = PagingData.empty()) + val imagesState: MutableStateFlow> get() = _imagesState + + init { + // TODO Wei + searchImages("") + } + + private fun searchImages(query: String) { + viewModelScope.launch { + searchImagesRepository.getSearchImages(query) + .distinctUntilChanged() + .cachedIn(viewModelScope) + .collect { pagingData -> + _imagesState.value = pagingData + } + } + } + + private fun switchLayoutType() { + updateState { + copy( + layoutType = if (layoutType == LayoutType.LIST) LayoutType.GRID else LayoutType.LIST, + ) + } + } + + override fun dispatch(action: PhotoLibraryViewAction) { + when (action) { + is PhotoLibraryViewAction.SearchImages -> searchImages(action.query) + is PhotoLibraryViewAction.SwitchLayoutType -> switchLayoutType() + } + } +} diff --git a/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryViewState.kt b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryViewState.kt new file mode 100644 index 0000000..177d4cf --- /dev/null +++ b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/PhotoLibraryViewState.kt @@ -0,0 +1,20 @@ +package com.wei.picquest.feature.photolibrary.photolibrary + +import com.wei.picquest.core.base.Action +import com.wei.picquest.core.base.State + +enum class LayoutType { + LIST, GRID +} + +sealed class PhotoLibraryViewAction : Action { + data class SearchImages( + val query: String, + ) : PhotoLibraryViewAction() + + object SwitchLayoutType : PhotoLibraryViewAction() +} + +data class PhotoLibraryViewState( + val layoutType: LayoutType = LayoutType.LIST, +) : State diff --git a/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/component/LayoutSwitchWarningDialog.kt b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/component/LayoutSwitchWarningDialog.kt new file mode 100644 index 0000000..09b3527 --- /dev/null +++ b/feature/photolibrary/src/main/java/com/wei/picquest/feature/photolibrary/photolibrary/component/LayoutSwitchWarningDialog.kt @@ -0,0 +1,38 @@ +package com.wei.picquest.feature.photolibrary.photolibrary.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import com.wei.picquest.feature.photolibrary.R + +@Composable +fun LayoutSwitchWarningDialog(onDismiss: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + text = { + val warningText = stringResource(R.string.layout_switch_warning) + + Text( + text = warningText, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.semantics { contentDescription = warningText }, + ) + }, + confirmButton = { + val buttonText = stringResource(R.string.understand) + + TextButton(onClick = onDismiss) { + Text( + text = buttonText, + modifier = Modifier.semantics { contentDescription = buttonText }, + ) + } + }, + ) +} diff --git a/feature/photolibrary/src/main/res/drawable/placeholder_image.png b/feature/photolibrary/src/main/res/drawable/placeholder_image.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c902e98cd753bf25db5cecff4fd00edf57f89b GIT binary patch literal 8806 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 + + Retry + 發生錯誤,請稍後重試 + 因為網格採動態佈局,故切換佈局時會重新滾動至頂部。 + 了解 + \ No newline at end of file diff --git a/feature/photolibrary/src/main/res/values/strings.xml b/feature/photolibrary/src/main/res/values/strings.xml new file mode 100644 index 0000000..21de76d --- /dev/null +++ b/feature/photolibrary/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + Retry + An error occurred, please try again later + Switching layouts will reset the scroll position to the top due to the dynamic grid layout. + Understand + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7be5d0a..5c78329 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,6 +39,8 @@ secrets = "2.0.1" lint = "31.0.2" robolectric = "4.10.3" roborazzi = "1.5.0-alpha-2" +paging = "3.2.1" +pagingCompose = "3.3.0-alpha02" [libraries] @@ -170,6 +172,11 @@ robolectric-shadows = { group = "org.robolectric", name = "shadows-framework", v # roborazzi - A Screenshot tests lib in jvm. roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" } +# paging 3 +paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } +paging-common = { group = "androidx.paging", name = "paging-common", version.ref = "paging" } +paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "pagingCompose" } + # Dependencies of the included build-logic android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }