diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 024dc000..302e7956 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -54,6 +54,7 @@ kotlin { implementation(project(":feature:follower")) implementation(project(":feature:favourite")) implementation(project(":feature:settings")) + implementation(project(":feature:match")) } } } diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index 53cac13a..cbe5dd85 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -9,6 +9,7 @@ import com.stslex.feature.favourite.di.featureFavouriteModule import com.stslex.feature.film.di.featureFilmModule import com.stslex.feature.film_feed.di.featureFeedModule import com.stslex.feature.follower.di.featureFollowerModule +import com.stslex.feature.match.di.featureMatchModule import com.stslex.feature.match_feed.di.featureMatchFeedModule import com.stslex.feature.profile.di.featureProfileModule import com.stslex.feature.settings.di.featureSettingsModule @@ -47,7 +48,8 @@ private fun KoinApplication.setupCommonModules() { featureAuthModule, featureFollowerModule, featureFavouriteModule, - featureSettingsModule + featureSettingsModule, + featureMatchModule ) ) } diff --git a/composeApp/src/commonMain/kotlin/InitialApp.kt b/composeApp/src/commonMain/kotlin/InitialApp.kt index 125beffd..7b22fb7d 100644 --- a/composeApp/src/commonMain/kotlin/InitialApp.kt +++ b/composeApp/src/commonMain/kotlin/InitialApp.kt @@ -45,7 +45,7 @@ fun InitialApp( modifier = modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) - ) { paddingValues -> + ) { _ -> Box( modifier = Modifier.fillMaxSize() ) { diff --git a/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/BottomNavigationTabs.kt b/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/BottomNavigationTabs.kt index 093bf878..59c4f296 100644 --- a/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/BottomNavigationTabs.kt +++ b/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/BottomNavigationTabs.kt @@ -6,6 +6,6 @@ enum class BottomNavigationTabs( val tab: Tab ) { FILM_FEED(FeedTab), - MATCH_FEED(MatchFeedTab), + MATCH_FEED(MatchTab), PROFILE(ProfileTab), } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/FeedTab.kt b/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/FeedTab.kt index c0f2053d..44a896bc 100644 --- a/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/FeedTab.kt +++ b/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/FeedTab.kt @@ -1,7 +1,7 @@ package main_screen.bottom_nav_bar import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.List +import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.vector.rememberVectorPainter @@ -17,7 +17,7 @@ object FeedTab : Tab { @Composable get() { val title = "feed" - val icon = rememberVectorPainter(Icons.Default.List) + val icon = rememberVectorPainter(Icons.AutoMirrored.Filled.List) return remember { TabOptions( @@ -30,7 +30,7 @@ object FeedTab : Tab { @Composable override fun Content() { - Navigator(FeedScreen){ + Navigator(FeedScreen) { SlideTransition(it) } } diff --git a/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/MatchFeedTab.kt b/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/MatchTab.kt similarity index 82% rename from composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/MatchFeedTab.kt rename to composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/MatchTab.kt index e98000f0..fc31b525 100644 --- a/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/MatchFeedTab.kt +++ b/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/MatchTab.kt @@ -8,9 +8,10 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions -import com.stslex.feature.match_feed.ui.MatchFeedScreen +import com.stslex.core.ui.navigation.args.MatchScreenArgs +import com.stslex.feature.match.ui.MatchScreen -object MatchFeedTab : Tab { +object MatchTab : Tab { override val options: TabOptions @Composable @@ -29,6 +30,6 @@ object MatchFeedTab : Tab { @Composable override fun Content() { - Navigator(MatchFeedScreen) + Navigator(MatchScreen(MatchScreenArgs.Self)) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt b/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt index cdd16248..eefcc306 100644 --- a/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt +++ b/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt @@ -8,6 +8,7 @@ import com.stslex.feature.favourite.FavouriteScreen import com.stslex.feature.film.ui.FilmScreen import com.stslex.feature.follower.navigation.FollowerScreenArgs import com.stslex.feature.follower.ui.FollowerScreen +import com.stslex.feature.match.ui.MatchScreen import com.stslex.feature.match_feed.ui.MatchFeedScreen import com.stslex.feature.settings.ui.SettingsScreen import main_screen.MainScreen @@ -35,15 +36,17 @@ class AppNavigatorImpl : AppNavigator { screen: AppScreen ) { when (screen) { - AppScreen.Back -> navigator.pop() - AppScreen.Auth -> navigator.replaceAll(AuthScreen) - AppScreen.Main -> navigator.replaceAll(MainScreen) + is AppScreen.Back -> navigator.pop() + is AppScreen.Auth -> navigator.replaceAll(AuthScreen) + is AppScreen.Main -> navigator.replaceAll(MainScreen) is AppScreen.Film -> navigator.push(FilmScreen(screen.id)) - AppScreen.MatchFeed -> navigator.push(MatchFeedScreen) + is AppScreen.MatchFeed -> navigator.push(MatchFeedScreen) is AppScreen.Favourite -> navigator.push(FavouriteScreen(uuid = screen.uuid)) is AppScreen.Followers -> navToFollowers(screen.uuid) is AppScreen.Following -> navToFollowing(screen.uuid) - AppScreen.Settings -> navigator.push(SettingsScreen) + is AppScreen.Settings -> navigator.push(SettingsScreen) + is AppScreen.MatchDetails -> TODO() + is AppScreen.Match -> navigator.push(MatchScreen(args = screen.args)) } } diff --git a/core/core/build.gradle.kts b/core/core/build.gradle.kts index 8af3b60b..6f63384f 100644 --- a/core/core/build.gradle.kts +++ b/core/core/build.gradle.kts @@ -1,10 +1,9 @@ -import org.jetbrains.compose.ExperimentalComposeLibrary - plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) alias(libs.plugins.jetbrainsCompose) alias(libs.plugins.kotlinCocoapods) + alias(libs.plugins.kotlinSerialization) } kotlin { @@ -41,6 +40,7 @@ kotlin { api(libs.koin.compose) api(libs.kotlinx.collections.immutable) api(libs.coroutines.core) + implementation(libs.kotlinx.serialization.json) } androidMain.dependencies { api(libs.coroutines.android) diff --git a/core/core/src/commonMain/kotlin/com/stslex/core/core/CommonUtils.kt b/core/core/src/commonMain/kotlin/com/stslex/core/core/CommonUtils.kt index fbe1125e..0da39fbe 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/core/core/CommonUtils.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/core/core/CommonUtils.kt @@ -1,9 +1,18 @@ package com.stslex.core.core import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> Logger.exception(throwable) } -expect fun randomUuid(): String \ No newline at end of file +expect fun randomUuid(): String + +suspend fun List.asyncMap( + transform: suspend (T) -> R +): List = coroutineScope { + map { item -> async { transform(item) } } +}.awaitAll() \ No newline at end of file diff --git a/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingCoreData.kt b/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingCoreData.kt new file mode 100644 index 00000000..2ce49903 --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingCoreData.kt @@ -0,0 +1,15 @@ +package com.stslex.core.core.paging + +interface PagingCoreData { + val page: Int + val pageSize: Int + val total: Int + val hasMore: Boolean + val result: List + + companion object { + + const val DEFAULT_PAGE_SIZE = 15 + const val DEFAULT_PAGE = 0 + } +} diff --git a/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingCoreItem.kt b/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingCoreItem.kt new file mode 100644 index 00000000..e00f7fbd --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingCoreItem.kt @@ -0,0 +1,5 @@ +package com.stslex.core.core.paging + +interface PagingCoreItem { + val uuid: String +} \ No newline at end of file diff --git a/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingResponse.kt b/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingResponse.kt new file mode 100644 index 00000000..2577def2 --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingResponse.kt @@ -0,0 +1,31 @@ +package com.stslex.core.core.paging + +import com.stslex.core.core.asyncMap +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PagingResponse( + @SerialName("page") + override val page: Int, + @SerialName("page_size") + override val pageSize: Int, + @SerialName("total") + override val total: Int, + @SerialName("has_more") + override val hasMore: Boolean, + @SerialName("result") + override val result: List, +) : PagingCoreData + +suspend fun PagingResponse.pagingMap( + transform: suspend (T) -> R, +): PagingResponse = PagingResponse( + page = page, + pageSize = pageSize, + total = total, + hasMore = hasMore, + result = result.asyncMap { + transform(it) + }, +) \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/base/NetworkClient.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/base/NetworkClient.kt index 6d5b5e48..7577fd58 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/base/NetworkClient.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/base/NetworkClient.kt @@ -1,8 +1,50 @@ package com.stslex.core.network.api.base +import com.stslex.core.network.api.base.NetworkClient.Companion.PARAMETER_PAGE +import com.stslex.core.network.api.base.NetworkClient.Companion.PARAMETER_PAGE_SIZE +import com.stslex.core.network.api.base.NetworkClient.Companion.PARAMETER_QUERY +import com.stslex.core.network.api.base.NetworkClient.Companion.PARAMETER_UUID +import com.stslex.core.network.model.PagingRequest import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.post interface NetworkClient { suspend fun request(request: suspend HttpClient.() -> T): T + + companion object { + internal const val PARAMETER_QUERY = "query" + internal const val PARAMETER_PAGE = "page" + internal const val PARAMETER_PAGE_SIZE = "page_size" + internal const val PARAMETER_UUID = "uuid" + } +} + +internal suspend inline fun NetworkClient.get( + urlString: String = "", + crossinline builder: suspend HttpRequestBuilder.() -> Unit +): T = request { + get(urlString = urlString) { + builder() + }.body() } + +internal suspend inline fun NetworkClient.post( + urlString: String = "", + crossinline builder: suspend HttpRequestBuilder.() -> Unit +): T = request { + post(urlString = urlString) { + builder() + }.body() +} + +internal fun HttpRequestBuilder.requestPaging(request: PagingRequest) { + parameter(PARAMETER_UUID, request.uuid) + parameter(PARAMETER_QUERY, request.query) + parameter(PARAMETER_PAGE, request.page) + parameter(PARAMETER_PAGE_SIZE, request.pageSize) +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/client/ServerApiClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/client/ServerApiClientImpl.kt index 8e8c6817..11f65a40 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/client/ServerApiClientImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/client/ServerApiClientImpl.kt @@ -20,4 +20,4 @@ class ServerApiClientImpl( request(client.authClient) } } -} \ No newline at end of file +} diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/client/MatchClient.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/client/MatchClient.kt new file mode 100644 index 00000000..38e67286 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/client/MatchClient.kt @@ -0,0 +1,16 @@ +package com.stslex.core.network.clients.match.client + +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.network.clients.match.model.request.MatchCreateRequest +import com.stslex.core.network.clients.match.model.response.MatchDetailResponse +import com.stslex.core.network.clients.match.model.response.MatchResponse +import com.stslex.core.network.model.PagingRequest + +interface MatchClient { + + suspend fun getMatches(request: PagingRequest): PagingResponse + + suspend fun getMatch(matchUuid: String): MatchDetailResponse + + suspend fun createMatch(request: MatchCreateRequest): MatchDetailResponse +} diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/client/MatchClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/client/MatchClientImpl.kt new file mode 100644 index 00000000..cd24a890 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/client/MatchClientImpl.kt @@ -0,0 +1,42 @@ +package com.stslex.core.network.clients.match.client + +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.network.api.base.get +import com.stslex.core.network.api.base.post +import com.stslex.core.network.api.base.requestPaging +import com.stslex.core.network.api.server.client.ServerApiClient +import com.stslex.core.network.clients.match.model.request.MatchCreateRequest +import com.stslex.core.network.clients.match.model.response.MatchDetailResponse +import com.stslex.core.network.clients.match.model.response.MatchResponse +import com.stslex.core.network.model.PagingRequest +import io.ktor.client.request.parameter +import io.ktor.client.request.setBody + +class MatchClientImpl( + private val client: ServerApiClient +) : MatchClient { + + override suspend fun getMatches( + request: PagingRequest + ): PagingResponse = client.get("$HOST/list") { + requestPaging(request) + } + + override suspend fun getMatch( + matchUuid: String + ): MatchDetailResponse = client.get("$HOST/detail") { + parameter(PARAMETER_MATCH_UUID, matchUuid) + } + + override suspend fun createMatch( + request: MatchCreateRequest + ): MatchDetailResponse = client.post("$HOST/create") { + setBody(request) + } + + companion object { + + private const val HOST = "match" + private const val PARAMETER_MATCH_UUID = "match_uuid" + } +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/client/MockMatchClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/client/MockMatchClientImpl.kt new file mode 100644 index 00000000..a34d080d --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/client/MockMatchClientImpl.kt @@ -0,0 +1,90 @@ +package com.stslex.core.network.clients.match.client + +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.network.clients.match.model.request.MatchCreateRequest +import com.stslex.core.network.clients.match.model.response.MatchDetailResponse +import com.stslex.core.network.clients.match.model.response.MatchResponse +import com.stslex.core.network.clients.match.model.response.MatchStatusResponse +import com.stslex.core.network.clients.match.model.response.MatchUserResponse +import com.stslex.core.network.model.PagingRequest +import com.stslex.core.network.utils.currentTimeMs +import kotlinx.coroutines.delay + +class MockMatchClientImpl : MatchClient { + + private val matches = MutableList(800) { + createMatch(it) + } + + override suspend fun getMatches( + request: PagingRequest + ): PagingResponse { + val page = request.page + val pageSize = request.pageSize + val startIndex = page * pageSize + val endIndex = (startIndex + pageSize).coerceAtMost(matches.size.dec()) + val data = matches.subList(startIndex, endIndex) + delay(1000) + return PagingResponse( + page = page, + pageSize = pageSize, + result = data.map { + MatchResponse( + uuid = it.uuid, + title = it.title, + description = it.description, + status = it.status, + participants = it.participants, + isCreator = it.isCreator, + expiresAt = it.expiresAt + ) + }, + total = matches.size, + hasMore = endIndex < matches.size + ) + } + + override suspend fun getMatch( + matchUuid: String + ): MatchDetailResponse { + delay(1000) + return matches.first { it.uuid == matchUuid } + } + + override suspend fun createMatch( + request: MatchCreateRequest + ): MatchDetailResponse { + delay(1000) + val createdMatch = createMatch(matches.size) + matches.add(createdMatch) + return createdMatch + } + + private fun createMatch(index: Int): MatchDetailResponse { + val created = currentTimeMs + val updated = created + 1000 * 60 * 60 + val expires = created + 1000 * 60 * 60 * 24 + return MatchDetailResponse( + uuid = "uuid$index", + title = "title$index", + description = "description$index", + status = if (index % 2 == 0) MatchStatusResponse.PENDING else MatchStatusResponse.ACTIVE, + participants = createMatchUsers(index), + isCreator = index % 2 == 0, + createdAt = created, + updatedAt = updated, + expiresAt = expires, + ) + } + + private fun createMatchUsers(index: Int): List = + List((index * 2) % 10) { + MatchUserResponse( + uuid = "uuid$it", + avatar = "avatar$it", + username = "username$it", + isCreator = it == 0, + isAccepted = it % 2 == 0 + ) + } +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/request/MatchCreateRequest.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/request/MatchCreateRequest.kt new file mode 100644 index 00000000..aab6781d --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/request/MatchCreateRequest.kt @@ -0,0 +1,16 @@ +package com.stslex.core.network.clients.match.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MatchCreateRequest( + @SerialName("title") + val title: String, + @SerialName("description") + val description: String, + @SerialName("expires_at") + val expiresAt: String, + @SerialName("participants_uuid") + val participantsUuid: List, +) \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchDetailResponse.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchDetailResponse.kt new file mode 100644 index 00000000..90fb4036 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchDetailResponse.kt @@ -0,0 +1,26 @@ +package com.stslex.core.network.clients.match.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MatchDetailResponse( + @SerialName("uuid") + val uuid: String, + @SerialName("title") + val title: String, + @SerialName("description") + val description: String, + @SerialName("status") + val status: MatchStatusResponse = MatchStatusResponse.PENDING, + @SerialName("participants") + val participants: List, + @SerialName("is_creator") + val isCreator: Boolean, + @SerialName("created_at") + val createdAt: Long, + @SerialName("updated_at") + val updatedAt: Long, + @SerialName("expires_at") + val expiresAt: Long, +) diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchResponse.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchResponse.kt new file mode 100644 index 00000000..c28f89b1 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchResponse.kt @@ -0,0 +1,23 @@ +package com.stslex.core.network.clients.match.model.response + +import com.stslex.core.core.paging.PagingCoreItem +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MatchResponse( + @SerialName("uuid") + override val uuid: String, + @SerialName("title") + val title: String, + @SerialName("description") + val description: String, + @SerialName("status") + val status: MatchStatusResponse, + @SerialName("participants") + val participants: List, + @SerialName("is_creator") + val isCreator: Boolean, + @SerialName("expires_at") + val expiresAt: Long, +) : PagingCoreItem diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchStatusResponse.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchStatusResponse.kt new file mode 100644 index 00000000..d5656052 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchStatusResponse.kt @@ -0,0 +1,22 @@ +package com.stslex.core.network.clients.match.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class MatchStatusResponse { + @SerialName("pending") + PENDING, + + @SerialName("active") + ACTIVE, + + @SerialName("expired") + EXPIRED, + + @SerialName("completed") + COMPLETED, + + @SerialName("canceled") + CANCELED, +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchUserResponse.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchUserResponse.kt new file mode 100644 index 00000000..e35ac286 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/match/model/response/MatchUserResponse.kt @@ -0,0 +1,18 @@ +package com.stslex.core.network.clients.match.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MatchUserResponse( + @SerialName("uuid") + val uuid: String, + @SerialName("avatar") + val avatar: String, + @SerialName("username") + val username: String, + @SerialName("is_creator") + val isCreator: Boolean, + @SerialName("is_accepted") + val isAccepted: Boolean, +) \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/MockProfileClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/MockProfileClientImpl.kt index ef92bf23..1f058617 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/MockProfileClientImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/MockProfileClientImpl.kt @@ -1,9 +1,9 @@ package com.stslex.core.network.clients.profile.client import com.stslex.core.core.Logger -import com.stslex.core.network.clients.profile.model.request.PagingRequest +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.network.clients.profile.model.request.PagingProfileRequest import com.stslex.core.network.clients.profile.model.response.BooleanResponse -import com.stslex.core.network.clients.profile.model.response.PagingResponse import com.stslex.core.network.clients.profile.model.response.UserFavouriteResultResponse import com.stslex.core.network.clients.profile.model.response.UserFollowerResponse import com.stslex.core.network.clients.profile.model.response.UserFollowerResultResponse @@ -33,7 +33,7 @@ class MockProfileClientImpl : ProfileClient { override suspend fun getProfile(): UserResponse = getProfile("uuid") override suspend fun searchUser( - request: PagingRequest + request: PagingProfileRequest ): UserSearchResponse { delay(2000) return UserSearchResponse( @@ -56,10 +56,10 @@ class MockProfileClientImpl : ProfileClient { } override suspend fun getFavourites( - request: PagingRequest + request: PagingProfileRequest ): PagingResponse { delay(2000) - return PagingResponse( + return PagingResponse( page = request.page, pageSize = request.pageSize, total = 1, @@ -75,7 +75,7 @@ class MockProfileClientImpl : ProfileClient { } override suspend fun getFollowers( - request: PagingRequest + request: PagingProfileRequest ): UserFollowerResponse { delay(2000) return UserFollowerResponse( @@ -91,7 +91,7 @@ class MockProfileClientImpl : ProfileClient { } override suspend fun getFollowing( - request: PagingRequest + request: PagingProfileRequest ): UserFollowerResponse { delay(2000) return UserFollowerResponse( diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClient.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClient.kt index 763c6262..af79162c 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClient.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClient.kt @@ -1,8 +1,8 @@ package com.stslex.core.network.clients.profile.client -import com.stslex.core.network.clients.profile.model.request.PagingRequest +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.network.clients.profile.model.request.PagingProfileRequest import com.stslex.core.network.clients.profile.model.response.BooleanResponse -import com.stslex.core.network.clients.profile.model.response.PagingResponse import com.stslex.core.network.clients.profile.model.response.UserFavouriteResultResponse import com.stslex.core.network.clients.profile.model.response.UserFollowerResponse import com.stslex.core.network.clients.profile.model.response.UserResponse @@ -14,13 +14,13 @@ interface ProfileClient { suspend fun getProfile(): UserResponse - suspend fun searchUser(request: PagingRequest): UserSearchResponse + suspend fun searchUser(request: PagingProfileRequest): UserSearchResponse - suspend fun getFavourites(request: PagingRequest): PagingResponse + suspend fun getFavourites(request: PagingProfileRequest): PagingResponse - suspend fun getFollowers(request: PagingRequest): UserFollowerResponse + suspend fun getFollowers(request: PagingProfileRequest): UserFollowerResponse - suspend fun getFollowing(request: PagingRequest): UserFollowerResponse + suspend fun getFollowing(request: PagingProfileRequest): UserFollowerResponse suspend fun addFavourite( uuid: String, diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt index da3c1fc7..348e683b 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt @@ -1,10 +1,14 @@ package com.stslex.core.network.clients.profile.client +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.network.api.base.NetworkClient.Companion.PARAMETER_PAGE +import com.stslex.core.network.api.base.NetworkClient.Companion.PARAMETER_PAGE_SIZE +import com.stslex.core.network.api.base.NetworkClient.Companion.PARAMETER_QUERY +import com.stslex.core.network.api.base.NetworkClient.Companion.PARAMETER_UUID import com.stslex.core.network.api.server.client.ServerApiClient import com.stslex.core.network.clients.profile.model.request.AddLikeRequest -import com.stslex.core.network.clients.profile.model.request.PagingRequest +import com.stslex.core.network.clients.profile.model.request.PagingProfileRequest import com.stslex.core.network.clients.profile.model.response.BooleanResponse -import com.stslex.core.network.clients.profile.model.response.PagingResponse import com.stslex.core.network.clients.profile.model.response.UserFavouriteResultResponse import com.stslex.core.network.clients.profile.model.response.UserFollowerResponse import com.stslex.core.network.clients.profile.model.response.UserResponse @@ -34,7 +38,7 @@ class ProfileClientImpl( } override suspend fun searchUser( - request: PagingRequest + request: PagingProfileRequest ): UserSearchResponse = client.request { get("$HOST/search") { requestPaging(request) @@ -42,7 +46,7 @@ class ProfileClientImpl( } override suspend fun getFavourites( - request: PagingRequest + request: PagingProfileRequest ): PagingResponse = client.request { get("$HOST/$HOST_FAVOURITE") { requestPaging(request) @@ -50,7 +54,7 @@ class ProfileClientImpl( } override suspend fun getFollowers( - request: PagingRequest + request: PagingProfileRequest ): UserFollowerResponse = client.request { get("$HOST/$HOST_FOLLOW/followers") { requestPaging(request) @@ -58,7 +62,7 @@ class ProfileClientImpl( } override suspend fun getFollowing( - request: PagingRequest + request: PagingProfileRequest ): UserFollowerResponse = client.request { get("$HOST/$HOST_FOLLOW/following") { requestPaging(request) @@ -94,7 +98,7 @@ class ProfileClientImpl( }.body() } - private fun HttpRequestBuilder.requestPaging(request: PagingRequest) { + private fun HttpRequestBuilder.requestPaging(request: PagingProfileRequest) { if (request.uuid != null) { parameter(PARAMETER_UUID, request.uuid) } @@ -108,11 +112,6 @@ class ProfileClientImpl( private const val HOST_FAVOURITE = "favourite" private const val HOST_FOLLOW = "follow" - - private const val PARAMETER_QUERY = "query" - private const val PARAMETER_PAGE = "page" - private const val PARAMETER_PAGE_SIZE = "page_size" - private const val PARAMETER_UUID = "uuid" } } diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/request/PagingUuidRequest.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/request/PagingProfileRequest.kt similarity index 83% rename from core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/request/PagingUuidRequest.kt rename to core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/request/PagingProfileRequest.kt index 9d38d4d8..64e1b58f 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/request/PagingUuidRequest.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/request/PagingProfileRequest.kt @@ -1,6 +1,6 @@ package com.stslex.core.network.clients.profile.model.request -data class PagingRequest( +data class PagingProfileRequest( val query: String = "", val page: Int, val pageSize: Int, diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/response/PagingResponse.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/response/PagingResponse.kt deleted file mode 100644 index 4c514fb0..00000000 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/response/PagingResponse.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.stslex.core.network.clients.profile.model.response - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class PagingResponse( - @SerialName("page") - val page: Int, - @SerialName("page_size") - val pageSize: Int, - @SerialName("total") - val total: Int, - @SerialName("has_more") - val hasMore: Boolean, - @SerialName("result") - val result: List, -) diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/response/UserFavouriteResultResponse.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/response/UserFavouriteResultResponse.kt index ed1f4bba..4d41b747 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/response/UserFavouriteResultResponse.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/response/UserFavouriteResultResponse.kt @@ -1,14 +1,15 @@ package com.stslex.core.network.clients.profile.model.response +import com.stslex.core.core.paging.PagingCoreItem import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class UserFavouriteResultResponse( @SerialName("uuid") - val uuid: String, + override val uuid: String, @SerialName("title") val title: String, @SerialName("is_favourite") val isFavourite: Boolean, -) \ No newline at end of file +) : PagingCoreItem \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt index 4ef7af46..4cf256b6 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt @@ -14,6 +14,8 @@ import com.stslex.core.network.clients.auth.client.AuthClient import com.stslex.core.network.clients.auth.client.AuthClientImpl import com.stslex.core.network.clients.film.client.FilmClient import com.stslex.core.network.clients.film.client.MockFilmClientImpl +import com.stslex.core.network.clients.match.client.MatchClient +import com.stslex.core.network.clients.match.client.MockMatchClientImpl import com.stslex.core.network.clients.profile.client.ProfileClient import com.stslex.core.network.clients.profile.client.ProfileClientImpl import com.stslex.core.network.utils.PagingWorker @@ -58,6 +60,7 @@ val coreNetworkModule = module { ) } single { + // todo remove mock MockFilmClientImpl() // FilmClientImpl( // client = get(), @@ -67,6 +70,10 @@ val coreNetworkModule = module { single { ProfileClientImpl(client = get()) } + single { + // todo remove mock + MockMatchClientImpl() + } /*Utils*/ single { diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/model/PagingRequest.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/model/PagingRequest.kt new file mode 100644 index 00000000..1a5cd24d --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/model/PagingRequest.kt @@ -0,0 +1,8 @@ +package com.stslex.core.network.model + +data class PagingRequest( + val uuid: String, + val page: Int, + val pageSize: Int, + val query: String = "", +) diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorker.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorker.kt index 335f6353..5033b37a 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorker.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorker.kt @@ -1,9 +1,11 @@ package com.stslex.core.network.utils -fun interface PagingWorker { +interface PagingWorker { suspend operator fun invoke( request: suspend () -> Unit ) + + suspend fun cancel() } diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorkerImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorkerImpl.kt index 5e6d2c08..454b9a88 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorkerImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorkerImpl.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.launch import kotlin.coroutines.coroutineContext class PagingWorkerImpl : PagingWorker { + private var job: Job? = null private var nextPageJob: Job? = null private var lastRequestTime = 0L @@ -23,6 +24,11 @@ class PagingWorkerImpl : PagingWorker { startRequest(request = request) } + override suspend fun cancel() { + job?.cancel() + nextPageJob?.cancel() + } + private suspend fun startRequest( request: suspend () -> Unit, start: CoroutineStart = CoroutineStart.DEFAULT, diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/AppError.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/AppError.kt new file mode 100644 index 00000000..aaedd4f8 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/AppError.kt @@ -0,0 +1,27 @@ +package com.stslex.core.ui.base + +import androidx.compose.runtime.Stable +import com.stslex.core.network.api.server.model.ErrorRefresh + +@Stable +sealed class AppError(open val message: String) { + + @Stable + data class AuthError( + override val message: String + ) : AppError(message) + + @Stable + data class OtherError( + override val message: String + ) : AppError(message) +} + +fun Throwable.mapToAppError( + otherMessage: String = "unknown error" +): AppError { + return when (this) { + is ErrorRefresh -> AppError.AuthError(message ?: otherMessage) + else -> AppError.OtherError(message ?: otherMessage) + } +} diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/paging/PagingItem.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/paging/PagingItem.kt new file mode 100644 index 00000000..e4916726 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/paging/PagingItem.kt @@ -0,0 +1,9 @@ +package com.stslex.core.ui.base.paging + +import androidx.compose.runtime.Stable +import com.stslex.core.core.paging.PagingCoreItem + +@Stable +interface PagingItem : PagingCoreItem { + override val uuid: String +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/paging/PagingState.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/paging/PagingState.kt new file mode 100644 index 00000000..a0574a58 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/paging/PagingState.kt @@ -0,0 +1,45 @@ +package com.stslex.core.ui.base.paging + +import androidx.compose.runtime.Stable +import com.stslex.core.core.asyncMap +import com.stslex.core.core.paging.PagingCoreData +import com.stslex.core.core.paging.PagingCoreData.Companion.DEFAULT_PAGE +import com.stslex.core.core.paging.PagingCoreData.Companion.DEFAULT_PAGE_SIZE +import com.stslex.core.core.paging.PagingCoreItem +import com.stslex.core.core.paging.PagingResponse +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@Stable +data class PagingState( + override val page: Int, + override val pageSize: Int, + override val total: Int, + override val hasMore: Boolean, + override val result: ImmutableList, +) : PagingCoreData { + + companion object { + + fun default( + pageSize: Int = DEFAULT_PAGE_SIZE, + ) = PagingState( + page = DEFAULT_PAGE, + pageSize = pageSize, + total = 0, + hasMore = true, + result = persistentListOf(), + ) + } +} + +suspend fun PagingResponse.pagingMap( + transform: suspend (T) -> R, +): PagingState = PagingState( + page = page.inc(), + pageSize = pageSize, + total = total, + hasMore = hasMore && result.isNotEmpty(), + result = result.asyncMap { transform(it) }.toImmutableList(), +) \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/BaseStore.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/BaseStore.kt index 3bd8ba2f..bd7438b3 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/BaseStore.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/BaseStore.kt @@ -9,7 +9,6 @@ import com.stslex.core.ui.mvi.Store.Action import com.stslex.core.ui.mvi.Store.Event import com.stslex.core.ui.mvi.Store.Navigation import com.stslex.core.ui.mvi.Store.State -import com.stslex.core.ui.navigation.Router import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -27,9 +26,15 @@ abstract class BaseStore( ) : Store, StateScreenModel(initialState) { private var _lastAction: A? = null - val lastAction: A? + protected val lastAction: A? get() = _lastAction + /** + * Sends an action to the store. Checks if the action is not the same as the last action. + * If the action is not the same as the last action, the last action is updated. + * The action is then processed. + * @param action - action to be sent + */ fun sendAction(action: A) { if (lastAction != action && action !is Action.RepeatLastAction) { _lastAction = action @@ -37,7 +42,10 @@ abstract class BaseStore( process(action) } - abstract fun process(action: A) + /** + * Process the action. This method should be overridden in the child class. + */ + protected abstract fun process(action: A) private fun exceptionHandler( onError: suspend (cause: Throwable) -> Unit = {}, @@ -49,22 +57,49 @@ abstract class BaseStore( } private val _event: MutableSharedFlow = MutableSharedFlow() + + /** + * Flow of events that are sent to the screen. + * */ val event: SharedFlow = _event.asSharedFlow() + /** + * Updates the state of the screen. + * @param update - function that updates the state + * */ protected fun updateState(update: (S) -> S) { mutableState.update(update) } + /** + * Sends an event to the screen. The event is sent on the default dispatcher of the AppDispatcher. + * @param event - event to be sent + * @see AppDispatcher + * */ protected fun sendEvent(event: E) { screenModelScope.launch(appDispatcher.default) { this@BaseStore._event.emit(event) } } + /** + * Navigates to the specified screen. The router is called with the specified event. + * @param event - event to be passed to the router + * @see Router + * */ protected fun navigate(event: N) { router(event) } + /** + * Launches a coroutine and catches exceptions. The coroutine is launched on the default dispatcher of the AppDispatcher. + * @param onError - error handler + * @param onSuccess - success handler + * @param action - action to be executed + * @return Job + * @see Job + * @see AppDispatcher + * */ protected fun launch( onError: suspend (Throwable) -> Unit = {}, onSuccess: suspend CoroutineScope.(T) -> Unit = {}, @@ -76,6 +111,15 @@ abstract class BaseStore( } ) + /** + * Launches a flow and collects it in the screenModelScope. The flow is collected on the default dispatcher. of the AppDispatcher. + * @param onError - error handler + * @param each - action for each element of the flow + * @return Job + * @see Flow + * @see Job + * @see AppDispatcher + * */ protected fun Flow.launchFlow( onError: suspend (cause: Throwable) -> Unit = {}, each: suspend (T) -> Unit diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/Router.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Router.kt similarity index 52% rename from core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/Router.kt rename to core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Router.kt index a6c1bdbc..b2814bd4 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/Router.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Router.kt @@ -1,8 +1,5 @@ -package com.stslex.core.ui.navigation - -import com.stslex.core.ui.mvi.Store +package com.stslex.core.ui.mvi fun interface Router { operator fun invoke(event: E) } - diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/AppScreen.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/AppScreen.kt index 8993e093..58e987d6 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/AppScreen.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/AppScreen.kt @@ -1,5 +1,7 @@ package com.stslex.core.ui.navigation +import com.stslex.core.ui.navigation.args.MatchScreenArgs + sealed interface AppScreen { data object Main : AppScreen @@ -25,4 +27,12 @@ sealed interface AppScreen { data class Followers( val uuid: String ) : AppScreen + + data class MatchDetails( + val matchUuid: String + ) : AppScreen + + data class Match( + val args: MatchScreenArgs + ) : AppScreen } \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/args/MatchScreenArgs.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/args/MatchScreenArgs.kt new file mode 100644 index 00000000..ad9ccc11 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/args/MatchScreenArgs.kt @@ -0,0 +1,21 @@ +package com.stslex.core.ui.navigation.args + +import androidx.compose.runtime.Stable + +@Stable +interface MatchScreenArgs { + + @Stable + data object Self : MatchScreenArgs + + @Stable + data class Other( + private val id: String + ) : MatchScreenArgs + + val isSelf: Boolean + get() = this is Self + + val uuid: String? + get() = (this as? Other)?.uuid +} \ No newline at end of file diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouter.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouter.kt index fe607d92..bd545d46 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouter.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.auth.navigation -import com.stslex.core.ui.navigation.Router +import com.stslex.core.ui.mvi.Router import com.stslex.feature.auth.ui.store.AuthStoreComponent.Navigation interface AuthRouter : Router diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepositoryImpl.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepositoryImpl.kt index 89d5fcd6..9f621552 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepositoryImpl.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepositoryImpl.kt @@ -1,7 +1,7 @@ package com.stslex.feature.favourite.data.repository import com.stslex.core.network.clients.profile.client.ProfileClient -import com.stslex.core.network.clients.profile.model.request.PagingRequest +import com.stslex.core.network.clients.profile.model.request.PagingProfileRequest import com.stslex.feature.favourite.data.model.FavouriteDataModel import com.stslex.feature.favourite.data.model.toData @@ -16,7 +16,7 @@ class FavouriteRepositoryImpl( pageSize: Int ): List = client .getFavourites( - PagingRequest( + PagingProfileRequest( uuid = uuid, query = query, page = page, diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouter.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouter.kt index bc606248..14b352d2 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouter.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.favourite.navigation -import com.stslex.core.ui.navigation.Router +import com.stslex.core.ui.mvi.Router import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Navigation interface FavouriteRouter : Router diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouter.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouter.kt index 07f73665..9d0323d8 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouter.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.film.navigation -import com.stslex.core.ui.navigation.Router +import com.stslex.core.ui.mvi.Router import com.stslex.feature.film.ui.store.FilmStoreComponent.Navigation interface FilmRouter : Router diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouter.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouter.kt index 555a79c7..94cfd665 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouter.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.film_feed.navigation -import com.stslex.core.ui.navigation.Router +import com.stslex.core.ui.mvi.Router import com.stslex.feature.film_feed.ui.store.FeedScreenStoreComponent.Navigation interface FeedScreenRouter : Router diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/repository/FollowerRepositoryImpl.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/repository/FollowerRepositoryImpl.kt index d3b1800d..b7293b02 100644 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/repository/FollowerRepositoryImpl.kt +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/repository/FollowerRepositoryImpl.kt @@ -1,7 +1,7 @@ package com.stslex.feature.follower.data.repository import com.stslex.core.network.clients.profile.client.ProfileClient -import com.stslex.core.network.clients.profile.model.request.PagingRequest +import com.stslex.core.network.clients.profile.model.request.PagingProfileRequest import com.stslex.feature.follower.data.model.FollowerDataModel import com.stslex.feature.follower.data.model.toData @@ -16,7 +16,7 @@ class FollowerRepositoryImpl( pageSize: Int ): List = client .getFollowers( - PagingRequest( + PagingProfileRequest( uuid = uuid, query = query, page = page, @@ -32,7 +32,7 @@ class FollowerRepositoryImpl( pageSize: Int ): List = client .getFollowing( - PagingRequest( + PagingProfileRequest( uuid = uuid, query = query, page = page, diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouter.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouter.kt index e7236c49..48d0f9c2 100644 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouter.kt +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.follower.navigation -import com.stslex.core.ui.navigation.Router +import com.stslex.core.ui.mvi.Router import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Navigation interface FollowerRouter : Router diff --git a/feature/match/.gitignore b/feature/match/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/match/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/match/build.gradle.kts b/feature/match/build.gradle.kts new file mode 100644 index 00000000..cc86f459 --- /dev/null +++ b/feature/match/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.kotlinCocoapods) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + jvm("desktop") + + iosX64() + iosArm64() + iosSimulatorArm64() + cocoapods { + summary = "Some description for the Shared Module" + homepage = "Link to the Shared Module homepage" + version = "1.0" + ios.deploymentTarget = "16.0" + podfile = project.file(project.rootProject.projectDir.path + "/iosApp/FeatureMatchPodfile") + framework { + baseName = "featureMatch" + } + } + + sourceSets { + commonMain.dependencies { + implementation(project(":core:core")) + implementation(project(":core:ui")) + implementation(project(":core:network")) + implementation(project(":core:database")) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} + +android { + namespace = "com.stslex.feature.match" + compileSdk = libs.versions.android.compileSdk.get().toInt() + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } +} + +tasks.withType { + compilerOptions.freeCompilerArgs.addAll( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$projectDir/build/compose/metrics", + ) +} + +tasks.withType { + compilerOptions.freeCompilerArgs.addAll( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$projectDir/build/compose/reports", + ) +} \ No newline at end of file diff --git a/feature/match/consumer-rules.pro b/feature/match/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/match/proguard-rules.pro b/feature/match/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/match/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchDataMapper.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchDataMapper.kt new file mode 100644 index 00000000..aa7eb523 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchDataMapper.kt @@ -0,0 +1,32 @@ +package com.stslex.feature.match.data.model + +import com.stslex.core.core.asyncMap +import com.stslex.core.network.clients.match.model.response.MatchResponse +import com.stslex.core.network.clients.match.model.response.MatchStatusResponse +import com.stslex.core.network.clients.match.model.response.MatchUserResponse + +internal suspend fun MatchResponse.toData() = MatchDataModel( + uuid = uuid, + title = title, + description = description, + status = status.toData(), + participants = participants.asyncMap { it.toData() }, + isCreator = isCreator, + expiresAt = expiresAt, +) + +private fun MatchStatusResponse.toData() = when (this) { + MatchStatusResponse.PENDING -> MatchDataStatusModel.PENDING + MatchStatusResponse.ACTIVE -> MatchDataStatusModel.ACTIVE + MatchStatusResponse.EXPIRED -> MatchDataStatusModel.EXPIRED + MatchStatusResponse.COMPLETED -> MatchDataStatusModel.COMPLETED + MatchStatusResponse.CANCELED -> MatchDataStatusModel.CANCELED +} + +private fun MatchUserResponse.toData() = MatchUserDataModel( + uuid = uuid, + avatar = avatar, + username = username, + isCreator = isCreator, + isAccepted = isAccepted +) \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchDataModel.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchDataModel.kt new file mode 100644 index 00000000..f199d506 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchDataModel.kt @@ -0,0 +1,13 @@ +package com.stslex.feature.match.data.model + +import com.stslex.core.ui.base.paging.PagingItem + +data class MatchDataModel( + override val uuid: String, + val title: String, + val description: String, + val status: MatchDataStatusModel, + val participants: List, + val isCreator: Boolean, + val expiresAt: Long, +) : PagingItem diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchDataStatusModel.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchDataStatusModel.kt new file mode 100644 index 00000000..d918722c --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchDataStatusModel.kt @@ -0,0 +1,9 @@ +package com.stslex.feature.match.data.model + +enum class MatchDataStatusModel { + PENDING, + ACTIVE, + EXPIRED, + COMPLETED, + CANCELED, +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchUserDataModel.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchUserDataModel.kt new file mode 100644 index 00000000..bdd1f6d2 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/model/MatchUserDataModel.kt @@ -0,0 +1,9 @@ +package com.stslex.feature.match.data.model + +data class MatchUserDataModel( + val uuid: String, + val avatar: String, + val username: String, + val isCreator: Boolean, + val isAccepted: Boolean, +) \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/repository/MatchRepository.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/repository/MatchRepository.kt new file mode 100644 index 00000000..05bddf52 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/repository/MatchRepository.kt @@ -0,0 +1,14 @@ +package com.stslex.feature.match.data.repository + +import com.stslex.core.core.paging.PagingResponse +import com.stslex.feature.match.data.model.MatchDataModel + +interface MatchRepository { + + suspend fun getMatches( + uuid: String, + query: String, + page: Int, + pageSize: Int + ): PagingResponse +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/repository/MatchRepositoryImpl.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/repository/MatchRepositoryImpl.kt new file mode 100644 index 00000000..c2b4f97a --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/data/repository/MatchRepositoryImpl.kt @@ -0,0 +1,29 @@ +package com.stslex.feature.match.data.repository + +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.core.paging.pagingMap +import com.stslex.core.network.clients.match.client.MatchClient +import com.stslex.core.network.model.PagingRequest +import com.stslex.feature.match.data.model.MatchDataModel +import com.stslex.feature.match.data.model.toData + +class MatchRepositoryImpl( + private val client: MatchClient +) : MatchRepository { + + override suspend fun getMatches( + uuid: String, + query: String, + page: Int, + pageSize: Int + ): PagingResponse = client + .getMatches( + request = PagingRequest( + uuid = uuid, + page = page, + pageSize = pageSize, + query = query + ) + ) + .pagingMap { it.toData() } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/di/MatchModule.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/di/MatchModule.kt new file mode 100644 index 00000000..dd29ae47 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/di/MatchModule.kt @@ -0,0 +1,29 @@ +package com.stslex.feature.match.di + +import com.stslex.feature.match.data.repository.MatchRepository +import com.stslex.feature.match.data.repository.MatchRepositoryImpl +import com.stslex.feature.match.domain.interactor.MatchInteractor +import com.stslex.feature.match.domain.interactor.MatchInteractorImpl +import com.stslex.feature.match.navigation.MatchRouter +import com.stslex.feature.match.navigation.MatchRouterImpl +import com.stslex.feature.match.ui.store.MatchStore +import org.koin.dsl.module + +val featureMatchModule = module { + factory { MatchRepositoryImpl(client = get()) } + factory { + MatchInteractorImpl( + repository = get(), + authController = get() + ) + } + factory { MatchRouterImpl(navigator = get()) } + factory { + MatchStore( + interactor = get(), + router = get(), + appDispatcher = get(), + userStore = get() + ) + } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractor.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractor.kt new file mode 100644 index 00000000..4149bb9d --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractor.kt @@ -0,0 +1,15 @@ +package com.stslex.feature.match.domain.interactor + +import com.stslex.core.core.paging.PagingResponse +import com.stslex.feature.match.domain.model.MatchDomainModel + +interface MatchInteractor { + + suspend fun getMatches( + uuid: String, + page: Int, + pageSize: Int, + ): PagingResponse + + suspend fun logout() +} diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractorImpl.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractorImpl.kt new file mode 100644 index 00000000..2828e174 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractorImpl.kt @@ -0,0 +1,33 @@ +package com.stslex.feature.match.domain.interactor + +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.core.paging.pagingMap +import com.stslex.core.network.utils.token.AuthController +import com.stslex.feature.match.data.repository.MatchRepository +import com.stslex.feature.match.domain.model.MatchDomainModel +import com.stslex.feature.match.domain.model.toDomain + +class MatchInteractorImpl( + private val repository: MatchRepository, + private val authController: AuthController +) : MatchInteractor { + + override suspend fun getMatches( + uuid: String, + page: Int, + pageSize: Int, + ): PagingResponse = repository + .getMatches( + uuid = uuid, + page = page, + pageSize = pageSize, + query = "" // todo add query + ) + .pagingMap { + it.toDomain() + } + + override suspend fun logout() { + authController.logOut() + } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchDomainMapper.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchDomainMapper.kt new file mode 100644 index 00000000..ef485d6f --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchDomainMapper.kt @@ -0,0 +1,43 @@ +package com.stslex.feature.match.domain.model + +import com.stslex.core.core.asyncMap +import com.stslex.feature.match.data.model.MatchDataModel +import com.stslex.feature.match.data.model.MatchDataStatusModel +import com.stslex.feature.match.data.model.MatchUserDataModel + +suspend fun MatchDataModel.toDomain() = MatchDomainModel( + uuid = uuid, + title = title, + description = description, + status = status.toDomain(), + participants = participants.asyncMap { it.toDomain() }, + isCreator = isCreator, + expiresAtDays = expiresAt.toDays(), + expiresAtHours = expiresAt.toHours(), + expiresAtMinutes = expiresAt.toMinutes(), + expiresAtSeconds = expiresAt.toSeconds(), +) + +private fun Long.toDays() = (this / (24 * 60 * 60)).toInt() + +private fun Long.toHours() = ((this % (24 * 60 * 60)) / (60 * 60)).toInt() + +private fun Long.toMinutes() = ((this % (60 * 60)) / 60).toInt() + +private fun Long.toSeconds() = (this % 60).toInt() + +private fun MatchUserDataModel.toDomain() = MatchUserDomainModel( + uuid = uuid, + avatar = avatar, + username = username, + isCreator = isCreator, + isAccepted = isAccepted +) + +private fun MatchDataStatusModel.toDomain() = when (this) { + MatchDataStatusModel.PENDING -> MatchDomainStatus.PENDING + MatchDataStatusModel.ACTIVE -> MatchDomainStatus.ACTIVE + MatchDataStatusModel.EXPIRED -> MatchDomainStatus.EXPIRED + MatchDataStatusModel.COMPLETED -> MatchDomainStatus.COMPLETED + MatchDataStatusModel.CANCELED -> MatchDomainStatus.CANCELED +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchDomainModel.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchDomainModel.kt new file mode 100644 index 00000000..aa1535f5 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchDomainModel.kt @@ -0,0 +1,16 @@ +package com.stslex.feature.match.domain.model + +import com.stslex.core.ui.base.paging.PagingItem + +data class MatchDomainModel( + override val uuid: String, + val title: String, + val description: String, + val status: MatchDomainStatus, + val participants: List, + val isCreator: Boolean, + val expiresAtDays: Int, + val expiresAtHours: Int, + val expiresAtMinutes: Int, + val expiresAtSeconds: Int, +) : PagingItem diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchDomainStatus.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchDomainStatus.kt new file mode 100644 index 00000000..d7b760ed --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchDomainStatus.kt @@ -0,0 +1,9 @@ +package com.stslex.feature.match.domain.model + +enum class MatchDomainStatus { + PENDING, + ACTIVE, + EXPIRED, + COMPLETED, + CANCELED, +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchUserDomainModel.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchUserDomainModel.kt new file mode 100644 index 00000000..70cb67b7 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/model/MatchUserDomainModel.kt @@ -0,0 +1,9 @@ +package com.stslex.feature.match.domain.model + +data class MatchUserDomainModel( + val uuid: String, + val avatar: String, + val username: String, + val isCreator: Boolean, + val isAccepted: Boolean, +) \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouter.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouter.kt new file mode 100644 index 00000000..ef65b251 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouter.kt @@ -0,0 +1,6 @@ +package com.stslex.feature.match.navigation + +import com.stslex.core.ui.mvi.Router +import com.stslex.feature.match.ui.store.MatchStoreComponent.Navigation + +interface MatchRouter : Router \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouterImpl.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouterImpl.kt new file mode 100644 index 00000000..f6940252 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouterImpl.kt @@ -0,0 +1,21 @@ +package com.stslex.feature.match.navigation + +import com.stslex.core.ui.navigation.AppNavigator +import com.stslex.core.ui.navigation.AppScreen +import com.stslex.feature.match.ui.store.MatchStoreComponent.Navigation + +class MatchRouterImpl( + private val navigator: AppNavigator +) : MatchRouter { + + override fun invoke(event: Navigation) { + when (event) { + is Navigation.MatchDetails -> navigateToMatchDetails(event.matchUuid) + is Navigation.LogOut -> navigator.navigate(AppScreen.Auth) + } + } + + private fun navigateToMatchDetails(matchUuid: String) { + navigator.navigate(AppScreen.MatchDetails(matchUuid)) + } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/MatchScreen.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/MatchScreen.kt new file mode 100644 index 00000000..d979ae38 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/MatchScreen.kt @@ -0,0 +1,88 @@ +package com.stslex.feature.match.ui + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.getScreenModel +import com.stslex.core.ui.components.AppSnackbarHost +import com.stslex.core.ui.navigation.args.MatchScreenArgs +import com.stslex.feature.match.ui.components.MatchScreenContent +import com.stslex.feature.match.ui.components.MatchScreenEmpty +import com.stslex.feature.match.ui.components.MatchScreenError +import com.stslex.feature.match.ui.components.MatchScreenShimmer +import com.stslex.feature.match.ui.store.MatchScreenState +import com.stslex.feature.match.ui.store.MatchStore +import com.stslex.feature.match.ui.store.MatchStoreComponent.Action +import com.stslex.feature.match.ui.store.MatchStoreComponent.Event +import com.stslex.feature.match.ui.store.MatchStoreComponent.State + +data class MatchScreen( + private val args: MatchScreenArgs +) : Screen { + + @Composable + override fun Content() { + val store = getScreenModel() + LaunchedEffect(Unit) { + store.sendAction(Action.Init(args = args)) + } + val state by remember { store.state }.collectAsState() + + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(Unit) { + store.event.collect { event -> + when (event) { + is Event.ShowSnackbar -> snackbarHostState.showSnackbar( + message = event.snackbar.message, + actionLabel = event.snackbar.action, + duration = event.snackbar.duration, + withDismissAction = event.snackbar.withDismissAction, + ) + } + } + } + + MatchScreen( + state = state, + onAction = store::sendAction, + snackbarHostState = snackbarHostState + ) + } +} + +@Composable +private fun MatchScreen( + state: State, + onAction: (Action) -> Unit, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, +) { + BoxWithConstraints( + modifier = modifier.fillMaxSize(), + ) { + when (val screen = state.screen) { + is MatchScreenState.Content -> MatchScreenContent( + state = state.pagingState, + screen = screen, + onAction = onAction + ) + + is MatchScreenState.Error -> MatchScreenError( + error = screen.error, + logOut = { onAction(Action.Logout) }, + repeatLastAction = { onAction(Action.RepeatLastAction) } + ) + + MatchScreenState.Empty -> MatchScreenEmpty() + MatchScreenState.Shimmer -> MatchScreenShimmer() + } + AppSnackbarHost(snackbarHostState) + } +} diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenContent.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenContent.kt new file mode 100644 index 00000000..22b9d157 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenContent.kt @@ -0,0 +1,127 @@ +package com.stslex.feature.match.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.stslex.core.ui.base.DotsPrintAnimation +import com.stslex.core.ui.base.paging.PagingState +import com.stslex.core.ui.theme.AppDimension +import com.stslex.feature.match.ui.model.MatchUiModel +import com.stslex.feature.match.ui.store.MatchScreenState +import com.stslex.feature.match.ui.store.MatchStoreComponent.Action + +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun MatchScreenContent( + state: PagingState, + screen: MatchScreenState.Content, + onAction: (Action) -> Unit, + modifier: Modifier = Modifier, +) { + val pullToRefreshState = rememberPullRefreshState( + refreshing = screen is MatchScreenState.Content.Refresh, + onRefresh = { onAction(Action.Refresh) }, + ) + val lazyListState = rememberLazyListState() + + LaunchedEffect(lazyListState) { + snapshotFlow { + lazyListState.firstVisibleItemIndex + } + .collect { firstVisibleItemIndex -> + if ( + firstVisibleItemIndex >= state.result.size - state.pageSize * 0.5f + ) { + onAction(Action.LoadMore) + } + } + } + + Column { + + } + Box( + modifier = modifier.fillMaxSize(), + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = lazyListState, + ) { + item { + Text("total count: ${state.total}, result size: ${state.result.size}") + } + items( + count = state.result.size, + key = { index -> + state.result[index].uuid + }, + ) { index -> + state.result.getOrNull(index)?.let { item -> + MatchItem( + item = item, + onItemClicked = { matchUuid -> + onAction(Action.OnMatchClick(matchUuid)) + }, + ) + } + } + if (screen is MatchScreenState.Content.Append) { + item { + DotsPrintAnimation( + dotsCount = 3, + ) + } + } + } + PullRefreshIndicator( + modifier = Modifier.align(Alignment.TopCenter), + state = pullToRefreshState, + refreshing = screen is MatchScreenState.Content.Refresh + ) + } +} + +@Composable +private fun MatchItem( + item: MatchUiModel, + onItemClicked: (matchUuid: String) -> Unit, + modifier: Modifier = Modifier, +) { + ElevatedCard( + modifier = modifier.fillMaxWidth().padding(AppDimension.Padding.medium), + onClick = { onItemClicked(item.uuid) }, + ) { + Column { + Text( + text = item.title, + modifier = Modifier + .padding(AppDimension.Padding.medium), + style = MaterialTheme.typography.titleLarge, + ) + Spacer(modifier = Modifier.height(AppDimension.Padding.medium)) + Text( + text = item.title, + modifier = Modifier + .padding(AppDimension.Padding.medium), + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenEmpty.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenEmpty.kt new file mode 100644 index 00000000..1ace8c09 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenEmpty.kt @@ -0,0 +1,20 @@ +package com.stslex.feature.match.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +internal fun MatchScreenEmpty( + modifier: Modifier = Modifier +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Empty") + } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenError.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenError.kt new file mode 100644 index 00000000..e65a2acc --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenError.kt @@ -0,0 +1,56 @@ +package com.stslex.feature.match.ui.components + +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.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.stslex.core.ui.base.AppError +import com.stslex.core.ui.theme.AppDimension + +@Composable +internal fun MatchScreenError( + error: AppError, + logOut: () -> Unit, + repeatLastAction: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (error) { + is AppError.AuthError -> { + Text(text = "Auth error: ${error.message}") + Spacer(modifier = Modifier.height(AppDimension.Padding.medium)) + Button(onClick = logOut) { + Text(text = "logout") + } + Spacer(modifier = Modifier.height(AppDimension.Padding.medium)) + Button(onClick = repeatLastAction) { + Text(text = "Retry") + } + } + + is AppError.OtherError -> { + Text(text = "Error: ${error.message}") + Spacer(modifier = Modifier.height(AppDimension.Padding.medium)) + Button(onClick = repeatLastAction) { + Text(text = "Retry") + } + } + } + } + } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenShimmer.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenShimmer.kt new file mode 100644 index 00000000..bcd711ef --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenShimmer.kt @@ -0,0 +1,20 @@ +package com.stslex.feature.match.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +internal fun MatchScreenShimmer( + modifier: Modifier = Modifier +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Shimmer") + } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchDataMapper.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchDataMapper.kt new file mode 100644 index 00000000..3c40734b --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchDataMapper.kt @@ -0,0 +1,37 @@ +package com.stslex.feature.match.ui.model + +import com.stslex.core.core.asyncMap +import com.stslex.feature.match.domain.model.MatchDomainModel +import com.stslex.feature.match.domain.model.MatchDomainStatus +import com.stslex.feature.match.domain.model.MatchUserDomainModel +import kotlinx.collections.immutable.toImmutableList + +internal suspend fun MatchDomainModel.toUi() = MatchUiModel( + uuid = uuid, + title = title, + description = description, + status = status.toUi(), + participants = participants + .asyncMap { it.toUi() } + .toImmutableList(), + isCreator = isCreator, + expiresAtDays = expiresAtDays, + expiresAtHours = expiresAtHours, + expiresAtMinutes = expiresAtMinutes, +) + +private fun MatchDomainStatus.toUi() = when (this) { + MatchDomainStatus.PENDING -> MatchUiStatusModel.PENDING + MatchDomainStatus.ACTIVE -> MatchUiStatusModel.ACTIVE + MatchDomainStatus.EXPIRED -> MatchUiStatusModel.EXPIRED + MatchDomainStatus.COMPLETED -> MatchUiStatusModel.COMPLETED + MatchDomainStatus.CANCELED -> MatchUiStatusModel.CANCELED +} + +private fun MatchUserDomainModel.toUi() = MatchUserUiModel( + uuid = uuid, + avatar = avatar, + username = username, + isCreator = isCreator, + isAccepted = isAccepted +) \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchUiModel.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchUiModel.kt new file mode 100644 index 00000000..6bbda61a --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchUiModel.kt @@ -0,0 +1,18 @@ +package com.stslex.feature.match.ui.model + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.paging.PagingItem +import kotlinx.collections.immutable.ImmutableList + +@Stable +data class MatchUiModel( + override val uuid: String, + val title: String, + val description: String, + val status: MatchUiStatusModel, + val participants: ImmutableList, + val isCreator: Boolean, + val expiresAtDays: Int, + val expiresAtHours: Int, + val expiresAtMinutes: Int, +) : PagingItem diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchUiStatusModel.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchUiStatusModel.kt new file mode 100644 index 00000000..a85f0c9d --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchUiStatusModel.kt @@ -0,0 +1,12 @@ +package com.stslex.feature.match.ui.model + +import androidx.compose.runtime.Stable + +@Stable +enum class MatchUiStatusModel { + PENDING, + ACTIVE, + EXPIRED, + COMPLETED, + CANCELED, +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchUserUiModel.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchUserUiModel.kt new file mode 100644 index 00000000..6cb5a4fe --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchUserUiModel.kt @@ -0,0 +1,12 @@ +package com.stslex.feature.match.ui.model + +import androidx.compose.runtime.Stable + +@Stable +data class MatchUserUiModel( + val uuid: String, + val avatar: String, + val username: String, + val isCreator: Boolean, + val isAccepted: Boolean, +) \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchScreenState.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchScreenState.kt new file mode 100644 index 00000000..c31ee576 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchScreenState.kt @@ -0,0 +1,32 @@ +package com.stslex.feature.match.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.AppError + +@Stable +sealed interface MatchScreenState { + + @Stable + data object Shimmer : MatchScreenState + + @Stable + data object Empty : MatchScreenState + + @Stable + data class Error( + val error: AppError + ) : MatchScreenState + + @Stable + sealed interface Content : MatchScreenState { + + @Stable + data object Data : Content + + @Stable + data object Append : Content + + @Stable + data object Refresh : Content + } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStore.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStore.kt new file mode 100644 index 00000000..c2f7bd5c --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStore.kt @@ -0,0 +1,201 @@ +package com.stslex.feature.match.ui.store + +import com.stslex.core.core.AppDispatcher +import com.stslex.core.core.paging.PagingCoreData.Companion.DEFAULT_PAGE +import com.stslex.core.database.store.UserStore +import com.stslex.core.ui.base.mapToAppError +import com.stslex.core.ui.base.paging.PagingState +import com.stslex.core.ui.base.paging.pagingMap +import com.stslex.core.ui.mvi.BaseStore +import com.stslex.core.ui.mvi.Store.Event.Snackbar +import com.stslex.feature.match.domain.interactor.MatchInteractor +import com.stslex.feature.match.navigation.MatchRouter +import com.stslex.feature.match.ui.model.toUi +import com.stslex.feature.match.ui.store.MatchStoreComponent.Action +import com.stslex.feature.match.ui.store.MatchStoreComponent.Event +import com.stslex.feature.match.ui.store.MatchStoreComponent.Navigation +import com.stslex.feature.match.ui.store.MatchStoreComponent.State +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Job + +class MatchStore( + appDispatcher: AppDispatcher, + router: MatchRouter, + private val interactor: MatchInteractor, + private val userStore: UserStore +) : BaseStore( + appDispatcher = appDispatcher, + router = router, + initialState = State.INITIAL +) { + + private var loadJob: Job? = null + + override fun process(action: Action) { + when (action) { + is Action.Init -> actionInit(action) + is Action.LoadMore -> actionLoadMore() + is Action.OnMatchClick -> actionOnMatchClick(action) + is Action.OnRetryClick -> actionRetryClick() + is Action.Refresh -> actionRefresh() + is Action.Logout -> actionLogout() + is Action.RepeatLastAction -> actionRepeatLastAction() + } + } + + private fun actionInit(action: Action.Init) { + val uuid = action.args.uuid ?: userStore.uuid + updateState { currentState -> + currentState.copy( + isSelf = action.args.isSelf, + uuid = uuid, + screen = MatchScreenState.Shimmer, + pagingState = PagingState.default() + ) + } + loadItems(isForceLoad = true) + } + + private fun actionLoadMore() { + if ( + state.value.pagingState.hasMore.not() || + state.value.screen !is MatchScreenState.Content.Data + ) { + return + } + updateState { currentState -> + currentState.copy( + screen = MatchScreenState.Content.Append + ) + } + loadItems(isForceLoad = false) + } + + private fun actionOnMatchClick(action: Action.OnMatchClick) { + navigate(Navigation.MatchDetails(action.matchUuid)) + } + + private fun actionRetryClick() { + if ( + state.value.screen !is MatchScreenState.Error || + state.value.screen is MatchScreenState.Shimmer + ) { + return + } + updateState { currentState -> + currentState.copy( + screen = MatchScreenState.Shimmer + ) + } + loadItems(isForceLoad = false) + } + + private fun actionRefresh() { + updateState { currentState -> + currentState.copy( + screen = MatchScreenState.Content.Refresh, + pagingState = currentState.pagingState.copy( + page = DEFAULT_PAGE + ) + ) + } + loadItems(isForceLoad = true) + } + + private fun loadItems(isForceLoad: Boolean) { + if (loadJob?.isActive == true && isForceLoad.not()) { + return + } + loadJob?.cancel() + loadJob = launch( + action = { + interactor.getMatches( + uuid = state.value.uuid, + page = state.value.pagingState.page, + pageSize = state.value.pagingState.pageSize, + ) + }, + onSuccess = { result -> + val newPagingState = result.pagingMap { it.toUi() } + if ( + newPagingState.result.isEmpty() && + (state.value.pagingState.page == DEFAULT_PAGE || state.value.pagingState.result.isEmpty()) + ) { + updateState { currentState -> + currentState.copy( + screen = MatchScreenState.Empty, + pagingState = newPagingState + ) + } + return@launch + } + val newItems = if (state.value.pagingState.page == DEFAULT_PAGE) { + newPagingState.result + } else { + (state.value.pagingState.result + newPagingState.result).toImmutableList() + } + updateState { currentState -> + currentState.copy( + screen = MatchScreenState.Content.Data, + pagingState = newPagingState.copy( + result = newItems + ) + ) + } + }, + onError = { error -> + val appError = error.mapToAppError("error load matches") + if (state.value.screen is MatchScreenState.Content) { + sendEvent( + Event.ShowSnackbar(Snackbar.Error(appError.message)) + ) + } else { + updateState { currentState -> + currentState.copy( + screen = MatchScreenState.Error(appError) + ) + } + } + } + ) + } + + private fun actionLogout() { + launch( + action = { + interactor.logout() + }, + onSuccess = { + navigate(Navigation.LogOut) + }, + onError = { error -> + val appError = error.mapToAppError("error logout") + if (state.value.screen is MatchScreenState.Content) { + sendEvent( + Event.ShowSnackbar(Snackbar.Error(appError.message)) + ) + } else { + updateState { currentState -> + currentState.copy( + screen = MatchScreenState.Error(appError) + ) + } + } + } + ) + } + + private fun actionRepeatLastAction() { + val lastAction = lastAction ?: return + updateState { currentState -> + val screen = when (currentState.screen) { + is MatchScreenState.Content -> MatchScreenState.Content.Refresh + is MatchScreenState.Error, + is MatchScreenState.Shimmer, + is MatchScreenState.Empty -> MatchScreenState.Shimmer + } + currentState.copy(screen = screen) + } + process(lastAction) + } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreComponent.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreComponent.kt new file mode 100644 index 00000000..f91a1bd7 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreComponent.kt @@ -0,0 +1,69 @@ +package com.stslex.feature.match.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.paging.PagingState +import com.stslex.core.ui.mvi.Store +import com.stslex.core.ui.mvi.Store.Event.Snackbar +import com.stslex.core.ui.navigation.args.MatchScreenArgs +import com.stslex.feature.match.ui.model.MatchUiModel + +interface MatchStoreComponent : Store { + + @Stable + data class State( + val screen: MatchScreenState, + val uuid: String, + val isSelf: Boolean, + val pagingState: PagingState + ) : Store.State { + + companion object { + + val INITIAL = State( + screen = MatchScreenState.Shimmer, + pagingState = PagingState.default(), + uuid = "", + isSelf = false + ) + } + } + + @Stable + sealed interface Event : Store.Event { + + data class ShowSnackbar( + val snackbar: Snackbar + ) : Event + } + + @Stable + sealed interface Action : Store.Action { + + data class Init( + val args: MatchScreenArgs + ) : Action + + data object Refresh : Action + + data object LoadMore : Action + + data class OnMatchClick( + val matchUuid: String + ) : Action + + data object OnRetryClick : Action + + data object Logout : Action + + data object RepeatLastAction : Action, Store.Action.RepeatLastAction + } + + @Stable + sealed interface Navigation : Store.Navigation { + + data class MatchDetails(val matchUuid: String) : Navigation + + data object LogOut : Navigation + } +} + diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouter.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouter.kt index b2dca079..88f13662 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouter.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.match_feed.navigation -import com.stslex.core.ui.navigation.Router +import com.stslex.core.ui.mvi.Router import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.Navigation interface MatchFeedRouter : Router diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt index 54435770..004ac468 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.profile.navigation -import com.stslex.core.ui.navigation.Router +import com.stslex.core.ui.mvi.Router import com.stslex.feature.profile.ui.store.ProfileStoreComponent interface ProfileRouter : Router \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt index 8a5ecf35..50dc3114 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator @@ -115,7 +116,7 @@ internal fun ProfileScreenError( contentAlignment = Alignment.Center ) { Column( - modifier = modifier, + modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt index 6301907c..9fe9df25 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.settings.navigation -import com.stslex.core.ui.navigation.Router +import com.stslex.core.ui.mvi.Router import com.stslex.feature.settings.ui.store.SettingsStoreComponent interface SettingsRouter : Router diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b4370694..6bbeb730 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,22 @@ [versions] +kotlin = "1.9.23" +compose-plugin = "1.6.1" + android-minSdk = "24" android-compileSdk = "34" android-targetSdk = "34" logback = "1.4.11" -androidCompose = "1.6.4" -compose-plugin = "1.6.0" +androidCompose = "1.6.6" compose-compiler = "1.5.4" agp = "8.2.2" -androidx-activityCompose = "1.8.2" -androidx-core-ktx = "1.12.0" +androidx-activityCompose = "1.9.0" +androidx-core-ktx = "1.13.0" androidx-appcompat = "1.6.1" androidx-material = "1.11.0" androidx-constraintlayout = "2.1.4" androidx-test-junit = "1.1.5" androidx-espresso-core = "3.5.1" -kotlin = "1.9.20" junit = "4.13.2" koin = "3.4.3" @@ -31,7 +32,7 @@ coil = "2.5.0" buildConfig = "4.2.0" lifecycleRuntimeKtx = "2.7.0" -androixComposeBom = "2024.03.00" +androixComposeBom = "2024.04.01" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } diff --git a/iosApp/FeatureMatchPodfile b/iosApp/FeatureMatchPodfile new file mode 100644 index 00000000..6bc04454 --- /dev/null +++ b/iosApp/FeatureMatchPodfile @@ -0,0 +1,5 @@ +target 'test' do + use_frameworks! + platform :ios, '16.0' + pod 'match', :path => '../feature/match' +end \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0e7e0f9b..bf1fa0b1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,3 +32,4 @@ include(":feature:auth") include(":feature:follower") include(":feature:favourite") include(":feature:settings") +include(":feature:match")