Refine mobile navigation and poster actions

This commit is contained in:
KhooLy 2026-07-18 17:56:43 +03:00
parent 909425202a
commit 62d20e23c4
20 changed files with 293 additions and 144 deletions

View file

@ -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,

View file

@ -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 {

View file

@ -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
}
},

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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",

View file

@ -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<CatalogItemUiModel?>(null) }
var posterActionItem by remember { mutableStateOf<CatalogItemUiModel?>(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)
)
}
}

View file

@ -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) }
}
},

View file

@ -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 {

View file

@ -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
}
}
}

View file

@ -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)
)

View file

@ -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<CatalogItemUiModel?>(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<CatalogItemUiModel>,
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<CatalogItemUiModel>,
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<CatalogRowUiModel>,
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<CatalogItemUiModel>,
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) })
}
}
}

View file

@ -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)
)

View file

@ -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 {

View file

@ -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

View file

@ -64,7 +64,6 @@ private val SETTINGS_SEARCH_KEYS: List<Pair<String, SettingsCategory>> = 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

View file

@ -43,7 +43,6 @@ class SettingsStore(
SettingsAction.DisconnectSyncRequested -> dataSource.disconnectSync()
SettingsAction.SwitchProfilesRequested -> Unit
SettingsAction.CheckForUpdateRequested -> Unit
SettingsAction.ManageDownloadsRequested -> Unit
}
}
}

View file

@ -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

View file

@ -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)
)
}
}
}
}

View file

@ -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"