diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 561f48de..9ca25fdb 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -38,6 +38,7 @@ kotlin { commonMain.dependencies { implementation(project(":core:core")) implementation(project(":core:network")) + implementation(project(":core:database")) implementation(project(":core:ui")) implementation(project(":feature:feed")) implementation(project(":feature:film")) diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index 7322e18e..f0c34bae 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -1,5 +1,6 @@ import androidx.compose.runtime.Composable import com.stslex.core.core.coreModule +import com.stslex.core.database.di.databaseModule import com.stslex.core.network.di.networkModule import com.stslex.core.ui.theme.AppTheme import com.stslex.feature.feed.di.feedModule @@ -34,6 +35,7 @@ private fun setupModules(): KoinAppDeclaration = { appModule, coreModule, networkModule, + databaseModule, feedModule, filmModule, profileModule, diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts new file mode 100644 index 00000000..b423b93b --- /dev/null +++ b/core/database/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsCompose) + kotlin("plugin.serialization") version "1.9.20" +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + jvm("desktop") + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "database" + isStatic = true + } + } + + sourceSets { + commonMain.dependencies { + implementation(project(":core:core")) + implementation(libs.kotlinx.serialization.json) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} + +android { + namespace = "com.stslex.core.database" + compileSdk = libs.versions.android.compileSdk.get().toInt() + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } +} diff --git a/core/database/src/commonMain/kotlin/com/stslex/core/database/di/DatabaseModel.kt b/core/database/src/commonMain/kotlin/com/stslex/core/database/di/DatabaseModel.kt new file mode 100644 index 00000000..a5d17233 --- /dev/null +++ b/core/database/src/commonMain/kotlin/com/stslex/core/database/di/DatabaseModel.kt @@ -0,0 +1,9 @@ +package com.stslex.core.database.di + +import com.stslex.core.database.sources.source.FavouriteFilmDataSource +import com.stslex.core.database.sources.source.FavouriteFilmDataSourceImpl +import org.koin.dsl.module + +val databaseModule = module { + single { FavouriteFilmDataSourceImpl() } +} \ No newline at end of file diff --git a/core/database/src/commonMain/kotlin/com/stslex/core/database/sources/model/FilmEntity.kt b/core/database/src/commonMain/kotlin/com/stslex/core/database/sources/model/FilmEntity.kt new file mode 100644 index 00000000..99097701 --- /dev/null +++ b/core/database/src/commonMain/kotlin/com/stslex/core/database/sources/model/FilmEntity.kt @@ -0,0 +1,18 @@ +package com.stslex.core.database.sources.model + +data class FilmEntity( + val id: String, + val title: String, + val description: String, + val poster: String, + val rating: String, + val duration: String, + val genres: List, + val actors: List, + val director: String, + val country: String, + val year: String, + val age: String, + val type: String, + val trailer: String, +) diff --git a/core/database/src/commonMain/kotlin/com/stslex/core/database/sources/source/FavouriteFilmDataSource.kt b/core/database/src/commonMain/kotlin/com/stslex/core/database/sources/source/FavouriteFilmDataSource.kt new file mode 100644 index 00000000..8f71ed98 --- /dev/null +++ b/core/database/src/commonMain/kotlin/com/stslex/core/database/sources/source/FavouriteFilmDataSource.kt @@ -0,0 +1,12 @@ +package com.stslex.core.database.sources.source + +import com.stslex.core.database.sources.model.FilmEntity + +interface FavouriteFilmDataSource { + + suspend fun getFilm(id: String): FilmEntity? + + suspend fun likeFilm(filmEntity: FilmEntity) + + suspend fun dislikeFilm(id: String) +} \ No newline at end of file diff --git a/core/database/src/commonMain/kotlin/com/stslex/core/database/sources/source/FavouriteFilmDataSourceImpl.kt b/core/database/src/commonMain/kotlin/com/stslex/core/database/sources/source/FavouriteFilmDataSourceImpl.kt new file mode 100644 index 00000000..da825e61 --- /dev/null +++ b/core/database/src/commonMain/kotlin/com/stslex/core/database/sources/source/FavouriteFilmDataSourceImpl.kt @@ -0,0 +1,19 @@ +package com.stslex.core.database.sources.source + +import com.stslex.core.database.sources.model.FilmEntity + +class FavouriteFilmDataSourceImpl : FavouriteFilmDataSource { + + override suspend fun getFilm(id: String): FilmEntity? { + return null + TODO("Not yet implemented") + } + + override suspend fun likeFilm(filmEntity: FilmEntity) { +// TODO("Not yet implemented") + } + + override suspend fun dislikeFilm(id: String) { +// TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClient.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClient.kt index a9f74c8e..0f2c1bad 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClient.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClient.kt @@ -2,10 +2,15 @@ package com.stslex.core.network.clients.film.client import com.stslex.core.network.clients.film.model.FilmFeedResponse import com.stslex.core.network.clients.film.model.FilmResponse +import kotlinx.coroutines.flow.Flow interface FilmClient { suspend fun getFeedFilms(page: Int, pageSize: Int): FilmFeedResponse - suspend fun getFilm(id: String): FilmResponse + fun getFilm(id: String): Flow + + suspend fun likeFilm(id: String) + + suspend fun dislikeFilm(id: String) } \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClientImpl.kt index f8ed1335..73139f28 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClientImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClientImpl.kt @@ -6,6 +6,8 @@ import com.stslex.core.network.clients.film.model.FilmResponse import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.request.parameter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow class FilmClientImpl( private val client: NetworkClient @@ -20,9 +22,20 @@ class FilmClientImpl( }.body() } - override suspend fun getFilm(id: String): FilmResponse = client.request { - get("feed") { - parameter("id", id) - }.body() + override fun getFilm(id: String): Flow = flow { + val result: FilmResponse = client.request { + get("feed") { + parameter("id", id) + }.body() + } + emit(result) + } + + override suspend fun likeFilm(id: String) { + TODO("Not yet implemented") + } + + override suspend fun dislikeFilm(id: String) { + TODO("Not yet implemented") } } \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/MockFilmClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/MockFilmClientImpl.kt index 6a8061fc..3b9d24f9 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/MockFilmClientImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/MockFilmClientImpl.kt @@ -4,9 +4,15 @@ import com.stslex.core.core.Logger import com.stslex.core.network.clients.film.model.FilmFeedResponse import com.stslex.core.network.clients.film.model.FilmResponse import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map class MockFilmClientImpl : FilmClient { + private val filmsFlow = MutableStateFlow(filmsList) + override suspend fun getFeedFilms( page: Int, pageSize: Int @@ -22,65 +28,78 @@ class MockFilmClientImpl : FilmClient { ) } - override suspend fun getFilm(id: String): FilmResponse = getFilmById(id.toInt()) + override fun getFilm(id: String): Flow = filmsFlow + .map { filmsList -> + filmsList[id.toInt() % filmsList.size].copy(id = id) + } + .filterNotNull() + + override suspend fun likeFilm(id: String) { + TODO("Not yet implemented") + } + + override suspend fun dislikeFilm(id: String) { + TODO("Not yet implemented") + } private fun getFilmById(id: Int) = filmsList[id % filmsList.size].copy( id = id.toString() ) +} - private val lokiFilm = FilmResponse( - id = "1", - title = "Локи", - description = "Сразу же после кражи Тессеракта в фильме «Мстители: Финал» (2019), альтернативная версия Локи попадает в «Управление временны́ми изменениями» (УВИ), бюрократическую организацию, которая существует вне пространства и времени. Богу обмана предстоит ответить за свои преступления против времени и перед ним встаёт выбор: подвергнуться стиранию из реальности или помочь УВИ в борьбе с большей угрозой.", - poster = "http://pico.kartinka.shop/poster/item/big/72418.jpg", - rating = "8.2", - genres = listOf("Боевик", "Фантастика", "Фэнтези", "Приключения"), - actors = listOf("Том Хиддлстон", "Софи Ди Мартин", "Оуэн Уилсон"), - director = "Кейт Херон", - year = "2021", - duration = "50 мин.", - country = "США", - age = "16+", - type = "Сериал", - trailer = "https://www.youtube.com/watch?v=nW948Va-l10", - isFavorite = false, - ) - private val infiniteWar = FilmResponse( - id = "2", - title = "Мстители: Война бесконечности", - description = "Пока Мстители и их союзники продолжают защищать мир от различных опасностей, с которыми не смог бы справиться один супергерой, новая угроза возникает из космоса: Танос. Межгалактический тиран преследует цель собрать все шесть Камней Бесконечности - артефакты невероятной силы, с помощью которых можно менять реальность по своему желанию. Всё, с чем Мстители сталкивались ранее, вело к этому моменту - судьба Земли никогда ещё не была столь неопределённой.", - poster = "http://pico.kartinka.shop/poster/item/big/34114.jpg", - rating = "8.4", - genres = listOf("Боевик", "Фантастика", "Фэнтези", "Приключения"), - actors = listOf("Роберт Дауни мл.", "Крис Хемсворт", "Марк Руффало"), - director = "Энтони Руссо", - year = "2018", - duration = "149 мин.", - country = "США", - age = "12+", - type = "Фильм", - trailer = "https://www.youtube.com/watch?v=6ZfuNTqbHE8", - isFavorite = true, - ) +private val lokiFilm = FilmResponse( + id = "1", + title = "Локи", + description = "Сразу же после кражи Тессеракта в фильме «Мстители: Финал» (2019), альтернативная версия Локи попадает в «Управление временны́ми изменениями» (УВИ), бюрократическую организацию, которая существует вне пространства и времени. Богу обмана предстоит ответить за свои преступления против времени и перед ним встаёт выбор: подвергнуться стиранию из реальности или помочь УВИ в борьбе с большей угрозой.", + poster = "http://pico.kartinka.shop/poster/item/big/72418.jpg", + rating = "8.2", + genres = listOf("Боевик", "Фантастика", "Фэнтези", "Приключения"), + actors = listOf("Том Хиддлстон", "Софи Ди Мартин", "Оуэн Уилсон"), + director = "Кейт Херон", + year = "2021", + duration = "50 мин.", + country = "США", + age = "16+", + type = "Сериал", + trailer = "https://www.youtube.com/watch?v=nW948Va-l10", + isFavorite = false, +) - private val rhinoFilm = FilmResponse( - id = "3", - title = "Вольт", - description = "Вольт — собака-полицейский, звезда телесериала, в котором он сражается с преступниками и спасает мир. Но когда камеры отключаются, Вольт не понимает, что происходит, и думает, что всё, что его окружает, настоящее. Когда его хозяйка Пенни похищают, Вольт отправляется в реальное путешествие, чтобы спасти её. На помощь ему приходят два необычных спутника — кот Мистер и хомяк Ролли.", - poster = "http://pico.kartinka.shop/poster/item/big/2570.jpg", - rating = "6.8", - genres = listOf("Комедия", "Фантастика", "Семейный", "Приключения", "Мультфильм"), - actors = listOf("Джон Траволта", "Майли Сайрус", "Сьюзи Эссман"), - director = "Байрон Ховард", - year = "2008", - duration = "96 мин.", - country = "США", - age = "6+", - type = "Фильм", - trailer = "https://www.youtube.com/watch?v=6ZfuNTqbHE8", - isFavorite = false, - ) +private val infiniteWar = FilmResponse( + id = "2", + title = "Мстители: Война бесконечности", + description = "Пока Мстители и их союзники продолжают защищать мир от различных опасностей, с которыми не смог бы справиться один супергерой, новая угроза возникает из космоса: Танос. Межгалактический тиран преследует цель собрать все шесть Камней Бесконечности - артефакты невероятной силы, с помощью которых можно менять реальность по своему желанию. Всё, с чем Мстители сталкивались ранее, вело к этому моменту - судьба Земли никогда ещё не была столь неопределённой.", + poster = "http://pico.kartinka.shop/poster/item/big/34114.jpg", + rating = "8.4", + genres = listOf("Боевик", "Фантастика", "Фэнтези", "Приключения"), + actors = listOf("Роберт Дауни мл.", "Крис Хемсворт", "Марк Руффало"), + director = "Энтони Руссо", + year = "2018", + duration = "149 мин.", + country = "США", + age = "12+", + type = "Фильм", + trailer = "https://www.youtube.com/watch?v=6ZfuNTqbHE8", + isFavorite = true, +) + +private val rhinoFilm = FilmResponse( + id = "3", + title = "Вольт", + description = "Вольт — собака-полицейский, звезда телесериала, в котором он сражается с преступниками и спасает мир. Но когда камеры отключаются, Вольт не понимает, что происходит, и думает, что всё, что его окружает, настоящее. Когда его хозяйка Пенни похищают, Вольт отправляется в реальное путешествие, чтобы спасти её. На помощь ему приходят два необычных спутника — кот Мистер и хомяк Ролли.", + poster = "http://pico.kartinka.shop/poster/item/big/2570.jpg", + rating = "6.8", + genres = listOf("Комедия", "Фантастика", "Семейный", "Приключения", "Мультфильм"), + actors = listOf("Джон Траволта", "Майли Сайрус", "Сьюзи Эссман"), + director = "Байрон Ховард", + year = "2008", + duration = "96 мин.", + country = "США", + age = "6+", + type = "Фильм", + trailer = "https://www.youtube.com/watch?v=6ZfuNTqbHE8", + isFavorite = false, +) - private val filmsList = listOf(lokiFilm, infiniteWar, rhinoFilm) -} \ No newline at end of file +private val filmsList = listOf(lokiFilm, infiniteWar, rhinoFilm) \ 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 5ee970ca..d915b30e 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 @@ -58,11 +58,16 @@ abstract class BaseStore( fun launch( onError: suspend (Throwable) -> Unit = {}, + onSuccess: suspend () -> Unit = {}, block: suspend CoroutineScope.() -> Unit, - ): Job = screenModelScope.launch( - context = exceptionHandler(onError) + appDispatcher.default, - block = block - ) + ): Job = screenModelScope.launch(appDispatcher.default) { + runCatching { block() } + .onSuccess { onSuccess() } + .onFailure { + Logger.exception(it) + onError(it) + } + } fun launch( action: suspend CoroutineScope.() -> T, diff --git a/feature/film/build.gradle.kts b/feature/film/build.gradle.kts index f13c244f..5edc29d9 100644 --- a/feature/film/build.gradle.kts +++ b/feature/film/build.gradle.kts @@ -31,6 +31,7 @@ kotlin { implementation(project(":core:core")) implementation(project(":core:ui")) implementation(project(":core:network")) + implementation(project(":core:database")) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/model/FilmDataMapper.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/model/FilmDataMapper.kt index 2edf9979..8d5040b6 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/model/FilmDataMapper.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/model/FilmDataMapper.kt @@ -1,5 +1,6 @@ package com.stslex.feature.film.data.model +import com.stslex.core.database.sources.model.FilmEntity import com.stslex.core.network.clients.film.model.FilmResponse fun FilmResponse.toData() = FilmData( @@ -18,4 +19,21 @@ fun FilmResponse.toData() = FilmData( type = type, trailer = trailer, isFavorite = isFavorite +) + +fun FilmData.toEntity(): FilmEntity = FilmEntity( + id = id, + title = title, + description = description, + poster = poster, + rating = rating, + duration = duration, + genres = genres, + actors = actors, + director = director, + country = country, + year = year, + age = age, + type = type, + trailer = trailer, ) \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepository.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepository.kt index 0941d75e..f7cf2eb3 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepository.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepository.kt @@ -6,4 +6,8 @@ import kotlinx.coroutines.flow.Flow interface FilmRepository { fun getFilm(id: String): Flow + + suspend fun likeFilm(filmData: FilmData) + + suspend fun dislikeFilm(id: String) } \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepositoryImpl.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepositoryImpl.kt index 0446ddea..6b8bc66f 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepositoryImpl.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepositoryImpl.kt @@ -1,17 +1,32 @@ package com.stslex.feature.film.data.repository +import com.stslex.core.database.sources.source.FavouriteFilmDataSource import com.stslex.core.network.clients.film.client.FilmClient import com.stslex.feature.film.data.model.FilmData import com.stslex.feature.film.data.model.toData +import com.stslex.feature.film.data.model.toEntity import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map class FilmRepositoryImpl( - private val client: FilmClient + private val client: FilmClient, + private val favouriteDatasource: FavouriteFilmDataSource ) : FilmRepository { - override fun getFilm(id: String): Flow = flow { - val film = client.getFilm(id = id).toData() - emit(film) + override fun getFilm(id: String): Flow = client.getFilm(id = id) + .map { + it.toData().copy( + isFavorite = favouriteDatasource.getFilm(id) != null + ) + } + + override suspend fun likeFilm(filmData: FilmData) { + favouriteDatasource.likeFilm(filmData.toEntity()) + client.likeFilm(filmData.id) + } + + override suspend fun dislikeFilm(id: String) { + favouriteDatasource.dislikeFilm(id) + client.dislikeFilm(id) } -} \ No newline at end of file +} diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/di/FilmModule.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/di/FilmModule.kt index 1a7730ee..998b7f93 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/di/FilmModule.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/di/FilmModule.kt @@ -21,5 +21,10 @@ val filmModule = module { FilmRouterImpl(navigator = get()) } factory { FilmInteractorImpl(repository = get()) } - factory { FilmRepositoryImpl(client = get()) } + factory { + FilmRepositoryImpl( + favouriteDatasource = get(), + client = get() + ) + } } \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/interactor/FilmInteractor.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/interactor/FilmInteractor.kt index c247f41e..f75e37fe 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/interactor/FilmInteractor.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/interactor/FilmInteractor.kt @@ -6,4 +6,8 @@ import kotlinx.coroutines.flow.Flow interface FilmInteractor { fun getFilm(id: String): Flow + + suspend fun likeFilm(film: FilmDomain) + + suspend fun dislikeFilm(id: String) } \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/interactor/FilmInteractorImpl.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/interactor/FilmInteractorImpl.kt index ed7c0b97..16808ea3 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/interactor/FilmInteractorImpl.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/interactor/FilmInteractorImpl.kt @@ -2,6 +2,7 @@ package com.stslex.feature.film.domain.interactor import com.stslex.feature.film.data.repository.FilmRepository import com.stslex.feature.film.domain.model.FilmDomain +import com.stslex.feature.film.domain.model.toData import com.stslex.feature.film.domain.model.toDomain import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -17,4 +18,12 @@ class FilmInteractorImpl( .map { film -> film.toDomain() } -} \ No newline at end of file + + override suspend fun likeFilm(film: FilmDomain) { + repository.likeFilm(film.toData()) + } + + override suspend fun dislikeFilm(id: String) { + repository.dislikeFilm(id) + } +} diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/model/FilmDomainMapper.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/model/FilmDomainMapper.kt index 7949ae2e..eec34651 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/model/FilmDomainMapper.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/domain/model/FilmDomainMapper.kt @@ -18,4 +18,22 @@ fun FilmData.toDomain() = FilmDomain( type = type, trailer = trailer, isFavorite = isFavorite +) + +fun FilmDomain.toData(): FilmData = FilmData( + id = id, + title = title, + description = description, + poster = poster, + rating = rating, + duration = duration, + genres = genres, + actors = actors, + director = director, + country = country, + year = year, + age = age, + type = type, + trailer = trailer, + isFavorite = isFavorite ) \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt index 5dd3fca6..646ecda1 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt @@ -64,7 +64,12 @@ internal fun FilmContent( is FilmScreenState.Content -> FilmContentScreen( film = screenState.data, - onAction = onAction + onLikeClick = { + onAction(Action.LikeButtonClick) + }, + onBackClick = { + onAction(Action.BackButtonClick) + } ) } } diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/components/FilmContentScreen.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/components/FilmContentScreen.kt index 3a1992db..5e4be024 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/components/FilmContentScreen.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/components/FilmContentScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.Place import androidx.compose.material.icons.filled.Share import androidx.compose.material.rememberSwipeableState @@ -52,18 +53,19 @@ import androidx.compose.ui.unit.dp import com.stslex.core.ui.base.SwipeScrollConnection import com.stslex.core.ui.base.SwipeState import com.stslex.core.ui.base.image.NetworkImage +import com.stslex.core.ui.base.onClick import com.stslex.core.ui.base.onClickDelay import com.stslex.core.ui.theme.AppDimension import com.stslex.core.ui.theme.toDp import com.stslex.core.ui.theme.toPx import com.stslex.feature.film.ui.model.Film -import com.stslex.feature.film.ui.store.FilmStoreComponent.Action @OptIn(ExperimentalMaterialApi::class) @Composable internal fun FilmContentScreen( film: Film, - onAction: (Action) -> Unit, + onBackClick: () -> Unit, + onLikeClick: () -> Unit, modifier: Modifier = Modifier, ) { val lazyListState = rememberLazyListState() @@ -121,7 +123,10 @@ internal fun FilmContentScreen( targetState = derivedStateOf { progress > 0.5f }.value, ) { isVisible -> if (isVisible) { - FilmActions() + FilmActions( + isLiked = film.isFavorite, + onLikeClick = onLikeClick + ) } } } @@ -131,7 +136,7 @@ internal fun FilmContentScreen( .offset( y = swipeableState.offset.value .coerceAtLeast(toolbarHeight.toFloat()) - .toDp - AppDimension.Padding.large * progress + .toDp - AppDimension.Padding.large * 2 * progress ) .clip( RoundedCornerShape( @@ -156,9 +161,7 @@ internal fun FilmContentScreen( ) ), title = film.title, - onBackClick = { - onAction(Action.BackButtonClick) - }, + onBackClick = onBackClick, textSize = MaterialTheme.typography.displayMedium .fontSize * ((1 - progress).coerceAtLeast(0.5f)) ) @@ -268,7 +271,9 @@ internal fun FilmBody( @Composable internal fun FilmActions( - modifier: Modifier = Modifier + onLikeClick: () -> Unit, + isLiked: Boolean, + modifier: Modifier = Modifier, ) { Column( modifier = modifier @@ -276,12 +281,19 @@ internal fun FilmActions( IconButton( modifier = Modifier .padding(AppDimension.Padding.smallest), - onClick = {} + onClick = onClick(onClick = onLikeClick) ) { - Icon( - imageVector = Icons.Default.Favorite, - contentDescription = "Back", - ) + if (isLiked) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = "Like", + ) + } else { + Icon( + imageVector = Icons.Default.FavoriteBorder, + contentDescription = "Like", + ) + } } IconButton( modifier = Modifier diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/model/FilmMapper.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/model/FilmMapper.kt index de636902..f795e6db 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/model/FilmMapper.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/model/FilmMapper.kt @@ -19,4 +19,22 @@ fun FilmDomain.toUi() = Film( type = type, trailer = trailer, isFavorite = isFavorite +) + +fun Film.toDomain(): FilmDomain = FilmDomain( + id = id, + title = title, + description = description, + poster = poster, + rating = rating, + duration = duration, + genres = genres.toImmutableList(), + actors = actors.toImmutableList(), + director = director, + country = country, + year = year, + age = age, + type = type, + trailer = trailer, + isFavorite = isFavorite ) \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmScreenState.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmScreenState.kt index 17976b82..32955d43 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmScreenState.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmScreenState.kt @@ -10,4 +10,7 @@ sealed interface FilmScreenState { data class Content(val data: Film) : FilmScreenState data object Loading : FilmScreenState + + val result: Film? + get() = (this as? Content)?.data } \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStore.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStore.kt index a56ce5f8..ee6c8d99 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStore.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStore.kt @@ -4,11 +4,13 @@ import com.stslex.core.core.AppDispatcher import com.stslex.core.ui.mvi.BaseStore import com.stslex.feature.film.domain.interactor.FilmInteractor import com.stslex.feature.film.navigation.FilmRouter +import com.stslex.feature.film.ui.model.toDomain import com.stslex.feature.film.ui.model.toUi import com.stslex.feature.film.ui.store.FilmStoreComponent.Action import com.stslex.feature.film.ui.store.FilmStoreComponent.Event import com.stslex.feature.film.ui.store.FilmStoreComponent.Navigation import com.stslex.feature.film.ui.store.FilmStoreComponent.State +import kotlinx.coroutines.Job class FilmStore( private val interactor: FilmInteractor, @@ -19,11 +21,33 @@ class FilmStore( appDispatcher = appDispatcher, initialState = State.INITIAL, ) { + private var likeJob: Job? = null override fun sendAction(action: Action) { when (action) { is Action.Init -> actionInit(action) is Action.BackButtonClick -> actionBackButtonClick() + is Action.LikeButtonClick -> actionLikeButtonClick() + } + } + + private fun actionLikeButtonClick() { + if (likeJob?.isActive == true) return + val film = state.value.screenState.result ?: return + + likeJob = launch( + onError = { + // TODO show toast error + }, + onSuccess = { + // TODO show toast success + } + ) { + if (film.isFavorite) { + interactor.dislikeFilm(film.id) + } else { + interactor.likeFilm(film.toDomain()) + } } } @@ -32,6 +56,12 @@ class FilmStore( } private fun actionInit(action: Action.Init) { + updateState { currentState -> + currentState.copy( + screenState = FilmScreenState.Loading, + filmId = action.id + ) + } interactor .getFilm(action.id) .launchFlow { film -> @@ -43,4 +73,3 @@ class FilmStore( } } } - diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStoreComponent.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStoreComponent.kt index 9fb86306..0456d733 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStoreComponent.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStoreComponent.kt @@ -7,11 +7,13 @@ interface FilmStoreComponent : Store { @Stable data class State( + val filmId: String, val screenState: FilmScreenState ) : Store.State { companion object { val INITIAL = State( + filmId = "", screenState = FilmScreenState.Loading ) } @@ -24,6 +26,8 @@ interface FilmStoreComponent : Store { data class Init(val id: String) : Action data object BackButtonClick : Action + + data object LikeButtonClick : Action } sealed interface Event : Store.Event diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d13173ca..b2fc6e1e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,14 @@ [versions] -kermit = "2.0.2" -logback = "1.4.11" -compose = "1.5.4" -compose-plugin = "1.5.10" -compose-compiler = "1.5.3" -agp = "8.1.2" android-minSdk = "24" android-compileSdk = "34" android-targetSdk = "34" -androidx-activityCompose = "1.8.0" + +logback = "1.4.11" +compose = "1.5.4" +compose-plugin = "1.5.11" +compose-compiler = "1.5.4" +agp = "8.1.4" +androidx-activityCompose = "1.8.1" androidx-core-ktx = "1.12.0" androidx-appcompat = "1.6.1" androidx-material = "1.10.0" @@ -20,14 +20,15 @@ junit = "4.13.2" koin = "3.4.3" koin-compose = "1.0.4" -voyagerVersion = "1.0.0-rc10" -ktor = "2.3.2" +ktor = "2.3.6" immutableCollection = "0.3.5" -kamel = "0.9.0" coroutines = "1.7.3" +voyagerVersion = "1.0.0-rc10" +kermit = "2.0.2" +kamel = "0.9.0" + [libraries] -kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -55,6 +56,10 @@ voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version. voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyagerVersion" } voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyagerVersion" } voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyagerVersion" } + +kamel = { module = "media.kamel:kamel-image", version.ref = "kamel" } +kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } + #todo check if this is needed logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } @@ -67,7 +72,6 @@ ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "k ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.0" } -kamel = { module = "media.kamel:kamel-image", version.ref = "kamel" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version = "1.7.9" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 2a84bab4..04dd357a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,4 +24,5 @@ include(":core:network") include(":core:ui") include(":feature:feed") include(":feature:film") -include(":feature:profile") \ No newline at end of file +include(":feature:profile") +include(":core:database")