diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 2e50a5d4..6c2f3c2c 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -51,6 +51,7 @@ kotlin { implementation(project(":feature:profile")) implementation(project(":feature:match_feed")) implementation(project(":feature:auth")) + implementation(project(":feature:follower")) } } } diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index 55e5e8f8..e5dd044a 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -7,6 +7,7 @@ import com.stslex.core.ui.theme.AppTheme import com.stslex.feature.auth.di.featureAuthModule 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_feed.di.featureMatchFeedModule import com.stslex.feature.profile.di.featureProfileModule import di.appModule @@ -42,6 +43,7 @@ private fun KoinApplication.setupCommonModules() { featureProfileModule, featureMatchFeedModule, featureAuthModule, + featureFollowerModule ) ) } diff --git a/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt b/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt index 4a215d69..f9dc6dfc 100644 --- a/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt +++ b/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt @@ -5,6 +5,8 @@ import com.stslex.core.ui.navigation.AppNavigator import com.stslex.core.ui.navigation.AppScreen import com.stslex.feature.auth.ui.AuthScreen import com.stslex.feature.film.ui.FilmScreen +import com.stslex.feature.follower.ui.FollowerScreen +import com.stslex.feature.follower.ui.FollowerScreenArgs import com.stslex.feature.match_feed.ui.MatchFeedScreen import main_screen.MainScreen @@ -37,8 +39,21 @@ class AppNavigatorImpl : AppNavigator { is AppScreen.Film -> navigator.push(FilmScreen(screen.id)) AppScreen.MatchFeed -> navigator.push(MatchFeedScreen) is AppScreen.Favourite -> TODO() - is AppScreen.Followers -> TODO() - is AppScreen.Following -> TODO() + is AppScreen.Followers -> navigator.push( + FollowerScreen( + args = FollowerScreenArgs.Follower( + uuid = screen.uuid + ) + ) + ) + + is AppScreen.Following -> navigator.push( + FollowerScreen( + args = FollowerScreenArgs.Following( + uuid = screen.uuid + ) + ) + ) } } } \ No newline at end of file diff --git a/feature/follower/build.gradle.kts b/feature/follower/build.gradle.kts new file mode 100644 index 00000000..085eab35 --- /dev/null +++ b/feature/follower/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/FeatureFollowerPodfile") + framework { + baseName = "featureFollower" + } + } + + sourceSets { + commonMain.dependencies { + implementation(project(":core:core")) + implementation(project(":core:ui")) + implementation(project(":core:network")) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} + +android { + namespace = "com.stslex.feature.follower" + 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/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/model/FollowerDataMapper.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/model/FollowerDataMapper.kt new file mode 100644 index 00000000..5d66b111 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/model/FollowerDataMapper.kt @@ -0,0 +1,13 @@ +package com.stslex.feature.follower.data.model + +import com.stslex.core.network.clients.profile.model.UserFollowerResponse + +fun UserFollowerResponse.toData(): List = this + .result + .map { response -> + FollowerDataModel( + uuid = response.uuid, + username = response.username, + avatarUrl = response.avatarUrl, + ) + } \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/model/FollowerDataModel.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/model/FollowerDataModel.kt new file mode 100644 index 00000000..1f7e7b64 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/model/FollowerDataModel.kt @@ -0,0 +1,7 @@ +package com.stslex.feature.follower.data.model + +data class FollowerDataModel( + val uuid: String, + val username: String, + val avatarUrl: String, +) diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/repository/FollowerRepository.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/repository/FollowerRepository.kt new file mode 100644 index 00000000..b2a48030 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/repository/FollowerRepository.kt @@ -0,0 +1,20 @@ +package com.stslex.feature.follower.data.repository + +import com.stslex.feature.follower.data.model.FollowerDataModel +import kotlinx.coroutines.flow.Flow + +interface FollowerRepository { + + fun getFollowers( + uuid: String, + page: Int, + pageSize: Int + ): Flow> + + fun getFollowing( + uuid: String, + page: Int, + pageSize: Int + ): Flow> +} + 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 new file mode 100644 index 00000000..8d3e2ea5 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/data/repository/FollowerRepositoryImpl.kt @@ -0,0 +1,38 @@ +package com.stslex.feature.follower.data.repository + +import com.stslex.core.network.clients.profile.client.ProfileClient +import com.stslex.feature.follower.data.model.FollowerDataModel +import com.stslex.feature.follower.data.model.toData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class FollowerRepositoryImpl( + private val client: ProfileClient +) : FollowerRepository { + + override fun getFollowers( + uuid: String, + page: Int, + pageSize: Int + ): Flow> = flow { + val result = client.getFollowers( + uuid = uuid, + page = page, + pageSize = pageSize + ).toData() + emit(result) + } + + override fun getFollowing( + uuid: String, + page: Int, + pageSize: Int + ): Flow> = flow { + val result = client.getFollowing( + uuid = uuid, + page = page, + pageSize = pageSize + ).toData() + emit(result) + } +} \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/di/FollowerModule.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/di/FollowerModule.kt new file mode 100644 index 00000000..fcaf47e3 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/di/FollowerModule.kt @@ -0,0 +1,35 @@ +package com.stslex.feature.follower.di + +import com.stslex.feature.follower.data.repository.FollowerRepository +import com.stslex.feature.follower.data.repository.FollowerRepositoryImpl +import com.stslex.feature.follower.domain.interactor.FollowerInteractor +import com.stslex.feature.follower.domain.interactor.FollowerInteractorImpl +import com.stslex.feature.follower.navigation.FollowerRouter +import com.stslex.feature.follower.navigation.FollowerRouterImpl +import com.stslex.feature.follower.ui.store.FollowerStore +import org.koin.dsl.module + +val featureFollowerModule = module { + + factory { + FollowerRepositoryImpl( + api = get() + ) + } + + factory { + FollowerInteractorImpl( + repository = get() + ) + } + + factory { FollowerRouterImpl(navigator = get()) } + + factory { + FollowerStore( + interactor = get(), + appDispatcher = get(), + router = get(), + ) + } +} \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/interactor/FollowerInteractor.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/interactor/FollowerInteractor.kt new file mode 100644 index 00000000..94416d74 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/interactor/FollowerInteractor.kt @@ -0,0 +1,20 @@ +package com.stslex.feature.follower.domain.interactor + +import com.stslex.feature.follower.ui.model.FollowerModel +import kotlinx.coroutines.flow.Flow + +interface FollowerInteractor { + + fun getFollowers( + uuid: String, + page: Int, + pageSize: Int + ): Flow> + + fun getFollowing( + uuid: String, + page: Int, + pageSize: Int + ): Flow> +} + diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/interactor/FollowerInteractorImpl.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/interactor/FollowerInteractorImpl.kt new file mode 100644 index 00000000..9857f41f --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/interactor/FollowerInteractorImpl.kt @@ -0,0 +1,35 @@ +package com.stslex.feature.follower.domain.interactor + +import com.stslex.feature.follower.data.repository.FollowerRepository +import com.stslex.feature.follower.domain.model.toUI +import com.stslex.feature.follower.ui.model.FollowerModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class FollowerInteractorImpl( + private val repository: FollowerRepository +) : FollowerInteractor { + + override fun getFollowers( + uuid: String, + page: Int, pageSize: Int + ): Flow> = repository + .getFollowers( + uuid = uuid, + page = page, + pageSize = pageSize + ) + .map { it.toUI() } + + override fun getFollowing( + uuid: String, + page: Int, + pageSize: Int + ): Flow> = repository + .getFollowing( + uuid = uuid, + page = page, + pageSize = pageSize + ) + .map { it.toUI() } +} \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/model/FollowerMapper.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/model/FollowerMapper.kt new file mode 100644 index 00000000..be505199 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/model/FollowerMapper.kt @@ -0,0 +1,12 @@ +package com.stslex.feature.follower.domain.model + +import com.stslex.feature.follower.data.model.FollowerDataModel +import com.stslex.feature.follower.ui.model.FollowerModel + +fun List.toUI(): List = map { it.toUI() } + +fun FollowerDataModel.toUI(): FollowerModel = FollowerModel( + uuid = uuid, + username = username, + avatarUrl = avatarUrl, +) \ No newline at end of file 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 new file mode 100644 index 00000000..e7236c49 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouter.kt @@ -0,0 +1,7 @@ +package com.stslex.feature.follower.navigation + +import com.stslex.core.ui.navigation.Router +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Navigation + +interface FollowerRouter : Router + diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouterImpl.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouterImpl.kt new file mode 100644 index 00000000..ff9b04d0 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouterImpl.kt @@ -0,0 +1,13 @@ +package com.stslex.feature.follower.navigation + +import com.stslex.core.ui.navigation.AppNavigator +import com.stslex.feature.follower.ui.store.FollowerStoreComponent + +class FollowerRouterImpl( + private val navigator: AppNavigator +) : FollowerRouter { + + override fun invoke(event: FollowerStoreComponent.Navigation) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt new file mode 100644 index 00000000..78999fd4 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt @@ -0,0 +1,42 @@ +package com.stslex.feature.follower.ui + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.getScreenModel +import com.stslex.feature.follower.ui.store.FollowerStore +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Action +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.State + +data class FollowerScreen( + val args: FollowerScreenArgs +) : Screen { + + @Composable + override fun Content() { + val store = getScreenModel() + val state by remember { store.state }.collectAsState() + + LaunchedEffect(key1 = Unit) { + store.sendAction(Action.Init(args = args)) + } + + FollowerScreen( + state = state, + onAction = store::sendAction + ) + } +} + + +@Composable +internal fun FollowerScreen( + state: State, + onAction: (Action) -> Unit +) { + Text("test") +} \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreenArgs.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreenArgs.kt new file mode 100644 index 00000000..d5b7c1b2 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreenArgs.kt @@ -0,0 +1,12 @@ +package com.stslex.feature.follower.ui + +sealed interface FollowerScreenArgs { + + data class Follower( + val uuid: String + ) : FollowerScreenArgs + + data class Following( + val uuid: String + ) : FollowerScreenArgs +} \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/model/FollowerModel.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/model/FollowerModel.kt new file mode 100644 index 00000000..7794ef18 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/model/FollowerModel.kt @@ -0,0 +1,10 @@ +package com.stslex.feature.follower.ui.model + +import androidx.compose.runtime.Stable + +@Stable +data class FollowerModel( + val uuid: String, + val username: String, + val avatarUrl: String, +) diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt new file mode 100644 index 00000000..0b09b313 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt @@ -0,0 +1,31 @@ +package com.stslex.feature.follower.ui.store + +import com.stslex.core.core.AppDispatcher +import com.stslex.core.ui.mvi.BaseStore +import com.stslex.feature.follower.domain.interactor.FollowerInteractor +import com.stslex.feature.follower.navigation.FollowerRouter +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Action +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Event +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Navigation +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.State + +class FollowerStore( + private val interactor: FollowerInteractor, + router: FollowerRouter, + appDispatcher: AppDispatcher, +) : BaseStore( + router = router, + appDispatcher = appDispatcher, + initialState = State.INITIAL, +) { + + override fun process(action: Action) { + when (action) { + is Action.Init -> actionInit(action) + } + } + + private fun actionInit(action: Action.Init) { + TODO() + } +} diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreComponent.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreComponent.kt new file mode 100644 index 00000000..67415c6f --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreComponent.kt @@ -0,0 +1,64 @@ +package com.stslex.feature.follower.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.mvi.Store +import com.stslex.feature.follower.ui.FollowerScreenArgs +import com.stslex.feature.follower.ui.model.FollowerModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +interface FollowerStoreComponent : Store { + + @Stable + data class State( + val uuid: String, + val data: ImmutableList, + val screen: FollowerScreenState + ) : Store.State { + + companion object { + + val INITIAL = State( + uuid = "", + data = emptyList().toImmutableList(), + screen = FollowerScreenState.Shimmer + ) + } + } + + @Stable + sealed interface Action : Store.Action { + + @Stable + data class Init( + val args: FollowerScreenArgs + ) : Action + } + + @Stable + sealed interface Event : Store.Event { + + @Stable + data class ErrorSnackBar(val message: String) : Event + } + + sealed interface Navigation : Store.Navigation +} + +@Stable +sealed interface FollowerScreenState { + + @Stable + sealed interface Content : FollowerScreenState { + + data object NotLoading : Content + + data object Loading : Content + } + + @Stable + data object Shimmer : FollowerScreenState + + @Stable + data class Error(val error: Throwable) : FollowerScreenState +} \ No newline at end of file diff --git a/iosApp/FeatureFollowerPodfile b/iosApp/FeatureFollowerPodfile new file mode 100644 index 00000000..a202bb8c --- /dev/null +++ b/iosApp/FeatureFollowerPodfile @@ -0,0 +1,5 @@ +target 'test' do + use_frameworks! + platform :ios, '16.0' + pod 'film', :path => '../feature/follower' +end \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a5d0437c..8f978e60 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,3 +29,4 @@ include(":feature:film") include(":feature:profile") include(":feature:match_feed") include(":feature:auth") +include(":feature:follower")