From 0656058dcc8ffe025c79c24c2162187e86f82ea0 Mon Sep 17 00:00:00 2001 From: gabrielmoro Date: Tue, 16 Jun 2026 15:56:12 -0300 Subject: [PATCH 1/4] add core session management and integrate with ListStream --- build-logic/src/main/java/Config.kt | 2 +- composeApp/build.gradle.kts | 1 + .../composeApp/presentation/di/AppModule.kt | 2 ++ core-session/build.gradle.kts | 12 +++++++ .../core/session/di/CoreSessionModule.kt | 8 +++++ .../core/session/di/SessionScope.kt | 3 ++ .../core/session/domain/SessionManager.kt | 35 +++++++++++++++++++ .../core/session/domain/UserSessionInfo.kt | 8 +++++ feature-list-streams/build.gradle.kts | 1 + .../screen/ListStreamsScreenPreview.kt | 1 - .../liststreams/list/di/ListStreamModule.kt | 3 +- .../navigation/ListStreamsNavigation.kt | 7 +--- .../screens/ListStreamViewModel.kt | 9 +++-- .../presentation/screens/ListStreamsScreen.kt | 3 +- .../screens/ListStreamsUIState.kt | 3 +- feature-profile/build.gradle.kts | 1 + .../screens/ProfilePickerStreamViewModel.kt | 13 +++++-- gradle/libs.versions.toml | 2 ++ settings.gradle.kts | 1 + 19 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 core-session/build.gradle.kts create mode 100644 core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/di/CoreSessionModule.kt create mode 100644 core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/di/SessionScope.kt create mode 100644 core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/SessionManager.kt create mode 100644 core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/UserSessionInfo.kt diff --git a/build-logic/src/main/java/Config.kt b/build-logic/src/main/java/Config.kt index 2d4891cb..1d9ebfd0 100644 --- a/build-logic/src/main/java/Config.kt +++ b/build-logic/src/main/java/Config.kt @@ -10,7 +10,7 @@ object Config { const val compileSdkVersion = 36 const val minSdkVersion = 28 const val targetSdkVersion = 36 - const val versionName = "1.5" + const val versionName = "3.2" const val versionCode = 1 const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 5d3d0841..6a076714 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -24,6 +24,7 @@ kotlin { implementation(projects.featureProfile) implementation(projects.featureNews) implementation(projects.coreShared) + implementation(projects.coreSession) api(projects.coreSharedUi) api(projects.coreCameraGallery) api(projects.coreBackgroundWork) diff --git a/composeApp/src/commonMain/kotlin/com/codandotv/streamplayerapp/composeApp/presentation/di/AppModule.kt b/composeApp/src/commonMain/kotlin/com/codandotv/streamplayerapp/composeApp/presentation/di/AppModule.kt index 49ceb258..c19d3c95 100644 --- a/composeApp/src/commonMain/kotlin/com/codandotv/streamplayerapp/composeApp/presentation/di/AppModule.kt +++ b/composeApp/src/commonMain/kotlin/com/codandotv/streamplayerapp/composeApp/presentation/di/AppModule.kt @@ -3,6 +3,7 @@ package com.codandotv.streamplayerapp.composeApp.presentation.di import com.codandotv.streamplayerapp.core.background.work.di.SyncModule import com.codandotv.streamplayerapp.core.local.storage.di.LocalStorageModule import com.codandotv.streamplayerapp.core.networking.di.NetworkModule +import com.codandotv.streamplayerapp.core.session.di.coreSessionModule import com.codandotv.streamplayerapp.core.shared.qualifier.QualifierDispatcherIO import com.codandotv.streamplayerapp.feature.liststreams.list.di.ListStreamModule import com.codandotv.streamplayerapp.feature.news.di.NewsScreenModule @@ -28,6 +29,7 @@ fun streamPlayerApplication(platformBlock: KoinApplication.() -> Unit): KoinAppl Dispatchers.IO } }, + coreSessionModule, NetworkModule().module, LocalStorageModule.module, SyncModule.module, diff --git a/core-session/build.gradle.kts b/core-session/build.gradle.kts new file mode 100644 index 00000000..110485de --- /dev/null +++ b/core-session/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("com.streamplayer.kmp-library") + id("com.streamplayer.koin-annotations-setup") +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.datetime) + } + } +} \ No newline at end of file diff --git a/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/di/CoreSessionModule.kt b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/di/CoreSessionModule.kt new file mode 100644 index 00000000..84001e9a --- /dev/null +++ b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/di/CoreSessionModule.kt @@ -0,0 +1,8 @@ +package com.codandotv.streamplayerapp.core.session.di + +import com.codandotv.streamplayerapp.core.session.domain.SessionManager +import org.koin.dsl.module + +val coreSessionModule = module { + factory { SessionManager() } +} diff --git a/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/di/SessionScope.kt b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/di/SessionScope.kt new file mode 100644 index 00000000..d920c4c9 --- /dev/null +++ b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/di/SessionScope.kt @@ -0,0 +1,3 @@ +package com.codandotv.streamplayerapp.core.session.di + +class SessionScope diff --git a/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/SessionManager.kt b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/SessionManager.kt new file mode 100644 index 00000000..8c53785f --- /dev/null +++ b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/SessionManager.kt @@ -0,0 +1,35 @@ +package com.codandotv.streamplayerapp.core.session.domain + +import com.codandotv.streamplayerapp.core.session.di.SessionScope +import org.koin.mp.KoinPlatform +import kotlin.time.Clock + +class SessionManager { + private val scopeId: String = "user_session" + + fun openSession(imageUrl: String) { + closeSession() + val koin = KoinPlatform.getKoin() + + val scope = koin.createScope( + scopeId = scopeId, + ) + scope.declare( + UserSessionInfo( + userTimestamp = Clock.System.now(), + profileImageUrl = imageUrl + ) + ) + } + + fun userSessionInfo(): UserSessionInfo? { + return KoinPlatform.getKoin() + .getScopeOrNull(scopeId) + ?.getOrNull() + } + + fun closeSession() { + val koin = KoinPlatform.getKoin() + koin.getScopeOrNull(scopeId)?.close() + } +} \ No newline at end of file diff --git a/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/UserSessionInfo.kt b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/UserSessionInfo.kt new file mode 100644 index 00000000..151bef3c --- /dev/null +++ b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/UserSessionInfo.kt @@ -0,0 +1,8 @@ +package com.codandotv.streamplayerapp.core.session.domain + +import kotlin.time.Instant + +data class UserSessionInfo( + val userTimestamp: Instant, + val profileImageUrl: String, +) \ No newline at end of file diff --git a/feature-list-streams/build.gradle.kts b/feature-list-streams/build.gradle.kts index d12b7e74..84f58171 100644 --- a/feature-list-streams/build.gradle.kts +++ b/feature-list-streams/build.gradle.kts @@ -14,6 +14,7 @@ kotlin { implementation(projects.coreShared) implementation(projects.coreSharedUi) implementation(projects.coreLocalStorage) + implementation(projects.coreSession) implementation(libs.bundles.compose) implementation(libs.paging.compose) diff --git a/feature-list-streams/src/androidMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/presentation/screen/ListStreamsScreenPreview.kt b/feature-list-streams/src/androidMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/presentation/screen/ListStreamsScreenPreview.kt index 12cd5539..d2c900b6 100644 --- a/feature-list-streams/src/androidMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/presentation/screen/ListStreamsScreenPreview.kt +++ b/feature-list-streams/src/androidMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/presentation/screen/ListStreamsScreenPreview.kt @@ -9,6 +9,5 @@ import com.codandotv.streamplayerapp.core.shared.ui.theme.ThemePreviews fun ListStreamsScreenPreview() { com.codandotv.streamplayerapp.feature.liststreams.list.presentation.screens.ListStreamsScreen( navController = rememberNavController(), - profilePicture = "" ) } diff --git a/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/di/ListStreamModule.kt b/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/di/ListStreamModule.kt index 3a97de4b..4b00a934 100644 --- a/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/di/ListStreamModule.kt +++ b/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/di/ListStreamModule.kt @@ -22,7 +22,8 @@ object ListStreamModule { ListStreamViewModel( listStreams = get(), listGenres = get(), - latestStream = get() + latestStream = get(), + sessionManager = get(), ) } diff --git a/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/navigation/ListStreamsNavigation.kt b/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/navigation/ListStreamsNavigation.kt index b2d3c6a4..4356a5a7 100644 --- a/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/navigation/ListStreamsNavigation.kt +++ b/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/navigation/ListStreamsNavigation.kt @@ -4,7 +4,6 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import com.codandotv.streamplayerapp.core.navigation.routes.BottomNavRoutes.HOME_COMPLETE -import com.codandotv.streamplayerapp.core.navigation.routes.BottomNavRoutes.PARAM.PROFILE_ID import com.codandotv.streamplayerapp.core.navigation.routes.Routes import com.codandotv.streamplayerapp.core.navigation.routes.Routes.DETAIL import com.codandotv.streamplayerapp.core.navigation.routes.Routes.PROFILE_PICKER @@ -13,11 +12,9 @@ import com.codandotv.streamplayerapp.feature.liststreams.list.presentation.scree import org.koin.compose.module.rememberKoinModules import org.koin.core.annotation.KoinExperimentalAPI -internal const val DEFAULT_ID = "" - @OptIn(KoinExperimentalAPI::class) fun NavGraphBuilder.listStreamsNavGraph(navController: NavHostController) { - composable(HOME_COMPLETE) { nav -> + composable(HOME_COMPLETE) { rememberKoinModules { listOf(ListStreamModule.module) } @@ -33,8 +30,6 @@ fun NavGraphBuilder.listStreamsNavGraph(navController: NavHostController) { onNavigateSearchScreen = { navController.navigate(Routes.SEARCH) }, - profilePicture = nav.savedStateHandle.get(PROFILE_ID) - ?: DEFAULT_ID ) } } diff --git a/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamViewModel.kt b/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamViewModel.kt index b5665b61..b23ef3aa 100644 --- a/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamViewModel.kt +++ b/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn import androidx.paging.map import com.codandotv.streamplayerapp.core.networking.handleError.catchFailure +import com.codandotv.streamplayerapp.core.session.domain.SessionManager import com.codandotv.streamplayerapp.core.shared.ui.widget.StreamsCardContent import com.codandotv.streamplayerapp.core.shared.ui.widget.StreamsCarouselContent import com.codandotv.streamplayerapp.feature.liststreams.core.ContentType @@ -41,13 +42,17 @@ import streamplayerapp_kmp.core_shared_ui.generated.resources.Res as SharedRes class ListStreamViewModel( private val listStreams: ListStreamUseCase, private val listGenres: GetGenresUseCase, - private val latestStream: GetTopRatedStream + private val latestStream: GetTopRatedStream, + private val sessionManager: SessionManager, ) : ViewModel() { private val _uiState = MutableStateFlow( ListStreamsUIState( streamsCarouselContent = emptyList(), - isLoading = false + isLoading = false, + profilePictureUrl = sessionManager + .userSessionInfo() + ?.profileImageUrl.orEmpty() ) ) val uiState = _uiState.stateIn( diff --git a/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamsScreen.kt b/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamsScreen.kt index a4506ae1..0cc0bfee 100644 --- a/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamsScreen.kt +++ b/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamsScreen.kt @@ -39,7 +39,6 @@ fun ListStreamsScreen( onNavigateDetailList: (String) -> Unit = {}, onNavigateProfilePicker: () -> Unit = {}, onNavigateSearchScreen: () -> Unit = {}, - profilePicture: String ) { val uiState by viewModel.uiState.collectAsState() val scrollBehavior = @@ -54,7 +53,7 @@ fun ListStreamsScreen( scrollBehavior = scrollBehavior, onNavigateProfilePicker = onNavigateProfilePicker, onNavigateSearchScreen = onNavigateSearchScreen, - onSelectedProfilePicture = profilePicture + onSelectedProfilePicture = uiState.profilePictureUrl ) }, bottomBar = { diff --git a/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamsUIState.kt b/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamsUIState.kt index 9c4eedd0..455569c4 100644 --- a/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamsUIState.kt +++ b/feature-list-streams/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/liststreams/list/presentation/screens/ListStreamsUIState.kt @@ -6,5 +6,6 @@ import com.codandotv.streamplayerapp.feature.liststreams.list.domain.model.Highl data class ListStreamsUIState( val highlightBanner: HighlightBanner? = null, val streamsCarouselContent: List, - val isLoading: Boolean + val isLoading: Boolean, + val profilePictureUrl: String ) diff --git a/feature-profile/build.gradle.kts b/feature-profile/build.gradle.kts index 36b4c3b0..b6eafeee 100644 --- a/feature-profile/build.gradle.kts +++ b/feature-profile/build.gradle.kts @@ -10,6 +10,7 @@ plugins { kotlin { sourceSets { commonMain.dependencies { + implementation(projects.coreSession) implementation(projects.coreNetworking) implementation(projects.coreNavigation) implementation(projects.coreShared) diff --git a/feature-profile/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/profile/presentation/screens/ProfilePickerStreamViewModel.kt b/feature-profile/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/profile/presentation/screens/ProfilePickerStreamViewModel.kt index ea57880f..43024dca 100644 --- a/feature-profile/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/profile/presentation/screens/ProfilePickerStreamViewModel.kt +++ b/feature-profile/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/profile/presentation/screens/ProfilePickerStreamViewModel.kt @@ -3,6 +3,9 @@ package com.codandotv.streamplayerapp.feature.profile.presentation.screens import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.codandotv.streamplayerapp.core.networking.handleError.catchFailure +import com.codandotv.streamplayerapp.core.session.domain.SessionManager +import com.codandotv.streamplayerapp.feature.profile.domain.ProfilePickerStreamUseCase +import com.codandotv.streamplayerapp.feature.profile.domain.ProfileStream import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -10,10 +13,12 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel +import org.koin.core.annotation.Provided @KoinViewModel class ProfilePickerStreamViewModel( - private val useCase: com.codandotv.streamplayerapp.feature.profile.domain.ProfilePickerStreamUseCase, + @Provided private val sessionManager: SessionManager, + private val useCase: ProfilePickerStreamUseCase, ) : ViewModel() { private val _uiState = MutableStateFlow(ProfilePickerStreamsUIState()) @@ -105,7 +110,7 @@ class ProfilePickerStreamViewModel( } @Suppress("MagicNumber") - fun moveSelectedProfileToCenterImage(profile: com.codandotv.streamplayerapp.feature.profile.domain.ProfileStream) { + fun moveSelectedProfileToCenterImage(profile: ProfileStream) { viewModelScope.launch { with(_uiState.value) { // move hide image to the position of the clicked item @@ -132,6 +137,10 @@ class ProfilePickerStreamViewModel( canMoveImageToCenter = !canMoveImageToCenter, expandImage = !expandImage ) + + viewModelScope.launch { + sessionManager.openSession(profile.imageUrl) + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d6966080..dd61ebc5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ koin-annotations = "2.3.1" ksp = "2.3.5" kotzilla = "2.1.3" lifecycle-viewmodel-compose-version = "2.9.6" +kotlinx-datetime = "0.8.0" dokka = "1.9.10" detekt = "1.23.6" @@ -57,6 +58,7 @@ firebase-bom = "33.14.0" firebase-crashlytics = "3.0.3" [libraries] +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } kotzilla-sdk-compose = { group = "io.kotzilla", name = "kotzilla-sdk-compose", version.ref = "kotzilla" } kotlin_gradle_plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } android_gradle_plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "android_gradle_plugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index f3bfb329..4b588396 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,6 +39,7 @@ include(":feature-news") include(":core-camera-gallery") include(":core-permission") include(":core-background-work") +include(":core-session") kover { enableCoverage() From 82c5d2146b4dcc49b0b52e85663791809c1a378e Mon Sep 17 00:00:00 2001 From: gabrielmoro Date: Tue, 16 Jun 2026 20:35:50 -0300 Subject: [PATCH 2/4] add AvatarImageIcon component and integrate profile picture in Search UI --- .../core/shared/ui/widget/AvatarImageIcon.kt | 27 +++++++++++++++++++ .../shared/ui/widget/StreamPlayerTopBar.kt | 10 ++----- feature-search/build.gradle.kts | 1 + .../widgets/SearchStreamsPreview.kt | 1 + .../presentation/screens/SearchScreen.kt | 1 + .../presentation/screens/SearchUIState.kt | 21 +++++++++++---- .../presentation/screens/SearchViewModel.kt | 26 ++++++++++++++---- .../presentation/widgets/SearchStreams.kt | 20 +++++--------- 8 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/AvatarImageIcon.kt diff --git a/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/AvatarImageIcon.kt b/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/AvatarImageIcon.kt new file mode 100644 index 00000000..921e10ff --- /dev/null +++ b/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/AvatarImageIcon.kt @@ -0,0 +1,27 @@ +package com.codandotv.streamplayerapp.core.shared.ui.widget + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import streamplayerapp_kmp.core_shared_ui.generated.resources.Res +import streamplayerapp_kmp.core_shared_ui.generated.resources.icon_profile +import streamplayerapp_kmp.core_shared_ui.generated.resources.perfil_fake + +@Composable +fun AvatarImageIcon(imageUrl: String?, modifier: Modifier = Modifier) { + AsyncImage( + modifier = modifier + .height(24.dp) + .clip(RoundedCornerShape(4.dp)), + model = imageUrl, + error = painterResource(Res.drawable.perfil_fake), + placeholder = painterResource(Res.drawable.perfil_fake), + contentDescription = stringResource(Res.string.icon_profile) + ) +} \ No newline at end of file diff --git a/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/StreamPlayerTopBar.kt b/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/StreamPlayerTopBar.kt index bc29940f..ed0cf6a2 100644 --- a/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/StreamPlayerTopBar.kt +++ b/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/StreamPlayerTopBar.kt @@ -104,14 +104,8 @@ private fun StreamPlayerTopBar( modifier = Modifier.fillMaxHeight(), onClick = { onNavigateProfilePicker() } ) { - AsyncImage( - modifier = Modifier - .height(24.dp) - .clip(RoundedCornerShape(4.dp)), - model = profilePicture, - error = painterResource(Res.drawable.perfil_fake), - placeholder = painterResource(Res.drawable.perfil_fake), - contentDescription = stringResource(Res.string.icon_profile) + AvatarImageIcon( + imageUrl = profilePicture, ) } } diff --git a/feature-search/build.gradle.kts b/feature-search/build.gradle.kts index 05d5b8e0..be0e364d 100644 --- a/feature-search/build.gradle.kts +++ b/feature-search/build.gradle.kts @@ -14,6 +14,7 @@ kotlin { implementation(projects.coreNetworking) implementation(projects.coreNavigation) + implementation(projects.coreSession) implementation(projects.coreShared) implementation(projects.coreSharedUi) implementation(projects.coreLocalStorage) diff --git a/feature-search/src/androidMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/widgets/SearchStreamsPreview.kt b/feature-search/src/androidMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/widgets/SearchStreamsPreview.kt index 536b1fa7..7591611b 100644 --- a/feature-search/src/androidMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/widgets/SearchStreamsPreview.kt +++ b/feature-search/src/androidMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/widgets/SearchStreamsPreview.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.tooling.preview.Preview @Preview fun SearchBarPreview() { StreamPlayerTopBar( + profilePictureUrl = null, onBackPressed = {} ) } diff --git a/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchScreen.kt b/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchScreen.kt index 4a50b431..5e7c9d34 100644 --- a/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchScreen.kt +++ b/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchScreen.kt @@ -89,6 +89,7 @@ private fun SetupSearchScreen( SearchableTopBar( modifier = Modifier.statusBarsPadding(), currentSearchText = currentText, + profilePictureUrl = uiState.profilePictureUrl, onSearchTextChanged = { value -> viewModel.setCurrentSearchText( newText = value diff --git a/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchUIState.kt b/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchUIState.kt index 8b60b6ba..0a9d7b85 100644 --- a/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchUIState.kt +++ b/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchUIState.kt @@ -2,9 +2,20 @@ package com.codandotv.streamplayerapp.feature.search.presentation.screens import com.codandotv.streamplayerapp.feature.search.data.model.ListSearchStreamResponse -sealed class SearchUIState { - data class Success(val listCharacters: ListSearchStreamResponse) : SearchUIState() - data class Error(val messageError: String = "") : SearchUIState() - object Loading : SearchUIState() - object Empty : SearchUIState() +sealed class SearchUIState( + open val profilePictureUrl: String? +) { + + data class Success( + val listCharacters: ListSearchStreamResponse, + override val profilePictureUrl: String? + ) : SearchUIState(profilePictureUrl) + + data class Error( + val messageError: String = "", + override val profilePictureUrl: String? + ) : SearchUIState(profilePictureUrl) + + object Loading : SearchUIState(null) + object Empty : SearchUIState(null) } diff --git a/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchViewModel.kt b/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchViewModel.kt index 288633d4..ddb4d993 100644 --- a/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchViewModel.kt +++ b/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/screens/SearchViewModel.kt @@ -3,6 +3,9 @@ package com.codandotv.streamplayerapp.feature.search.presentation.screens import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.codandotv.streamplayerapp.core.networking.handleError.catchFailure +import com.codandotv.streamplayerapp.core.session.domain.SessionManager +import com.codandotv.streamplayerapp.feature.search.domain.MostPopularMoviesUseCase +import com.codandotv.streamplayerapp.feature.search.domain.SearchUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -12,11 +15,13 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel +import org.koin.core.annotation.Provided @KoinViewModel class SearchViewModel( - private val searchUseCase: com.codandotv.streamplayerapp.feature.search.domain.SearchUseCase, - private val mostPopularMoviesUseCase: com.codandotv.streamplayerapp.feature.search.domain.MostPopularMoviesUseCase + private val searchUseCase: SearchUseCase, + private val mostPopularMoviesUseCase: MostPopularMoviesUseCase, + @Provided private val sessionManager: SessionManager ) : ViewModel() { private var tryAgain: () -> Unit = {} @@ -45,6 +50,8 @@ class SearchViewModel( } } + private fun userProfilePictureUrl() = sessionManager.userSessionInfo()?.profileImageUrl + private fun fetchMovieByQuery() { tryAgain = ::fetchMovieByQuery @@ -60,7 +67,10 @@ class SearchViewModel( if (result.results.isEmpty()) { SearchUIState.Empty } else { - SearchUIState.Success(result) + SearchUIState.Success( + profilePictureUrl = userProfilePictureUrl(), + listCharacters = result + ) } } } @@ -80,7 +90,10 @@ class SearchViewModel( if (result.results.isEmpty()) { SearchUIState.Empty } else { - SearchUIState.Success(result) + SearchUIState.Success( + profilePictureUrl = userProfilePictureUrl(), + listCharacters = result + ) } } } @@ -89,7 +102,10 @@ class SearchViewModel( private fun showError(messageError: String) { _uiState.update { - SearchUIState.Error(messageError = messageError) + SearchUIState.Error( + messageError = messageError, + profilePictureUrl = userProfilePictureUrl(), + ) } } diff --git a/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/widgets/SearchStreams.kt b/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/widgets/SearchStreams.kt index 0c3e30bd..ab31de77 100644 --- a/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/widgets/SearchStreams.kt +++ b/feature-search/src/commonMain/kotlin/com/codandotv/streamplayerapp/feature/search/presentation/widgets/SearchStreams.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons @@ -22,21 +21,18 @@ import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import com.codandotv.streamplayerapp.core.shared.ui.resources.Colors +import com.codandotv.streamplayerapp.core.shared.ui.widget.AvatarImageIcon import com.codandotv.streamplayerapp.core.shared.ui.widget.CloseButton import com.codandotv.streamplayerapp.core.shared.ui.widget.MicButton import com.codandotv.streamplayerapp.core.shared.ui.widget.SearchIcon -import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import streamplayerapp_kmp.core_shared_ui.generated.resources.icon_back import streamplayerapp_kmp.core_shared_ui.generated.resources.icon_cast -import streamplayerapp_kmp.core_shared_ui.generated.resources.icon_profile -import streamplayerapp_kmp.core_shared_ui.generated.resources.perfil_fake import streamplayerapp_kmp.feature_search.generated.resources.Res import streamplayerapp_kmp.feature_search.generated.resources.search_list_main_search import streamplayerapp_kmp.core_shared_ui.generated.resources.Res as SharedRes @@ -45,6 +41,7 @@ import streamplayerapp_kmp.core_shared_ui.generated.resources.Res as SharedRes @Composable fun SearchableTopBar( currentSearchText: String, + profilePictureUrl: String?, onSearchTextChanged: (String) -> Unit, onSearchDispatched: () -> Unit, onSearchIconPressed: () -> Unit, @@ -54,7 +51,8 @@ fun SearchableTopBar( ) { Column(modifier = modifier) { StreamPlayerTopBar( - onBackPressed = onBackPressed + onBackPressed = onBackPressed, + profilePictureUrl = profilePictureUrl, ) SearchTopBar( currentSearchText = currentSearchText, @@ -68,6 +66,7 @@ fun SearchableTopBar( @Composable internal fun StreamPlayerTopBar( + profilePictureUrl: String?, onBackPressed: () -> Unit ) { Row( @@ -103,13 +102,8 @@ internal fun StreamPlayerTopBar( modifier = Modifier.fillMaxHeight(), onClick = { /* todo */ } ) { - Icon( - modifier = Modifier - .height(24.dp) - .clip(RoundedCornerShape(4.dp)), - painter = painterResource(SharedRes.drawable.perfil_fake), - contentDescription = stringResource(SharedRes.string.icon_profile), - tint = Color.Unspecified, + AvatarImageIcon( + imageUrl = profilePictureUrl, ) } } From 9470bafd4350d235d3685abc86c663319ebf4026 Mon Sep 17 00:00:00 2001 From: gabrielmoro Date: Tue, 16 Jun 2026 20:39:23 -0300 Subject: [PATCH 3/4] fix: add missing newlines at the end of several files --- .../streamplayerapp/core/session/domain/SessionManager.kt | 2 +- .../streamplayerapp/core/session/domain/UserSessionInfo.kt | 2 +- .../streamplayerapp/core/shared/ui/widget/AvatarImageIcon.kt | 2 +- .../core/shared/ui/widget/StreamPlayerTopBar.kt | 5 ----- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/SessionManager.kt b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/SessionManager.kt index 8c53785f..e9a31651 100644 --- a/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/SessionManager.kt +++ b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/SessionManager.kt @@ -32,4 +32,4 @@ class SessionManager { val koin = KoinPlatform.getKoin() koin.getScopeOrNull(scopeId)?.close() } -} \ No newline at end of file +} diff --git a/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/UserSessionInfo.kt b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/UserSessionInfo.kt index 151bef3c..3f8ec09b 100644 --- a/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/UserSessionInfo.kt +++ b/core-session/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/session/domain/UserSessionInfo.kt @@ -5,4 +5,4 @@ import kotlin.time.Instant data class UserSessionInfo( val userTimestamp: Instant, val profileImageUrl: String, -) \ No newline at end of file +) diff --git a/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/AvatarImageIcon.kt b/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/AvatarImageIcon.kt index 921e10ff..30b91cfa 100644 --- a/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/AvatarImageIcon.kt +++ b/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/AvatarImageIcon.kt @@ -24,4 +24,4 @@ fun AvatarImageIcon(imageUrl: String?, modifier: Modifier = Modifier) { placeholder = painterResource(Res.drawable.perfil_fake), contentDescription = stringResource(Res.string.icon_profile) ) -} \ No newline at end of file +} diff --git a/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/StreamPlayerTopBar.kt b/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/StreamPlayerTopBar.kt index ed0cf6a2..f8148fae 100644 --- a/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/StreamPlayerTopBar.kt +++ b/core-shared-ui/src/commonMain/kotlin/com/codandotv/streamplayerapp/core/shared/ui/widget/StreamPlayerTopBar.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api @@ -22,19 +21,15 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage import com.codandotv.streamplayerapp.core.shared.ui.resources.Colors import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import streamplayerapp_kmp.core_shared_ui.generated.resources.Res import streamplayerapp_kmp.core_shared_ui.generated.resources.ic_netflix import streamplayerapp_kmp.core_shared_ui.generated.resources.icon_netflix -import streamplayerapp_kmp.core_shared_ui.generated.resources.icon_profile import streamplayerapp_kmp.core_shared_ui.generated.resources.icon_search -import streamplayerapp_kmp.core_shared_ui.generated.resources.perfil_fake import streamplayerapp_kmp.core_shared_ui.generated.resources.topbar_categories import streamplayerapp_kmp.core_shared_ui.generated.resources.topbar_movies import streamplayerapp_kmp.core_shared_ui.generated.resources.topbar_shows From 73c4dd3e1832777334510063f53e92717141e720 Mon Sep 17 00:00:00 2001 From: gabrielmoro Date: Tue, 16 Jun 2026 20:56:16 -0300 Subject: [PATCH 4/4] add: create AGENTS.md, opencode.json, and SKILL.md for project documentation and configuration --- .opencode/skills/open-pr/SKILL.md | 96 ++++++++++++++++++++++++ AGENTS.md | 121 ++++++++++++++++++++++++++++++ opencode.json | 7 ++ 3 files changed, 224 insertions(+) create mode 100644 .opencode/skills/open-pr/SKILL.md create mode 100644 AGENTS.md create mode 100644 opencode.json diff --git a/.opencode/skills/open-pr/SKILL.md b/.opencode/skills/open-pr/SKILL.md new file mode 100644 index 00000000..f1b36174 --- /dev/null +++ b/.opencode/skills/open-pr/SKILL.md @@ -0,0 +1,96 @@ +--- +name: open-pr +description: Open a Pull Request comparing current feature branch against master, generating a pt-br description +--- + +# open-pr + +Open a Pull Request on GitHub comparing the current branch against `master`, generating a description in **pt-br**. + +## Behavior + +1. **Detect current branch** — run `git rev-parse --abbrev-ref HEAD` to get the feature branch name. + +2. **Fetch latest `master`** — run `git fetch origin master`. + +3. **Analyze changes** — compare HEAD against `master`: + + ```bash + git log master..HEAD --oneline --no-decorate + git diff master...HEAD --stat + ``` + +4. **Categorize commits** by conventional commit type: + + | Type | Label | + |---|---| + | `feat` | Nova funcionalidade | + | `fix` | Correção | + | `refactor` | Refatoração | + | `docs` | Documentação | + | `test` | Testes | + | `chore` | Manutenção | + | `build` | Build / CI | + | `BREAKING` | Mudança crítica | + +5. **Identify affected modules** — from changed file paths, determine which modules were modified: + - `composeApp` + - `feature-*` + - `core-*` + - `build-logic` + - `androidApp` + - `gradle` / config + +6. **Generate PR body in pt-br** with the following structure: + + ```markdown + ## O que foi feito? + + [Resumo das alterações baseado nos commits] + + ## Módulos afetados + + - [lista de módulos] + + ## Tipo de mudança + + - [ ] Nova funcionalidade + - [ ] Correção de bug + - [ ] Refatoração + - [ ] Documentação + - [ ] Testes + - [ ] Build / CI + + ## Como testar? + + 1. [passos para testar] + 2. [passos adicionais] + + ## Checklist + + - [ ] Build passa localmente + - [ ] Lint / Detekt não aponta erros novos + - [ ] Testes existentes continuam passando + - [ ] Testes novos foram adicionados (se aplicável) + ``` + +7. **Determine PR title** — use the first commit subject line or a concise summary derived from the branch name (e.g. `feat: adiciona tela de perfil`). + +8. **Open the PR** using `gh`: + + ```bash + gh pr create \ + --base master \ + --head \ + --title "" \ + --body "<body>" + ``` + +9. If `gh` is not authenticated or available, print the full PR content to stdout with instructions. + +## Notes + +- All PR text MUST be in **pt-br** (Portuguese — Brazil). +- Follow the project's existing commit style (conventional commits). +- If the branch has only one commit, use its message as the PR title. +- If the PR would be empty (no diff), warn the user. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..9871a021 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,121 @@ +# StreamPlayerApp — KMP + +## Project Overview + +- **Purpose:** Open-source Netflix-like app built by the CodandoTV community for learning and mentoring. +- **Type:** Kotlin Multiplatform (KMP) Application +- **Primary Language:** Kotlin 2.3.20 +- **Runtime/Platform:** Multiplatform — Android, iOS, Desktop (Compose Multiplatform) +- **Min SDK:** 28 (Android) | **Compile SDK:** 36 | **Target SDK:** 36 + +## Build System + +| Item | Value | +|---|---| +| Gradle | 9.3.1 | +| Kotlin | 2.3.20 | +| AGP | 9.1.0 | +| JDK | 21 | +| Version Catalog | `gradle/libs.versions.toml` | +| Convention Plugins | `build-logic/` (precompiled script plugins) | +| Application ID | `com.codandotv.streamplayerapp` | + +### Convention Plugins (build-logic) + +- `com.streamplayer.kmp-library` — base KMP module setup (multiplatform, serialization, parcelize, dokka, detekt) +- `com.streamplayer.dokka` — documentation generation +- `com.streamplayer.detekt` — static analysis +- `com.streamplayer.koin-annotations-setup` — Koin annotation processing +- `popcorngp-setup-plugin` — Popcorn Guinea Pig test setup + +### Key Dependencies + +- Compose Multiplatform 1.10.0 +- Koin 4.2.0 / Koin Annotations 2.3.1 +- Ktor 3.0.1 / OkHttp 4.12.0 +- Room 2.7.0-alpha13 +- Coil 3.1.0 +- Navigation Compose 2.9.2 +- Kotlinx Datetime 0.8.0 +- Firebase (BOM 33.14.0, Crashlytics, Analytics) +- Detekt 1.23.6 +- Dokka 1.9.10 + +## Project Structure + +### Main Application + +| Module | Type | Description | +|---|---|---| +| `composeApp` | KMP + Compose | Main UI module, aggregates features | +| `androidApp` | Android App | Android entry point wrapper | + +### Feature Modules + +| Module | Description | +|---|---| +| `feature-list-streams` | Stream listing | +| `feature-detail` | Detail screen | +| `feature-search` | Search functionality | +| `feature-profile` | User profile | +| `feature-news` | News feed | + +### Core Modules + +| Module | Description | +|---|---| +| `core-shared` | Shared domain models and utilities | +| `core-networking` | Networking layer (Ktor + OkHttp) | +| `core-shared-ui` | Shared UI components and theming | +| `core-navigation` | Navigation infrastructure | +| `core-local-storage` | Local persistence (Room) | +| `core-camera-gallery` | Camera and gallery features | +| `core-permission` | Permission handling (MOKO Permissions) | +| `core-background-work` | Background task management (WorkManager, KMPNotifier) | +| `core-session` | Session management | + +### Build Infrastructure + +| Module | Description | +|---|---| +| `build-logic` | Convention plugins and shared build config | +| `Config.kt` | Centralized version and configuration constants | + +### Test Infrastructure + +- JUnit 4.13.2 +- MockK 1.13.7 +- Kotlinx Coroutines Test 1.8.1 +- Koin Test +- Popcorn Guinea Pig (test fixtures) + +### Documentation + +- **Generator:** Dokka 1.9.10 +- **Source:** API docs generated via `dokka` task +- **Project docs:** README.md, README_pt-br.md + +## Version Management + +Versions are managed in: + +| File | Content | +|---|---| +| `gradle/libs.versions.toml` | Dependency and plugin versions | +| `build-logic/src/main/java/Config.kt` | `versionName = "3.2"`, `versionCode`, SDK versions, JDK target | +| `gradle.properties` | Gradle daemon config, AndroidX settings | + +## Distribution + +This is an application project and is **not** published as a library. No Maven Central, Gradle Plugin Portal, or pub.dev distribution. + +## Git Workflow + +| Item | Value | +|---|---| +| Default Branch | `main` | +| CI | GitHub Actions (`build.yml`, `linter.yaml`, `popcorn.yaml`) | +| Versioning | SemVer (current: 3.2) | +| Tags | Standard Git tags | +| PR Language | pt-br | +| Review | Via GitHub PRs, CODEOWNERS in `.github/CODEOWNERS` | diff --git a/opencode.json b/opencode.json new file mode 100644 index 00000000..61966d80 --- /dev/null +++ b/opencode.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://opencode.ai/config.json", + "instructions": ["AGENTS.md"], + "skills": { + "paths": [".opencode/skills"] + } +}