diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/AndroidSettingsDataSource.kt b/app/src/main/java/com/fluxa/app/ui/catalog/AndroidSettingsDataSource.kt index 3788c02..662bf72 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/AndroidSettingsDataSource.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/AndroidSettingsDataSource.kt @@ -173,9 +173,12 @@ class AndroidSettingsDataSource( appearance = SettingsAppearanceUiModel( accentColorArgb = profile.safeAccentColorArgb.toLong() and 0xffffffffL, amoledMode = profile.safeAmoledMode, - animationsEnabled = profile.safeAnimationsEnabled + animationsEnabled = profile.safeAnimationsEnabled, + floatingBottomBar = profile.floatingBottomBar ?: false, + bottomBarLabels = profile.bottomBarLabels ?: false ), appearanceHome = SettingsAppearanceHomeUiModel( + topBarEnabled = profile.homeTopBarEnabled ?: true, cardCornerPreset = profile.safeCardCornerPreset, interfaceDensity = profile.safeInterfaceDensity, posterWidthPreset = profile.safePosterWidthPreset, @@ -290,12 +293,15 @@ class AndroidSettingsDataSource( accentColorArgb = value.accentColorArgb.toInt(), amoledMode = value.amoledMode, appTheme = if (value.amoledMode) "dark" else it.appTheme, - animationsEnabled = value.animationsEnabled + animationsEnabled = value.animationsEnabled, + floatingBottomBar = value.floatingBottomBar, + bottomBarLabels = value.bottomBarLabels ) } override suspend fun updateAppearanceHome(value: SettingsAppearanceHomeUiModel) = update { it.copy( + homeTopBarEnabled = value.topBarEnabled, cardCornerPreset = value.cardCornerPreset, interfaceDensity = value.interfaceDensity, posterWidthPreset = value.posterWidthPreset, diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt index 0ee42e9..5151b7d 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt @@ -485,6 +485,12 @@ class HomeViewModel @Inject constructor( } } + fun addToWatchlist(meta: Meta) { + viewModelScope.launch { + if (!watchlistManager.isInWatchlist(meta.id)) toggleWatchlist(meta) + } + } + fun toggleBillboardWatchlist() { val movie = billboardState.movieValue ?: return viewModelScope.launch { diff --git a/app/src/main/java/com/fluxa/app/ui/routes/AppRoutesHost.kt b/app/src/main/java/com/fluxa/app/ui/routes/AppRoutesHost.kt index e79323d..4781f55 100644 --- a/app/src/main/java/com/fluxa/app/ui/routes/AppRoutesHost.kt +++ b/app/src/main/java/com/fluxa/app/ui/routes/AppRoutesHost.kt @@ -133,6 +133,11 @@ internal fun AppRoutesHost( .resolveMeta(action.item.id, action.item.type) ?.let(homeViewModel::forgetPlaybackProgress) } + is com.fluxa.app.shared.feature.catalog.CatalogAction.AddToLibraryRequested -> { + androidFluxaPlatformServices!!.catalogHomeDataSource + .resolveMeta(action.item.id, action.item.type) + ?.let(homeViewModel::addToWatchlist) + } else -> Unit } }, diff --git a/core/src/commonMain/resources/i18n/english_us.json b/core/src/commonMain/resources/i18n/english_us.json index 3e37c08..2434f7a 100644 --- a/core/src/commonMain/resources/i18n/english_us.json +++ b/core/src/commonMain/resources/i18n/english_us.json @@ -1091,5 +1091,12 @@ "settings.season_posters_on_hero": "Season Posters on Hero", "settings.detail_season_posters_on_hero_desc": "Use season posters in the detail page hero.", "settings.home_season_posters_on_hero_desc": "Use season posters in the home hero banner.", - "format.season_short": "S%s" + "format.season_short": "S%s", + "settings.floating_bottom_bar": "Floating bottom bar", + "settings.floating_bottom_bar_desc": "Lift the navigation bar above the bottom edge.", + "settings.bottom_bar_labels": "Show bottom bar labels", + "settings.bottom_bar_labels_desc": "Display destination names below navigation icons.", + "settings.home_top_bar": "Home top bar", + "settings.home_top_bar_desc": "Show Home, Series, Movies, notifications, and profile above the hero.", + "common.add_to_library": "Add to Library" } diff --git a/core/src/commonMain/resources/i18n/tr_tr.json b/core/src/commonMain/resources/i18n/tr_tr.json index 46e93f9..3b53977 100644 --- a/core/src/commonMain/resources/i18n/tr_tr.json +++ b/core/src/commonMain/resources/i18n/tr_tr.json @@ -1091,5 +1091,12 @@ "settings.season_posters_on_hero": "Hero'da Sezon Afişleri", "settings.detail_season_posters_on_hero_desc": "Detay sayfası hero alanında sezon afişlerini kullanır.", "settings.home_season_posters_on_hero_desc": "Ana sayfa hero bannerında sezon afişlerini kullanır.", - "format.season_short": "S%s" + "format.season_short": "S%s", + "settings.floating_bottom_bar": "Yüzen alt çubuk", + "settings.floating_bottom_bar_desc": "Gezinme çubuğunu alt kenarın üzerinde gösterir.", + "settings.bottom_bar_labels": "Alt çubuk etiketlerini göster", + "settings.bottom_bar_labels_desc": "Gezinme simgelerinin altında hedef adlarını gösterir.", + "settings.home_top_bar": "Ana sayfa üst çubuğu", + "settings.home_top_bar_desc": "Hero alanının üzerinde Ana Sayfa, Diziler, Filmler, bildirimler ve profili gösterir.", + "common.add_to_library": "Kitaplığa ekle" } diff --git a/data/src/commonMain/kotlin/com/fluxa/app/data/local/UserProfile.kt b/data/src/commonMain/kotlin/com/fluxa/app/data/local/UserProfile.kt index 8a4f83b..7dbd714 100644 --- a/data/src/commonMain/kotlin/com/fluxa/app/data/local/UserProfile.kt +++ b/data/src/commonMain/kotlin/com/fluxa/app/data/local/UserProfile.kt @@ -121,6 +121,9 @@ data class UserProfile( val detailSeasonPostersOnHero: Boolean? = true, val homeSeasonPostersOnHero: Boolean? = true, val animationsEnabled: Boolean? = true, + val floatingBottomBar: Boolean? = false, + val bottomBarLabels: Boolean? = false, + val homeTopBarEnabled: Boolean? = true, val reduceMotion: Boolean? = false, val gifAutoplayEnabled: Boolean? = true, val startPage: String? = "home", diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt index 2292f4d..dc97e16 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt @@ -2,16 +2,14 @@ package com.fluxa.app.shared import androidx.compose.animation.AnimatedContent import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.togetherWith import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -42,6 +40,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.CircularProgressIndicator @@ -60,7 +59,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned @@ -121,6 +119,7 @@ import com.fluxa.app.shared.feature.player.PlayerRenderAction import com.fluxa.app.shared.feature.player.PlayerRenderState import com.fluxa.app.ui.catalog.CatalogCard import com.fluxa.app.ui.catalog.FluxaColors +import com.fluxa.app.ui.catalog.PosterActionSheet private val FluxaColorScheme = darkColorScheme( background = FluxaColors.background, @@ -318,6 +317,7 @@ fun FluxaApp( language = state.language, onQueryChanged = { value -> onSearchAction(SearchAction.QueryChanged(value)) }, onItemSelected = { item -> onSearchAction(SearchAction.ItemSelected(item)) }, + onAddToLibrary = { item -> onCatalogAction(CatalogAction.AddToLibraryRequested(item)) }, onClearHistory = { onSearchAction(SearchAction.ClearHistory) }, modifier = Modifier.fillMaxSize() ) @@ -326,6 +326,7 @@ fun FluxaApp( language = state.language, onQueryChanged = { value -> onSearchAction(SearchAction.QueryChanged(value)) }, onItemSelected = { item -> onSearchAction(SearchAction.ItemSelected(item)) }, + onAddToLibrary = { item -> onCatalogAction(CatalogAction.AddToLibraryRequested(item)) }, onClearHistory = { onSearchAction(SearchAction.ClearHistory) }, modifier = Modifier.fillMaxSize() ) @@ -438,6 +439,7 @@ fun FluxaApp( onAvatarClick = { onDestinationSelected(FluxaDestination.Settings) }, onCategorySelected = onCategorySelected, profileAvatarUrl = profileState?.activeProfile?.avatarUrl, + topBarEnabled = settingsState?.appearanceHome?.topBarEnabled != false, modifier = Modifier.fillMaxSize() ) else -> FluxaDestinationPlaceholder( @@ -452,6 +454,10 @@ fun FluxaApp( FluxaNavigationBar( destination = state.destination, accentColorArgb = profileState?.activeProfile?.accentColorArgb, + floating = settingsState?.appearance?.floatingBottomBar == true, + showLabels = settingsState?.appearance?.bottomBarLabels == true, + showProfile = settingsState?.appearanceHome?.topBarEnabled == false, + profileAvatarUrl = profileState?.activeProfile?.avatarUrl, language = state.language, calendarState = calendarState, onDestinationSelected = onDestinationSelected, @@ -491,6 +497,10 @@ private fun CalendarUiState.hasUnseenReleases(): Boolean { private fun FluxaNavigationBar( destination: FluxaDestination, accentColorArgb: Long?, + floating: Boolean, + showLabels: Boolean, + showProfile: Boolean, + profileAvatarUrl: String?, language: String?, calendarState: CalendarUiState?, onDestinationSelected: (FluxaDestination) -> Unit, @@ -499,62 +509,70 @@ private fun FluxaNavigationBar( val selectedColor = accentColorArgb?.let { Color(it) } ?: Color.White val inactiveColor = Color(0xFFA0A5AD) val hasCalendarBadge = calendarState?.hasUnseenReleases() == true - Column( + val barShape = RoundedCornerShape(if (floating) 28.dp else 0.dp) + val items = if (showProfile) { + FluxaBottomNavItems + FluxaBottomNavItem(FluxaDestination.Settings, FluxaIcons.BottomSettings, FluxaIcons.BottomSettingsOutline) + } else { + FluxaBottomNavItems + } + Box( modifier = modifier .fillMaxWidth() - .background( - Brush.verticalGradient( - colorStops = arrayOf( - 0f to FluxaColors.background.copy(alpha = 0f), - 0.4f to FluxaColors.background - ) - ) - ) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)) + .padding(horizontal = if (floating) 12.dp else 0.dp) + .padding(bottom = if (floating) 10.dp else 0.dp) ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .background(Color.White.copy(alpha = 0.08f)) - ) Row( modifier = Modifier .fillMaxWidth() - .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)) + .clip(barShape) + .background(if (floating) Color(0xF2222222) else Color(0xFF111111)) .padding(horizontal = 12.dp) - .padding(top = 10.dp, bottom = 10.dp), + .height(if (showLabels) 68.dp else 64.dp), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically ) { - FluxaBottomNavItems.forEach { item -> + items.forEach { item -> val isSelected = item.destination == destination val tint by animateColorAsState( targetValue = if (isSelected) selectedColor else inactiveColor, label = "nav-item-tint" ) - val scale by animateFloatAsState( - targetValue = if (isSelected) 1.1f else 1f, - animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium), - label = "nav-item-scale" - ) Column( modifier = Modifier .weight(1f) - .clip(RoundedCornerShape(14.dp)) + .clip(RoundedCornerShape(18.dp)) .clickable { onDestinationSelected(item.destination) } - .padding(vertical = 4.dp), + .padding(vertical = 8.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(2.dp) ) { Box { - Icon( - if (isSelected) item.selectedIcon else item.icon, - contentDescription = null, - tint = tint, - modifier = Modifier - .size(24.dp) - .graphicsLayer(scaleX = scale, scaleY = scale) - ) + if (item.destination == FluxaDestination.Settings && !profileAvatarUrl.isNullOrBlank()) { + FluxaRemoteImage( + imageUrl = profileAvatarUrl, + cacheKey = "bottom-profile:$profileAvatarUrl", + contentDescription = null, + modifier = Modifier + .size(32.dp) + .then( + if (isSelected) { + Modifier.border(2.dp, tint, CircleShape).padding(2.dp) + } else { + Modifier + } + ) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + } else { + Icon( + if (isSelected) item.selectedIcon else item.icon, + contentDescription = null, + tint = tint, + modifier = Modifier.size(29.dp) + ) + } if (item.showsCalendarBadge && hasCalendarBadge) { Box( modifier = Modifier @@ -566,13 +584,15 @@ private fun FluxaNavigationBar( ) } } - Text( - text = AppStrings.t(language, item.destination.titleKey), - color = tint, - fontSize = 11.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + if (showLabels) { + Text( + text = AppStrings.t(language, item.destination.titleKey), + color = tint, + fontSize = 10.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } } } } @@ -668,6 +688,7 @@ private fun FluxaHomeContent( onAvatarClick: () -> Unit, onCategorySelected: (id: String, title: String) -> Unit, profileAvatarUrl: String?, + topBarEnabled: Boolean, modifier: Modifier ) { if (state.catalogHome.isLoading && state.catalogHome.rows.isEmpty()) { @@ -688,7 +709,7 @@ private fun FluxaHomeContent( val showHero = state.catalogHome.showHeroSection && heroItems.isNotEmpty() val listState = rememberLazyListState() var heroHeightPx by remember { mutableIntStateOf(0) } - var continueWatchingActionItem by remember { mutableStateOf(null) } + var posterActionItem by remember { mutableStateOf(null) } val scrimAlpha by remember { androidx.compose.runtime.derivedStateOf { when { @@ -777,18 +798,14 @@ private fun FluxaHomeContent( CatalogCard( model = item.card, onClick = { onCatalogAction(CatalogAction.ItemSelected(item)) }, - onLongClick = if (row.categoryType == "continue_watching") { - { continueWatchingActionItem = item } - } else { - null - } + onLongClick = { posterActionItem = item } ) } } } } } - if (showHero) { + if (showHero && topBarEnabled) { FluxaHomeOverlayBar( activeFilter = state.catalogHome.activeFilter, language = state.language, @@ -802,72 +819,19 @@ private fun FluxaHomeContent( } } - continueWatchingActionItem?.let { item -> - ContinueWatchingActionSheet( + posterActionItem?.let { item -> + PosterActionSheet( + item = item, language = state.language, - onDismiss = { continueWatchingActionItem = null }, - onPlayManually = { - continueWatchingActionItem = null - onCatalogAction(CatalogAction.PlayRequested(item)) - }, - onDetails = { - continueWatchingActionItem = null - onCatalogAction(CatalogAction.ItemSelected(item)) - }, - onMarkWatched = { - continueWatchingActionItem = null - onCatalogAction(CatalogAction.MarkWatchedRequested(item)) - }, - onDrop = { - continueWatchingActionItem = null - onCatalogAction(CatalogAction.DropRequested(item)) + onDismiss = { posterActionItem = null }, + onAddToLibrary = { + posterActionItem = null + onCatalogAction(CatalogAction.AddToLibraryRequested(item)) } ) } } -@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) -@Composable -private fun ContinueWatchingActionSheet( - language: String?, - onDismiss: () -> Unit, - onPlayManually: () -> Unit, - onDetails: () -> Unit, - onMarkWatched: () -> Unit, - onDrop: () -> Unit -) { - val sheetState = androidx.compose.material3.rememberModalBottomSheetState(skipPartiallyExpanded = true) - androidx.compose.material3.ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState, - containerColor = FluxaColors.surfaceRaised - ) { - Column(modifier = Modifier.padding(bottom = 24.dp)) { - ContinueWatchingActionRow(AppStrings.t(language, "home.play_manually"), onPlayManually) - ContinueWatchingActionRow(AppStrings.t(language, "home.view_details"), onDetails) - ContinueWatchingActionRow(AppStrings.t(language, "detail.mark_watched"), onMarkWatched) - ContinueWatchingActionRow(AppStrings.t(language, "home.forget_progress"), onDrop) - } - } -} - -@Composable -private fun ContinueWatchingActionRow(label: String, onClick: () -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 20.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = label, - color = Color.White, - fontSize = 15.sp - ) - } -} - @Composable private fun FluxaHomeSkeleton(modifier: Modifier = Modifier) { LazyColumn( @@ -1053,26 +1017,27 @@ private fun FluxaHomeHeroSlide( } Row( modifier = Modifier - .padding(top = 4.dp) - .clip(RoundedCornerShape(8.dp)) + .padding(top = 6.dp) + .height(48.dp) + .clip(RoundedCornerShape(14.dp)) .background(Color.White) .clickable { onCatalogAction(CatalogAction.PlayRequested(item)) } - .padding(horizontal = 20.dp, vertical = 10.dp), + .padding(horizontal = 24.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = FluxaIcons.PlayArrow, + imageVector = Icons.Rounded.PlayArrow, contentDescription = null, tint = Color.Black, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(26.dp) ) Text( text = AppStrings.t(language, "common.play"), color = Color.Black, - fontWeight = FontWeight.Bold, - fontSize = 14.sp, - modifier = Modifier.padding(start = 6.dp) + fontWeight = FontWeight.ExtraBold, + fontSize = 15.sp, + modifier = Modifier.padding(start = 7.dp) ) } } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt index 247f9b1..c78c1f8 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt @@ -445,7 +445,6 @@ fun FluxaAppHost( SettingsAction.ConnectSimklRequested -> onConnectSimklRequested() SettingsAction.ConnectAnilistRequested -> onConnectAnilistRequested() SettingsAction.CheckForUpdateRequested -> onCheckForUpdateRequested() - SettingsAction.ManageDownloadsRequested -> appState.openLibraryDownloads() else -> scope.launch { settingsStore?.dispatch(action) } } }, diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogContracts.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogContracts.kt index 5259f7c..4fc8937 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogContracts.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogContracts.kt @@ -66,6 +66,7 @@ sealed interface CatalogAction { data class FilterChanged(val filter: String) : CatalogAction data class MarkWatchedRequested(val item: CatalogItemUiModel) : CatalogAction data class DropRequested(val item: CatalogItemUiModel) : CatalogAction + data class AddToLibraryRequested(val item: CatalogItemUiModel) : CatalogAction } interface CatalogHomeDataSource { diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogHomeStore.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogHomeStore.kt index eb1086b..ea57f9b 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogHomeStore.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogHomeStore.kt @@ -22,7 +22,8 @@ class CatalogHomeStore( is CatalogAction.ResumeRequested, is CatalogAction.ItemFocused, is CatalogAction.MarkWatchedRequested, - is CatalogAction.DropRequested -> Unit + is CatalogAction.DropRequested, + is CatalogAction.AddToLibraryRequested -> Unit } } } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/detail/DetailScreen.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/detail/DetailScreen.kt index bbd2d24..16a6310 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/detail/DetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/detail/DetailScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -284,17 +285,24 @@ private fun ResumeButton(content: DetailUiModel, language: String?, onAction: (D horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(6.dp)) - .background(Color(0xFFE0E0E0)) + .height(54.dp) + .clip(RoundedCornerShape(14.dp)) + .background(Color.White) .clickable { onAction(DetailAction.Play()) } - .padding(horizontal = 16.dp, vertical = 12.dp) + .padding(horizontal = 18.dp), + verticalArrangement = Arrangement.Center ) { Row(verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Filled.PlayArrow, contentDescription = null, tint = Color.Black, modifier = Modifier.size(20.dp)) + Icon( + Icons.Rounded.PlayArrow, + contentDescription = null, + tint = Color.Black, + modifier = Modifier.size(27.dp) + ) Text( text = label, color = Color.Black, - fontWeight = FontWeight.Bold, + fontWeight = FontWeight.ExtraBold, fontSize = 16.sp, modifier = Modifier.padding(start = 8.dp) ) diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/SearchScreen.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/SearchScreen.kt index 8318faf..f15529c 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/SearchScreen.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/SearchScreen.kt @@ -19,6 +19,10 @@ import androidx.compose.foundation.lazy.grid.items as gridItems import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -32,6 +36,7 @@ import com.fluxa.app.shared.feature.catalog.CatalogRowUiModel import com.fluxa.app.shared.skeletonShimmer import com.fluxa.app.ui.catalog.CatalogCard import com.fluxa.app.ui.catalog.FluxaColors +import com.fluxa.app.ui.catalog.PosterActionSheet @Composable fun SearchScreen( @@ -39,9 +44,11 @@ fun SearchScreen( language: String?, onQueryChanged: (String) -> Unit, onItemSelected: (CatalogItemUiModel) -> Unit, + onAddToLibrary: (CatalogItemUiModel) -> Unit = {}, onClearHistory: () -> Unit = {}, modifier: Modifier = Modifier ) { + var actionItem by remember { mutableStateOf(null) } Column( modifier = modifier .fillMaxSize() @@ -61,11 +68,13 @@ fun SearchScreen( state.resultRows.isNotEmpty() -> SearchResultRows( rows = state.resultRows, onItemSelected = onItemSelected, + onItemLongPressed = { actionItem = it }, modifier = Modifier.weight(1f) ) state.results.isNotEmpty() -> SearchResults( items = state.results, onItemSelected = onItemSelected, + onItemLongPressed = { actionItem = it }, modifier = Modifier.weight(1f) ) state.isLoading -> SearchSkeletonGrid(modifier = Modifier.weight(1f)) @@ -74,6 +83,7 @@ fun SearchScreen( language = language, recentItems = state.recentItems, onItemSelected = onItemSelected, + onItemLongPressed = { actionItem = it }, modifier = Modifier.weight(1f) ) state.recentItems.isNotEmpty() -> SearchHistory( @@ -81,6 +91,7 @@ fun SearchScreen( clearLabel = AppStrings.t(language, "auto.clear_search"), items = state.recentItems, onItemSelected = onItemSelected, + onItemLongPressed = { actionItem = it }, onClearHistory = onClearHistory, modifier = Modifier.weight(1f) ) @@ -90,6 +101,17 @@ fun SearchScreen( ) } } + actionItem?.let { item -> + PosterActionSheet( + item = item, + language = language, + onDismiss = { actionItem = null }, + onAddToLibrary = { + actionItem = null + onAddToLibrary(item) + } + ) + } } @Composable @@ -98,6 +120,7 @@ private fun SearchNoResultsForQuery( language: String?, recentItems: List, onItemSelected: (CatalogItemUiModel) -> Unit, + onItemLongPressed: (CatalogItemUiModel) -> Unit = {}, modifier: Modifier ) { Column(modifier = modifier.fillMaxSize()) { @@ -111,6 +134,7 @@ private fun SearchNoResultsForQuery( SearchResults( items = recentItems.take(5), onItemSelected = onItemSelected, + onItemLongPressed = onItemLongPressed, modifier = Modifier.fillMaxSize() ) } @@ -123,6 +147,7 @@ private fun SearchHistory( clearLabel: String, items: List, onItemSelected: (CatalogItemUiModel) -> Unit, + onItemLongPressed: (CatalogItemUiModel) -> Unit = {}, onClearHistory: () -> Unit, modifier: Modifier ) { @@ -143,6 +168,7 @@ private fun SearchHistory( SearchResults( items = items, onItemSelected = onItemSelected, + onItemLongPressed = onItemLongPressed, modifier = Modifier.fillMaxSize() ) } @@ -152,6 +178,7 @@ private fun SearchHistory( internal fun SearchResultRows( rows: List, onItemSelected: (CatalogItemUiModel) -> Unit, + onItemLongPressed: (CatalogItemUiModel) -> Unit = {}, modifier: Modifier ) { LazyColumn( @@ -170,7 +197,7 @@ internal fun SearchResultRows( ) LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) { items(row.items, key = { it.id }) { item -> - CatalogCard(model = item.card, onClick = { onItemSelected(item) }) + CatalogCard(model = item.card, onClick = { onItemSelected(item) }, onLongClick = { onItemLongPressed(item) }) } } } @@ -182,6 +209,7 @@ internal fun SearchResultRows( internal fun SearchResults( items: List, onItemSelected: (CatalogItemUiModel) -> Unit, + onItemLongPressed: (CatalogItemUiModel) -> Unit = {}, modifier: Modifier ) { LazyVerticalGrid( @@ -192,7 +220,7 @@ internal fun SearchResults( verticalArrangement = Arrangement.spacedBy(16.dp) ) { gridItems(items, key = { it.id }) { item -> - CatalogCard(model = item.card, onClick = { onItemSelected(item) }) + CatalogCard(model = item.card, onClick = { onItemSelected(item) }, onLongClick = { onItemLongPressed(item) }) } } } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/TvSearchScreen.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/TvSearchScreen.kt index 23a1023..34a8c3f 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/TvSearchScreen.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/TvSearchScreen.kt @@ -12,6 +12,7 @@ fun TvSearchScreen( language: String?, onQueryChanged: (String) -> Unit, onItemSelected: (CatalogItemUiModel) -> Unit, + onAddToLibrary: (CatalogItemUiModel) -> Unit = {}, onClearHistory: () -> Unit = {}, modifier: Modifier = Modifier ) { @@ -20,6 +21,7 @@ fun TvSearchScreen( language = language, onQueryChanged = onQueryChanged, onItemSelected = onItemSelected, + onAddToLibrary = onAddToLibrary, onClearHistory = onClearHistory, modifier = modifier.padding(start = 38.dp, top = 56.dp, end = 38.dp, bottom = 24.dp) ) diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsContracts.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsContracts.kt index 036bef8..e0602e2 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsContracts.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsContracts.kt @@ -58,10 +58,13 @@ data class SettingsGeneralUiModel( data class SettingsAppearanceUiModel( val accentColorArgb: Long = 0xFFFFFFFFL, val amoledMode: Boolean = false, - val animationsEnabled: Boolean = true + val animationsEnabled: Boolean = true, + val floatingBottomBar: Boolean = false, + val bottomBarLabels: Boolean = false ) data class SettingsAppearanceHomeUiModel( + val topBarEnabled: Boolean = true, val cardCornerPreset: String = "medium", val interfaceDensity: String = "normal", val posterWidthPreset: String = "medium", @@ -220,7 +223,6 @@ sealed interface SettingsAction { data object DisconnectSyncRequested : SettingsAction data object SwitchProfilesRequested : SettingsAction data object CheckForUpdateRequested : SettingsAction - data object ManageDownloadsRequested : SettingsAction } interface SettingsDataSource { diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsScreen.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsScreen.kt index e02594e..fe0ee54 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsScreen.kt @@ -649,6 +649,12 @@ private fun SettingsAppearanceContent(model: SettingsAppearanceUiModel, lang: St SettingsToggleRow(AppStrings.t(lang, "auto.disable_animations"), value = !model.animationsEnabled) { onAction(SettingsAction.AppearanceChanged(model.copy(animationsEnabled = !it))) } + SettingsToggleRow(AppStrings.t(lang, "settings.floating_bottom_bar"), description = AppStrings.t(lang, "settings.floating_bottom_bar_desc"), value = model.floatingBottomBar) { + onAction(SettingsAction.AppearanceChanged(model.copy(floatingBottomBar = it))) + } + SettingsToggleRow(AppStrings.t(lang, "settings.bottom_bar_labels"), description = AppStrings.t(lang, "settings.bottom_bar_labels_desc"), value = model.bottomBarLabels) { + onAction(SettingsAction.AppearanceChanged(model.copy(bottomBarLabels = it))) + } } Spacer(Modifier.height(20.dp)) SettingsGroupCard { @@ -688,6 +694,9 @@ private fun SettingsAppearanceHomeContent(model: SettingsAppearanceHomeUiModel, ) SettingsSectionHeader(AppStrings.t(lang, "settings.layout")) SettingsGroupCard { + SettingsToggleRow(AppStrings.t(lang, "settings.home_top_bar"), description = AppStrings.t(lang, "settings.home_top_bar_desc"), value = model.topBarEnabled) { + onAction(SettingsAction.AppearanceHomeChanged(model.copy(topBarEnabled = it))) + } SettingsChoiceRow(AppStrings.t(lang, "auto.card_corners"), model.cardCornerPreset, cornerOptions) { onAction(SettingsAction.AppearanceHomeChanged(model.copy(cardCornerPreset = it))) } SettingsChoiceRow(AppStrings.t(lang, "auto.interface_density"), model.interfaceDensity, densityOptions) { onAction(SettingsAction.AppearanceHomeChanged(model.copy(interfaceDensity = it))) } SettingsChoiceRow(AppStrings.t(lang, "auto.poster_width"), model.posterWidthPreset, posterWidthOptions) { onAction(SettingsAction.AppearanceHomeChanged(model.copy(posterWidthPreset = it))) } @@ -1024,10 +1033,6 @@ private fun SettingsDownloadsContent(model: SettingsDownloadsUiModel, lang: Stri } } - SettingsSectionHeader(AppStrings.t(lang, "auto.downloads")) - SettingsGroupCard { - SettingsActionRow(AppStrings.t(lang, "settings.manage_downloads")) { onAction(SettingsAction.ManageDownloadsRequested) } - } } @Composable diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsSearch.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsSearch.kt index 12deb19..48f4190 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsSearch.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsSearch.kt @@ -64,7 +64,6 @@ private val SETTINGS_SEARCH_KEYS: List> = listOf( "settings.top_10_catalogs" to SettingsCategory.Content, "settings.download_source_selection" to SettingsCategory.Downloads, "settings.download_subtitle" to SettingsCategory.Downloads, - "settings.manage_downloads" to SettingsCategory.Downloads, "settings.automatic_updates" to SettingsCategory.Hub, "settings.check_for_updates" to SettingsCategory.Hub, "settings.developer" to SettingsCategory.Developer diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsStore.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsStore.kt index 9053253..eb540bc 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsStore.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/settings/SettingsStore.kt @@ -43,7 +43,6 @@ class SettingsStore( SettingsAction.DisconnectSyncRequested -> dataSource.disconnectSync() SettingsAction.SwitchProfilesRequested -> Unit SettingsAction.CheckForUpdateRequested -> Unit - SettingsAction.ManageDownloadsRequested -> Unit } } } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/FluxaIcons.kt b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/FluxaIcons.kt index 4ace2ce..55a687c 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/FluxaIcons.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/FluxaIcons.kt @@ -12,8 +12,8 @@ import androidx.compose.material.icons.automirrored.filled.VolumeOff import androidx.compose.material.icons.automirrored.filled.VolumeUp import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.DateRange -import androidx.compose.material.icons.outlined.Explore import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.VideoLibrary import androidx.compose.ui.graphics.vector.ImageVector @@ -33,8 +33,8 @@ object FluxaIcons { val BookmarkBorder: ImageVector = Icons.Filled.Bookmark val BottomCalendar: ImageVector = Icons.Filled.DateRange val BottomCalendarOutline: ImageVector = Icons.Outlined.DateRange - val BottomDiscover: ImageVector = Icons.Filled.Explore - val BottomDiscoverOutline: ImageVector = Icons.Outlined.Explore + val BottomDiscover: ImageVector = Icons.Filled.Search + val BottomDiscoverOutline: ImageVector = Icons.Outlined.Search val BottomHome: ImageVector = Icons.Filled.Home val BottomHomeOutline: ImageVector = Icons.Outlined.Home val BottomLibrary: ImageVector = Icons.Filled.VideoLibrary diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/PosterActionSheet.kt b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/PosterActionSheet.kt new file mode 100644 index 0000000..260fd23 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/PosterActionSheet.kt @@ -0,0 +1,97 @@ +package com.fluxa.app.ui.catalog + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.fluxa.app.common.AppStrings +import com.fluxa.app.shared.feature.catalog.CatalogItemUiModel +import com.fluxa.app.shared.image.FluxaRemoteImage + +@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) +@Composable +fun PosterActionSheet( + item: CatalogItemUiModel, + language: String?, + onDismiss: () -> Unit, + onAddToLibrary: () -> Unit +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + containerColor = FluxaColors.surfaceRaised + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp).padding(bottom = 28.dp), + verticalArrangement = Arrangement.spacedBy(18.dp) + ) { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + FluxaRemoteImage( + imageUrl = item.card.artworkUrl, + cacheKey = "poster-sheet:${item.card.artworkUrl}", + contentDescription = null, + modifier = Modifier.width(92.dp).height(138.dp).clip(RoundedCornerShape(10.dp)), + contentScale = ContentScale.Crop + ) + Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) { + Text(item.card.title, color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold, maxLines = 2, overflow = TextOverflow.Ellipsis) + item.card.subtitle.takeIf { it.isNotBlank() }?.let { + Text(it, color = Color.White.copy(alpha = 0.65f), fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + val metadata = listOfNotNull( + item.ageRating?.takeIf { it.isNotBlank() }, + item.runtimeLabel?.takeIf { it.isNotBlank() }, + item.seasonsCount?.takeIf { it > 0 }?.let { "$it ${AppStrings.t(language, "auto.seasons")}" } + ).joinToString(" • ") + if (metadata.isNotBlank()) { + Text(metadata, color = Color.White.copy(alpha = 0.72f), fontSize = 13.sp) + } + item.description?.takeIf { it.isNotBlank() }?.let { + Text(it, color = Color.White.copy(alpha = 0.78f), fontSize = 13.sp, lineHeight = 17.sp, maxLines = 4, overflow = TextOverflow.Ellipsis) + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color.White) + .clickable(onClick = onAddToLibrary), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Filled.Add, contentDescription = null, tint = Color.Black) + Text( + AppStrings.t(language, "common.add_to_library"), + color = Color.Black, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } +} diff --git a/shared/src/iosMain/kotlin/com/fluxa/app/shared/platform/AppleSettingsDataSource.kt b/shared/src/iosMain/kotlin/com/fluxa/app/shared/platform/AppleSettingsDataSource.kt index bbc8386..e3a4ba4 100644 --- a/shared/src/iosMain/kotlin/com/fluxa/app/shared/platform/AppleSettingsDataSource.kt +++ b/shared/src/iosMain/kotlin/com/fluxa/app/shared/platform/AppleSettingsDataSource.kt @@ -42,10 +42,13 @@ class AppleSettingsDataSource : SettingsDataSource { defaults.setDouble(value.accentColorArgb.toDouble(), K.accentColorArgb) defaults.setBool(value.amoledMode, K.amoledMode) defaults.setBool(value.animationsEnabled, K.animationsEnabled) + defaults.setBool(value.floatingBottomBar, K.floatingBottomBar) + defaults.setBool(value.bottomBarLabels, K.bottomBarLabels) state.value = state.value.copy(appearance = value) } override suspend fun updateAppearanceHome(value: SettingsAppearanceHomeUiModel) { + defaults.setBool(value.topBarEnabled, K.homeTopBarEnabled) defaults.setObject(value.cardCornerPreset, K.cardCornerPreset) defaults.setObject(value.interfaceDensity, K.interfaceDensity) defaults.setObject(value.posterWidthPreset, K.posterWidthPreset) @@ -221,9 +224,12 @@ class AppleSettingsDataSource : SettingsDataSource { appearance = SettingsAppearanceUiModel( accentColorArgb = defaults.objectForKey(K.accentColorArgb)?.let { defaults.doubleForKey(K.accentColorArgb).toLong() } ?: 0xFFFFFFFFL, amoledMode = defaults.boolOrDefault(K.amoledMode, false), - animationsEnabled = defaults.boolOrDefault(K.animationsEnabled, true) + animationsEnabled = defaults.boolOrDefault(K.animationsEnabled, true), + floatingBottomBar = defaults.boolOrDefault(K.floatingBottomBar, false), + bottomBarLabels = defaults.boolOrDefault(K.bottomBarLabels, false) ), appearanceHome = SettingsAppearanceHomeUiModel( + topBarEnabled = defaults.boolOrDefault(K.homeTopBarEnabled, true), cardCornerPreset = defaults.stringForKey(K.cardCornerPreset) ?: "medium", interfaceDensity = defaults.stringForKey(K.interfaceDensity) ?: "normal", posterWidthPreset = defaults.stringForKey(K.posterWidthPreset) ?: "medium", @@ -318,6 +324,9 @@ class AppleSettingsDataSource : SettingsDataSource { const val accentColorArgb = "fluxa.apple.settings.accentColorArgb" const val amoledMode = "fluxa.apple.settings.amoledMode" const val animationsEnabled = "fluxa.apple.settings.animationsEnabled" + const val homeTopBarEnabled = "fluxa.apple.settings.homeTopBarEnabled" + const val floatingBottomBar = "fluxa.apple.settings.floatingBottomBar" + const val bottomBarLabels = "fluxa.apple.settings.bottomBarLabels" const val cardCornerPreset = "fluxa.apple.settings.cardCornerPreset" const val interfaceDensity = "fluxa.apple.settings.interfaceDensity" const val posterWidthPreset = "fluxa.apple.settings.posterWidthPreset"