Split TvSettingsSections.kt (818 lines) into focused section files

Dissolve the monolithic sections file into TvSettingsAccountSection,
TvSettingsAppearanceSection, TvSettingsContentSection, TvSettingsPlaybackSection,
and TvSettingsSystemSection. TvSettingsPreviews retains the preview composables.
All section functions stay internal for same-module access from TvSettingsScreen.
This commit is contained in:
KhooLy 2026-07-03 12:20:12 +03:00
parent 331b989ce8
commit 423609a0e3
7 changed files with 1187 additions and 489 deletions

View file

@ -0,0 +1,222 @@
@file:OptIn(androidx.tv.material3.ExperimentalTvMaterial3Api::class, androidx.compose.material3.ExperimentalMaterial3Api::class)
package com.fluxa.app.ui.catalog
import com.fluxa.app.common.AppStrings
import com.fluxa.app.data.local.*
import com.fluxa.app.data.remote.*
import com.fluxa.app.data.repository.*
import com.fluxa.app.domain.discovery.*
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle
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.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.Surface
import coil3.compose.AsyncImage
import java.util.Locale
@Composable
internal fun AccountSettings(
profile: UserProfile,
lang: String,
onSwitchProfiles: () -> Unit,
onConnectTrakt: () -> Unit,
onConnectMal: () -> Unit,
onConnectSimkl: () -> Unit,
onUpdateProfile: (UserProfile) -> Unit
) {
var tmdbExpanded by remember { mutableStateOf(false) }
SettingsSection(
AppStrings.t(lang, "auto.account_sync"),
AppStrings.t(lang, "auto.account_devices_and_sync")
) {
SettingsActionTile(
title = AppStrings.t(lang, "settings.switch_profiles"),
subtitle = AppStrings.t(lang, "settings.switch_profiles_desc"),
icon = FluxaIcons.AccountCircle,
onClick = onSwitchProfiles
)
}
SettingsSection(
AppStrings.t(lang, "settings.sync_with"),
AppStrings.t(lang, "settings.sync_with_desc")
) {
SettingsConnectionTile(
title = AppStrings.t(lang, "brand.trakt"),
iconRes = com.fluxa.app.R.drawable.ic_trakt,
value = if (!profile.traktAccessToken.isNullOrBlank()) {
AppStrings.t(lang, "auto.connected")
} else {
AppStrings.t(lang, "auto.not_connected")
},
onClick = onConnectTrakt
)
SettingsConnectionTile(
title = AppStrings.t(lang, "brand.myanimelist"),
iconRes = com.fluxa.app.R.drawable.ic_myanimelist,
value = if (!profile.malAccessToken.isNullOrBlank()) AppStrings.t(lang, "auto.connected") else AppStrings.t(lang, "auto.not_connected"),
onClick = onConnectMal
)
SettingsConnectionTile(
title = AppStrings.t(lang, "brand.simkl"),
iconRes = com.fluxa.app.R.drawable.ic_simkl,
value = if (!profile.simklAccessToken.isNullOrBlank()) AppStrings.t(lang, "auto.connected") else AppStrings.t(lang, "auto.not_connected"),
onClick = onConnectSimkl
)
if (!profile.traktAccessToken.isNullOrBlank() || !profile.malAccessToken.isNullOrBlank() || !profile.simklAccessToken.isNullOrBlank()) {
SettingsActionTile(
title = AppStrings.t(lang, "auto.disconnect"),
subtitle = AppStrings.t(lang, "integration.connected_accounts"),
icon = FluxaIcons.Logout,
accent = FluxaColors.errorRed,
onClick = {
onUpdateProfile(
profile.copy(
traktAccessToken = null,
traktRefreshToken = null,
traktLastSyncAt = null,
traktLastSyncedItems = null,
traktLastContinueWatchingCount = null,
traktLastWatchlistCount = null,
malAccessToken = null,
malRefreshToken = null,
simklAccessToken = null
)
)
}
)
}
}
SettingsSection(
AppStrings.t(lang, "settings.apis"),
AppStrings.t(lang, "settings.apis_desc")
) {
SettingsActionTile(
title = AppStrings.t(lang, "settings.tmdb_api"),
subtitle = if (profile.safeTmdbApiKey.isNotBlank()) AppStrings.t(lang, "settings.tmdb_api_configured") else AppStrings.t(lang, "settings.tmdb_api_not_configured"),
icon = FluxaIcons.Storage,
onClick = { tmdbExpanded = !tmdbExpanded }
)
if (tmdbExpanded) {
SettingsTextFieldTile(
title = AppStrings.t(lang, "settings.tmdb_api_key"),
subtitle = AppStrings.t(lang, "settings.tmdb_api_key_desc"),
value = profile.safeTmdbApiKey,
placeholder = AppStrings.t(lang, "settings.tmdb_api_key_placeholder"),
onValueChange = { onUpdateProfile(profile.copy(tmdbApiKey = it.trim())) }
)
if (profile.safeTmdbApiKey.isNotBlank()) {
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_cast_images"),
subtitle = AppStrings.t(lang, "settings.tmdb_cast_images_desc"),
checked = profile.safeTmdbCastImagesEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbCastImagesEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_similar_results"),
subtitle = AppStrings.t(lang, "settings.tmdb_similar_results_desc"),
checked = profile.safeTmdbSimilarResultsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbSimilarResultsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_trailers"),
subtitle = AppStrings.t(lang, "settings.tmdb_trailers_desc"),
checked = profile.safeTmdbTrailersEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbTrailersEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_recommendations"),
subtitle = AppStrings.t(lang, "settings.tmdb_recommendations_desc"),
checked = profile.safeTmdbRecommendationsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbRecommendationsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_collection_info"),
subtitle = AppStrings.t(lang, "settings.tmdb_collection_info_desc"),
checked = profile.safeTmdbCollectionInfoEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbCollectionInfoEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_episode_images"),
subtitle = AppStrings.t(lang, "settings.tmdb_episode_images_desc"),
checked = profile.safeTmdbEpisodeImagesEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbEpisodeImagesEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_logos_backdrops"),
subtitle = AppStrings.t(lang, "settings.tmdb_logos_backdrops_desc"),
checked = profile.safeTmdbLogosBackdropsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbLogosBackdropsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_ratings"),
subtitle = AppStrings.t(lang, "settings.tmdb_ratings_desc"),
checked = profile.safeTmdbRatingsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbRatingsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_basic_info"),
subtitle = AppStrings.t(lang, "settings.tmdb_basic_info_desc"),
checked = profile.safeTmdbBasicInfoEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbBasicInfoEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_details"),
subtitle = AppStrings.t(lang, "settings.tmdb_details_desc"),
checked = profile.safeTmdbDetailsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbDetailsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_productions"),
subtitle = AppStrings.t(lang, "settings.tmdb_productions_desc"),
checked = profile.safeTmdbProductionsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbProductionsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_networks"),
subtitle = AppStrings.t(lang, "settings.tmdb_networks_desc"),
checked = profile.safeTmdbNetworksEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbNetworksEnabled = it)) }
)
}
}
}
}

View file

@ -0,0 +1,256 @@
@file:OptIn(androidx.tv.material3.ExperimentalTvMaterial3Api::class, androidx.compose.material3.ExperimentalMaterial3Api::class)
package com.fluxa.app.ui.catalog
import com.fluxa.app.common.AppStrings
import com.fluxa.app.data.local.*
import com.fluxa.app.data.remote.*
import com.fluxa.app.data.repository.*
import com.fluxa.app.domain.discovery.*
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
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 androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.Surface
import coil3.compose.AsyncImage
import java.util.Locale
@Composable
internal fun GeneralSettings(profile: UserProfile, lang: String, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.general"), AppStrings.t(lang, "auto.app")) {
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.interface_language"),
subtitle = AppStrings.t(lang, "auto.app_copy_and_category_labels"),
options = listOf(
ChoiceOption("none", AppStrings.t(lang, "settings.none")),
ChoiceOption("tr", languageDisplayName("tr", lang)),
ChoiceOption("en", languageDisplayName("en", lang))
),
selected = profile.safeLanguage,
onSelect = { onUpdateProfile(profile.copy(language = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.start_page"),
subtitle = AppStrings.t(lang, "settings.start_page_desc"),
options = listOf(
ChoiceOption("home", AppStrings.t(lang, "nav.home")),
ChoiceOption("discover", AppStrings.t(lang, "nav.discover")),
ChoiceOption("library", AppStrings.t(lang, "nav.library"))
),
selected = profile.safeStartPage,
onSelect = { onUpdateProfile(profile.copy(startPage = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.tv_nav_layout"),
subtitle = AppStrings.t(lang, "auto.tv_nav_layout_subtitle"),
options = listOf(
ChoiceOption("left", AppStrings.t(lang, "auto.tv_nav_layout_left")),
ChoiceOption("top", AppStrings.t(lang, "auto.tv_nav_layout_top"))
),
selected = profile.safeTvNavLayout,
onSelect = { onUpdateProfile(profile.copy(tvNavLayout = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "auto.background_playback"),
subtitle = AppStrings.t(lang, "settings.background_playback_desc"),
checked = profile.safeBackgroundPlayback,
onToggle = { onUpdateProfile(profile.copy(backgroundPlayback = it)) }
)
}
}
@Composable
internal fun AppearanceSettings(profile: UserProfile, lang: String, previewMeta: Meta?, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.accent_color"), AppStrings.t(lang, "auto.appearance")) {
SettingsAccentColorTile(profile, onUpdateProfile)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.amoled"),
subtitle = AppStrings.t(lang, "auto.makes_the_background_and_bottom_bar_pure_bla"),
checked = profile.safeAmoledMode,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(appTheme = "dark", amoledMode = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "auto.disable_animations"),
subtitle = AppStrings.t(lang, "auto.turns_off_app_wide_transitions_and_card_anim"),
checked = !profile.safeAnimationsEnabled,
onToggle = { onUpdateProfile(profile.copy(animationsEnabled = !it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.card_layout"),
subtitle = AppStrings.t(lang, "auto.how_rows_and_home_shelves_look"),
options = listOf(
ChoiceOption("vertical", AppStrings.t(lang, "auto.vertical_layout")),
ChoiceOption("horizontal", AppStrings.t(lang, "auto.horizontal"))
),
selected = profile.safeCardLayout,
onSelect = { onUpdateProfile(profile.copy(cardLayout = it)) }
)
}
SettingsSection(AppStrings.t(lang, "settings.appearance_home_screen"), AppStrings.t(lang, "settings.appearance_home_screen_desc")) {
TvPosterPreview(profile, lang, previewMeta)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.card_corners"),
subtitle = "",
options = mobilePosterCornerOptions(lang),
selected = profile.safeCardCornerPreset,
onSelect = { onUpdateProfile(profile.copy(cardCornerPreset = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.interface_density"),
subtitle = "",
options = mobilePresetOptions(lang),
selected = profile.safeInterfaceDensity,
onSelect = { onUpdateProfile(profile.copy(interfaceDensity = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.poster_width"),
subtitle = "",
options = mobilePosterWidthOptions(lang),
selected = profile.safePosterWidthPreset,
onSelect = { onUpdateProfile(profile.copy(posterWidthPreset = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.landscape_mode"),
subtitle = "",
checked = profile.safePosterLandscapeMode,
onToggle = { onUpdateProfile(profile.copy(posterLandscapeMode = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "auto.hide_titles"),
subtitle = "",
checked = profile.safePosterHideTitles,
onToggle = { onUpdateProfile(profile.copy(posterHideTitles = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.season_posters_on_hero"),
subtitle = AppStrings.t(lang, "settings.home_season_posters_on_hero_desc"),
checked = profile.safeHomeSeasonPostersOnHero,
onToggle = { onUpdateProfile(profile.copy(homeSeasonPostersOnHero = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "auto.continue_watching"),
subtitle = "",
checked = profile.safeContinueWatchingEnabled,
onToggle = { onUpdateProfile(profile.copy(continueWatchingEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.continue_watching_hide_titles"),
subtitle = "",
checked = profile.safeContinueWatchingHideTitles,
onToggle = { onUpdateProfile(profile.copy(continueWatchingHideTitles = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.continue_watching_layout"),
subtitle = AppStrings.t(lang, "auto.show_that_shelf_as_posters_or_episode_cards"),
options = listOf(
ChoiceOption("vertical", AppStrings.t(lang, "auto.vertical_layout")),
ChoiceOption("horizontal", AppStrings.t(lang, "auto.horizontal")),
ChoiceOption("inherit", AppStrings.t(lang, "auto.match_global"))
),
selected = profile.safeContinueWatchingLayout,
onSelect = { onUpdateProfile(profile.copy(continueWatchingLayout = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.series_artwork"),
subtitle = AppStrings.t(lang, "auto.choose_episode_stills_or_regular_artwork_for"),
options = listOf(
ChoiceOption("episode", AppStrings.t(lang, "auto.episode_cover")),
ChoiceOption("poster", AppStrings.t(lang, "auto.poster")),
ChoiceOption("background", AppStrings.t(lang, "auto.backdrop"))
),
selected = profile.safeContinueWatchingArtwork,
onSelect = { onUpdateProfile(profile.copy(continueWatchingArtwork = it)) }
)
}
SettingsSection(AppStrings.t(lang, "settings.expanded_posters"), AppStrings.t(lang, "settings.expanded_posters_desc")) {
SettingsToggleTile(
title = AppStrings.t(lang, "settings.expanded_posters"),
subtitle = AppStrings.t(lang, "settings.expanded_posters_desc"),
checked = profile.safeExpandedPostersEnabled,
onToggle = { onUpdateProfile(profile.copy(expandedPostersEnabled = it)) }
)
if (profile.safeExpandedPostersEnabled) {
SettingsSecondsSliderTile(
title = AppStrings.t(lang, "settings.expanded_posters_delay"),
subtitle = AppStrings.t(lang, "settings.expanded_posters_delay_desc"),
value = profile.safeExpandedPostersDelaySeconds,
valueRange = 0..10,
zeroLabel = AppStrings.t(lang, "settings.expanded_posters_delay_instant"),
onValueChange = { onUpdateProfile(profile.copy(expandedPostersDelaySeconds = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.trailer_on_expanded_posters"),
subtitle = AppStrings.t(lang, "settings.trailer_on_expanded_posters_desc"),
checked = profile.safeTrailerOnExpandedPostersEnabled,
onToggle = { onUpdateProfile(profile.copy(trailerOnExpandedPostersEnabled = it)) }
)
if (profile.safeTrailerOnExpandedPostersEnabled) {
SettingsSecondsSliderTile(
title = AppStrings.t(lang, "settings.trailer_on_expanded_posters_delay"),
subtitle = AppStrings.t(lang, "settings.trailer_on_expanded_posters_delay_desc"),
value = profile.safeTrailerOnExpandedPostersDelaySeconds,
valueRange = 0..15,
onValueChange = { onUpdateProfile(profile.copy(trailerOnExpandedPostersDelaySeconds = it)) }
)
}
}
}
SettingsSection(AppStrings.t(lang, "settings.appearance_detail_screen"), AppStrings.t(lang, "settings.appearance_detail_screen_desc")) {
SettingsToggleTile(
title = AppStrings.t(lang, "settings.trailer_on_hero"),
subtitle = AppStrings.t(lang, "settings.trailer_on_hero_desc"),
checked = profile.safeTrailerOnHero,
onToggle = { onUpdateProfile(profile.copy(trailerOnHero = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.blur_unwatched_episodes"),
subtitle = AppStrings.t(lang, "settings.blur_unwatched_episodes_desc"),
checked = profile.safeBlurUnwatchedEpisodes,
onToggle = { onUpdateProfile(profile.copy(blurUnwatchedEpisodes = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.season_selector"),
subtitle = AppStrings.t(lang, "settings.season_selector_desc"),
options = mobileSeasonSelectorOptions(lang),
selected = profile.safeDetailSeasonSelectorMode,
onSelect = { onUpdateProfile(profile.copy(detailSeasonSelectorMode = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.season_posters_on_hero"),
subtitle = AppStrings.t(lang, "settings.detail_season_posters_on_hero_desc"),
checked = profile.safeDetailSeasonPostersOnHero,
onToggle = { onUpdateProfile(profile.copy(detailSeasonPostersOnHero = it)) }
)
TvEpisodeLayoutPreview(profile, lang) { onUpdateProfile(profile.copy(episodeCardsLayout = it)) }
}
}

View file

@ -0,0 +1,137 @@
@file:OptIn(androidx.tv.material3.ExperimentalTvMaterial3Api::class, androidx.compose.material3.ExperimentalMaterial3Api::class)
package com.fluxa.app.ui.catalog
import com.fluxa.app.common.AppStrings
import com.fluxa.app.data.local.*
import com.fluxa.app.data.remote.*
import com.fluxa.app.data.repository.*
import com.fluxa.app.domain.discovery.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun ContentSettings(
profile: UserProfile,
lang: String,
categories: List<HomeCategory>,
userAddons: List<AddonDescriptor>,
cs3FeedOptions: List<MetadataFeedOption>,
onUpdateProfile: (UserProfile) -> Unit
) {
val metadataFeedOptions = buildMetadataFeedOptions(userAddons, lang) + cs3FeedOptions
val heroFeedOptions = orderedMetadataFeeds(metadataFeedOptions, profile.heroFeedOrder)
val homeFeedOptions = orderedMetadataFeeds(metadataFeedOptions, profile.homeFeedOrder)
val metadataFeedKeys = metadataFeedOptions.map { it.key }
val heroSelectedKeys = effectiveMetadataFeedSelection(profile.heroFeedToggles, metadataFeedKeys) ?: metadataFeedKeys.take(2)
val homeSelectedKeys = effectiveMetadataFeedSelection(profile.homeFeedToggles, metadataFeedKeys)
val homeVisibleFeedOptions = homeFeedOptions.filter { isMetadataFeedEnabled(homeSelectedKeys, it.key) }
val topTenSelectedKeys = profile.safeTopTenFeedToggles
SettingsSection(AppStrings.t(lang, "settings.hero_catalogs"), "") {
SettingsToggleTile(
title = AppStrings.t(lang, "settings.show_hero_section"),
subtitle = AppStrings.t(lang, "settings.show_hero_section_desc"),
checked = profile.safeShowHeroSection,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(showHeroSection = it)) }
)
heroFeedOptions.forEachIndexed { index, option ->
SettingsOrderedToggleTile(
title = option.label,
checked = heroSelectedKeys.contains(option.key),
onToggle = {
onUpdateProfile(profile.sanitizedUpdate(heroFeedToggles = toggleMetadataFeed(heroSelectedKeys, metadataFeedKeys, option.key, maxEnabled = 2)))
},
canMoveUp = index > 0,
canMoveDown = index < heroFeedOptions.lastIndex,
onMoveUp = { onUpdateProfile(profile.sanitizedUpdate(heroFeedOrder = moveMetadataFeedOrder(metadataFeedOptions, profile.heroFeedOrder, option.key, -1))) },
onMoveDown = { onUpdateProfile(profile.sanitizedUpdate(heroFeedOrder = moveMetadataFeedOrder(metadataFeedOptions, profile.heroFeedOrder, option.key, 1))) }
)
}
}
SettingsSection(AppStrings.t(lang, "settings.home_catalogs"), AppStrings.t(lang, "settings.home_catalogs_desc")) {
homeFeedOptions.forEachIndexed { index, option ->
SettingsOrderedToggleTile(
title = option.label,
checked = isMetadataFeedEnabled(homeSelectedKeys, option.key),
onToggle = {
onUpdateProfile(profile.sanitizedUpdate(
homeFeedToggles = toggleMetadataFeed(homeSelectedKeys, metadataFeedKeys, option.key),
cs3FeedsConfigured = if (cs3FeedOptions.isNotEmpty()) true else profile.cs3FeedsConfigured
))
},
canMoveUp = index > 0,
canMoveDown = index < homeFeedOptions.lastIndex,
onMoveUp = { onUpdateProfile(profile.sanitizedUpdate(homeFeedOrder = moveMetadataFeedOrder(metadataFeedOptions, profile.homeFeedOrder, option.key, -1))) },
onMoveDown = { onUpdateProfile(profile.sanitizedUpdate(homeFeedOrder = moveMetadataFeedOrder(metadataFeedOptions, profile.homeFeedOrder, option.key, 1))) }
)
}
}
SettingsSection(AppStrings.t(lang, "settings.top_10_catalogs"), AppStrings.t(lang, "settings.top_10_catalogs_desc")) {
homeVisibleFeedOptions.forEach { option ->
SettingsToggleTile(
title = option.label,
subtitle = "",
checked = topTenSelectedKeys.contains(option.key),
onToggle = { onUpdateProfile(profile.sanitizedUpdate(topTenFeedToggles = toggleTopTenFeed(topTenSelectedKeys, option.key))) }
)
}
}
}
@Composable
internal fun AdvancedSettings(profile: UserProfile, lang: String, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "settings.advanced_settings"), AppStrings.t(lang, "settings.advanced_settings_desc")) {
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.buffer_cache"),
subtitle = AppStrings.t(lang, "settings.buffer_cache_desc"),
options = listOf(
ChoiceOption("100", "100 MB"),
ChoiceOption("500", "500 MB"),
ChoiceOption("1000", "1 GB"),
ChoiceOption("2000", "2 GB")
),
selected = profile.safePlayerBufferCacheMb.toString(),
onSelect = { onUpdateProfile(profile.sanitizedUpdate(playerBufferCacheMb = it.toInt())) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.forward_buffer"),
subtitle = AppStrings.t(lang, "settings.forward_buffer_desc"),
options = bufferSecondOptions(lang),
selected = profile.safePlayerForwardBufferSeconds.toString(),
onSelect = { onUpdateProfile(profile.sanitizedUpdate(playerForwardBufferSeconds = it.toInt())) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.back_buffer"),
subtitle = AppStrings.t(lang, "settings.back_buffer_desc"),
options = bufferSecondOptions(lang, includeZero = true),
selected = profile.safePlayerBackBufferSeconds.toString(),
onSelect = { onUpdateProfile(profile.sanitizedUpdate(playerBackBufferSeconds = it.toInt())) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.audio_decoder_mode"),
subtitle = AppStrings.t(lang, "settings.audio_decoder_mode_desc"),
options = audioDecoderModeOptions(lang),
selected = profile.safeAudioDecoderMode,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(audioDecoderMode = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tunneled_playback"),
subtitle = AppStrings.t(lang, "settings.tunneled_playback_desc"),
checked = profile.safeTunneledPlayback,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(tunneledPlayback = it)) }
)
}
}

View file

@ -0,0 +1,134 @@
@file:OptIn(androidx.tv.material3.ExperimentalTvMaterial3Api::class, androidx.compose.material3.ExperimentalMaterial3Api::class)
package com.fluxa.app.ui.catalog
import com.fluxa.app.common.AppStrings
import com.fluxa.app.data.local.*
import com.fluxa.app.data.remote.*
import com.fluxa.app.data.repository.*
import com.fluxa.app.domain.discovery.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun PlaybackSettings(
profile: UserProfile,
lang: String,
onOpenSubtitles: () -> Unit,
onOpenAdvanced: () -> Unit,
onUpdateProfile: (UserProfile) -> Unit
) {
SettingsSection(AppStrings.t(lang, "auto.playback"), AppStrings.t(lang, "auto.shape_how_playback_behaves")) {
SettingsActionTile(
title = AppStrings.t(lang, "auto.subtitles"),
subtitle = AppStrings.t(lang, "settings.subtitles_settings_desc"),
icon = FluxaIcons.Subtitles,
onClick = onOpenSubtitles
)
SettingsActionTile(
title = AppStrings.t(lang, "settings.advanced_settings"),
subtitle = AppStrings.t(lang, "settings.advanced_settings_desc"),
icon = FluxaIcons.Speed,
onClick = onOpenAdvanced
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.skip_forward"),
subtitle = AppStrings.t(lang, "auto.how_far_right_arrow_should_jump"),
options = listOf(ChoiceOption("10", "10s"), ChoiceOption("15", "15s"), ChoiceOption("30", "30s")),
selected = profile.safeSeekForwardSeconds.toString(),
onSelect = { onUpdateProfile(profile.sanitizedUpdate(seekForwardSeconds = it.toInt())) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.skip_back"),
subtitle = AppStrings.t(lang, "auto.how_far_left_arrow_should_rewind"),
options = listOf(ChoiceOption("10", "10s"), ChoiceOption("15", "15s"), ChoiceOption("30", "30s")),
selected = profile.safeSeekBackwardSeconds.toString(),
onSelect = { onUpdateProfile(profile.sanitizedUpdate(seekBackwardSeconds = it.toInt())) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "auto.auto_skip_intros"),
subtitle = AppStrings.t(lang, "auto.prefer_skipping_intros_where_supported"),
checked = profile.safeAutoSkipIntro,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(autoSkipIntro = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.stream_source_selection"),
subtitle = AppStrings.t(lang, "settings.stream_source_selection_desc"),
options = streamSourceSelectionOptions(lang),
selected = profile.safeStreamSourceSelectionMode,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(streamSourceSelectionMode = it)) }
)
if (profile.safeStreamSourceSelectionMode == STREAM_SOURCE_MODE_REGEX) {
SettingsTextFieldTile(
title = AppStrings.t(lang, "settings.regex_pattern"),
subtitle = AppStrings.t(lang, "settings.regex_pattern_desc"),
value = profile.safeStreamSourceRegexPattern,
placeholder = AppStrings.t(lang, "settings.regex_pattern_placeholder"),
onValueChange = { onUpdateProfile(profile.sanitizedUpdate(streamSourceRegexPattern = it)) }
)
}
SettingsToggleTile(
title = AppStrings.t(lang, "auto.auto_play_next_episode"),
subtitle = AppStrings.t(lang, "auto.queue_the_next_episode_automatically"),
checked = profile.safeAutoPlayNextEpisode,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(autoPlayNextEpisode = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.try_binge_group"),
subtitle = AppStrings.t(lang, "settings.try_binge_group_desc"),
checked = profile.safeTryBingeGroup,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(tryBingeGroup = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.preferred_player"),
subtitle = AppStrings.t(lang, "auto.choose_between_internal_or_external_playback"),
options = listOf(
ChoiceOption("exoplayer", "ExoPlayer"),
ChoiceOption("mpv", "MPV")
),
selected = profile.safePreferredPlayer,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(preferredPlayer = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.dv_fallback"),
subtitle = AppStrings.t(lang, "settings.dv_fallback_desc"),
options = dolbyVisionFallbackOptions(lang),
selected = profile.safeDolbyVisionFallbackMode,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(dolbyVisionFallbackMode = it)) }
)
if (profile.safeDolbyVisionFallbackMode == "auto") {
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.dv_rpu_mode"),
subtitle = AppStrings.t(lang, "settings.dv_rpu_mode_desc"),
options = dvRpuModeOptions(lang),
selected = profile.safeDvRpuMode.toString(),
onSelect = { onUpdateProfile(profile.sanitizedUpdate(dvRpuMode = it.toIntOrNull() ?: 2)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.dv_zero_level5"),
subtitle = AppStrings.t(lang, "settings.dv_zero_level5_desc"),
checked = profile.safeDvZeroLevel5,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(dvZeroLevel5 = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.dv_hdr10plus_mode"),
subtitle = AppStrings.t(lang, "settings.dv_hdr10plus_mode_desc"),
options = dvHdr10PlusModeOptions(lang),
selected = profile.safeDvHdr10PlusMode,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(dvHdr10PlusMode = it)) }
)
}
}
}

View file

@ -0,0 +1,257 @@
@file:OptIn(androidx.tv.material3.ExperimentalTvMaterial3Api::class)
package com.fluxa.app.ui.catalog
import com.fluxa.app.common.AppStrings
import com.fluxa.app.data.local.*
import com.fluxa.app.data.remote.*
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
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 androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.Surface
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
@Composable
internal fun TvPosterPreview(profile: UserProfile, lang: String, meta: Meta?) {
val landscape = profile.safePosterLandscapeMode
val width by animateDpAsState(
targetValue = mobilePosterWidth(profile.safePosterWidthPreset, landscape),
animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand),
label = "tvPosterPreviewWidth"
)
val height by animateDpAsState(
targetValue = mobilePosterHeight(profile.safePosterWidthPreset, landscape),
animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand),
label = "tvPosterPreviewHeight"
)
val radius by animateDpAsState(
targetValue = mobileCornerRadius(profile.safeCardCornerPreset),
animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand),
label = "tvPosterPreviewRadius"
)
Column(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.width(width)
.height(height)
.clip(RoundedCornerShape(radius))
.background(Color.White.copy(alpha = 0.06f))
.border(1.dp, Color.White.copy(alpha = 0.38f), RoundedCornerShape(radius))
) {
val targetUrl = remember(meta?.id, meta?.poster, meta?.background, landscape) {
if (landscape && meta != null) horizontalArtworkCandidates(meta).firstOrNull() else meta?.poster
}
var displayUrl by remember { mutableStateOf(targetUrl) }
LaunchedEffect(targetUrl) {
if (targetUrl != null) displayUrl = targetUrl
}
val context = LocalContext.current
if (!displayUrl.isNullOrBlank()) {
AsyncImage(
model = ImageRequest.Builder(context)
.data(displayUrl)
.crossfade(280)
.build(),
contentDescription = meta?.name,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
onError = { displayUrl = null }
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.linearGradient(listOf(Color(0xFF2B3854), Color(0xFF7C3F54), Color(0xFF171B24)))
)
)
}
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
listOf(Color.Transparent, Color.Black.copy(alpha = if (profile.safePosterHideTitles) 0.12f else 0.76f))
)
)
)
}
if (!profile.safePosterHideTitles) {
Spacer(Modifier.height(8.dp))
Text(
text = meta?.name ?: AppStrings.t(lang, "auto.content_title"),
color = Color.White.copy(alpha = 0.92f),
fontSize = 13.sp,
fontWeight = FontWeight.Black,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.width(width)
)
Text(
text = meta?.releaseInfo?.take(4) ?: meta?.released?.take(4) ?: "2026",
color = Color.White.copy(alpha = 0.5f),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.width(width)
)
}
}
}
@Composable
internal fun TvEpisodeLayoutPreview(profile: UserProfile, lang: String, onSelect: (String) -> Unit) {
val isHorizontal = profile.safeEpisodeCardsLayout == "horizontal"
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
TvLayoutPreviewCard(
title = AppStrings.t(lang, "settings.episode_layout_list"),
selected = !isHorizontal,
horizontal = false,
onClick = { onSelect("list") }
)
Spacer(Modifier.width(20.dp))
TvLayoutPreviewCard(
title = AppStrings.t(lang, "settings.episode_layout_horizontal"),
selected = isHorizontal,
horizontal = true,
onClick = { onSelect("horizontal") }
)
}
}
@Composable
private fun TvLayoutPreviewCard(
title: String,
selected: Boolean,
horizontal: Boolean,
onClick: () -> Unit
) {
var focused by remember { mutableStateOf(false) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Surface(
onClick = onClick,
modifier = Modifier
.size(if (horizontal) 168.dp else 140.dp, if (horizontal) 106.dp else 118.dp)
.onFocusChanged { focused = it.isFocused },
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(14.dp)),
colors = ClickableSurfaceDefaults.colors(
containerColor = Color(0xFF171B24),
contentColor = Color.White
),
border = ClickableSurfaceDefaults.border(
border = Border(
androidx.compose.foundation.BorderStroke(1.dp, if (selected) Color.White.copy(alpha = 0.75f) else Color.White.copy(alpha = 0.14f)),
shape = RoundedCornerShape(14.dp)
),
focusedBorder = Border(
androidx.compose.foundation.BorderStroke(2.dp, Color.White),
shape = RoundedCornerShape(14.dp)
)
)
) {
Box(modifier = Modifier.fillMaxSize().padding(10.dp)) {
if (horizontal) {
Column(
verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.clip(RoundedCornerShape(6.dp))
.background(Color.White.copy(alpha = 0.14f))
)
Box(
modifier = Modifier
.fillMaxWidth(0.88f)
.height(4.dp)
.clip(RoundedCornerShape(2.dp))
.background(Color.White.copy(alpha = 0.55f))
)
Box(
modifier = Modifier
.fillMaxWidth(0.65f)
.height(3.dp)
.clip(RoundedCornerShape(2.dp))
.background(Color.White.copy(alpha = 0.22f))
)
}
} else {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.width(48.dp)
.fillMaxHeight()
.clip(RoundedCornerShape(6.dp))
.background(Color.White.copy(alpha = 0.14f))
)
Column(
verticalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.weight(1f)
) {
Box(
modifier = Modifier
.fillMaxWidth(0.85f)
.height(4.dp)
.clip(RoundedCornerShape(2.dp))
.background(Color.White.copy(alpha = 0.55f))
)
Box(
modifier = Modifier
.fillMaxWidth(0.6f)
.height(3.dp)
.clip(RoundedCornerShape(2.dp))
.background(Color.White.copy(alpha = 0.22f))
)
}
}
}
}
}
Spacer(Modifier.height(8.dp))
Text(
title,
color = if (selected || focused) Color.White else Color.White.copy(alpha = 0.6f),
fontSize = 12.sp,
fontWeight = FontWeight.Black
)
}
}

View file

@ -1,489 +0,0 @@
@file:OptIn(androidx.tv.material3.ExperimentalTvMaterial3Api::class, androidx.compose.material3.ExperimentalMaterial3Api::class)
package com.fluxa.app.ui.catalog
import com.fluxa.app.common.AppStrings
import com.fluxa.app.data.local.*
import com.fluxa.app.data.remote.*
import com.fluxa.app.data.repository.*
import com.fluxa.app.domain.discovery.*
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.ui.res.painterResource
import com.fluxa.app.R
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle
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.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.Surface
import coil3.compose.AsyncImage
import java.util.Locale
@Composable
internal fun AccountSettings(
profile: UserProfile,
lang: String,
onSwitchProfiles: () -> Unit,
onConnectTrakt: () -> Unit,
onConnectMal: () -> Unit,
onConnectSimkl: () -> Unit,
onUpdateProfile: (UserProfile) -> Unit
) {
var tmdbExpanded by remember { mutableStateOf(false) }
SettingsSection(
AppStrings.t(lang, "auto.account_sync"),
AppStrings.t(lang, "auto.account_devices_and_sync")
) {
SettingsActionTile(
title = AppStrings.t(lang, "settings.switch_profiles"),
subtitle = AppStrings.t(lang, "settings.switch_profiles_desc"),
icon = FluxaIcons.AccountCircle,
onClick = onSwitchProfiles
)
}
SettingsSection(
AppStrings.t(lang, "settings.sync_with"),
AppStrings.t(lang, "settings.sync_with_desc")
) {
SettingsConnectionTile(
title = AppStrings.t(lang, "brand.trakt"),
iconRes = R.drawable.ic_trakt,
value = if (!profile.traktAccessToken.isNullOrBlank()) {
AppStrings.t(lang, "auto.connected")
} else {
AppStrings.t(lang, "auto.not_connected")
},
onClick = onConnectTrakt
)
SettingsConnectionTile(
title = AppStrings.t(lang, "brand.myanimelist"),
iconRes = R.drawable.ic_myanimelist,
value = if (!profile.malAccessToken.isNullOrBlank()) AppStrings.t(lang, "auto.connected") else AppStrings.t(lang, "auto.not_connected"),
onClick = onConnectMal
)
SettingsConnectionTile(
title = AppStrings.t(lang, "brand.simkl"),
iconRes = R.drawable.ic_simkl,
value = if (!profile.simklAccessToken.isNullOrBlank()) AppStrings.t(lang, "auto.connected") else AppStrings.t(lang, "auto.not_connected"),
onClick = onConnectSimkl
)
if (!profile.traktAccessToken.isNullOrBlank() || !profile.malAccessToken.isNullOrBlank() || !profile.simklAccessToken.isNullOrBlank()) {
SettingsActionTile(
title = AppStrings.t(lang, "auto.disconnect"),
subtitle = AppStrings.t(lang, "integration.connected_accounts"),
icon = FluxaIcons.Logout,
accent = Color(0xFFFF6B6B),
onClick = {
onUpdateProfile(
profile.copy(
traktAccessToken = null,
traktRefreshToken = null,
traktLastSyncAt = null,
traktLastSyncedItems = null,
traktLastContinueWatchingCount = null,
traktLastWatchlistCount = null,
malAccessToken = null,
malRefreshToken = null,
simklAccessToken = null
)
)
}
)
}
}
SettingsSection(
AppStrings.t(lang, "settings.apis"),
AppStrings.t(lang, "settings.apis_desc")
) {
SettingsActionTile(
title = AppStrings.t(lang, "settings.tmdb_api"),
subtitle = if (profile.safeTmdbApiKey.isNotBlank()) AppStrings.t(lang, "settings.tmdb_api_configured") else AppStrings.t(lang, "settings.tmdb_api_not_configured"),
icon = FluxaIcons.Storage,
onClick = { tmdbExpanded = !tmdbExpanded }
)
if (tmdbExpanded) {
SettingsTextFieldTile(
title = AppStrings.t(lang, "settings.tmdb_api_key"),
subtitle = AppStrings.t(lang, "settings.tmdb_api_key_desc"),
value = profile.safeTmdbApiKey,
placeholder = AppStrings.t(lang, "settings.tmdb_api_key_placeholder"),
onValueChange = { onUpdateProfile(profile.copy(tmdbApiKey = it.trim())) }
)
if (profile.safeTmdbApiKey.isNotBlank()) {
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_cast_images"),
subtitle = AppStrings.t(lang, "settings.tmdb_cast_images_desc"),
checked = profile.safeTmdbCastImagesEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbCastImagesEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_similar_results"),
subtitle = AppStrings.t(lang, "settings.tmdb_similar_results_desc"),
checked = profile.safeTmdbSimilarResultsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbSimilarResultsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_trailers"),
subtitle = AppStrings.t(lang, "settings.tmdb_trailers_desc"),
checked = profile.safeTmdbTrailersEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbTrailersEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_recommendations"),
subtitle = AppStrings.t(lang, "settings.tmdb_recommendations_desc"),
checked = profile.safeTmdbRecommendationsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbRecommendationsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_collection_info"),
subtitle = AppStrings.t(lang, "settings.tmdb_collection_info_desc"),
checked = profile.safeTmdbCollectionInfoEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbCollectionInfoEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_episode_images"),
subtitle = AppStrings.t(lang, "settings.tmdb_episode_images_desc"),
checked = profile.safeTmdbEpisodeImagesEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbEpisodeImagesEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_logos_backdrops"),
subtitle = AppStrings.t(lang, "settings.tmdb_logos_backdrops_desc"),
checked = profile.safeTmdbLogosBackdropsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbLogosBackdropsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_ratings"),
subtitle = AppStrings.t(lang, "settings.tmdb_ratings_desc"),
checked = profile.safeTmdbRatingsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbRatingsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_basic_info"),
subtitle = AppStrings.t(lang, "settings.tmdb_basic_info_desc"),
checked = profile.safeTmdbBasicInfoEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbBasicInfoEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_details"),
subtitle = AppStrings.t(lang, "settings.tmdb_details_desc"),
checked = profile.safeTmdbDetailsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbDetailsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_productions"),
subtitle = AppStrings.t(lang, "settings.tmdb_productions_desc"),
checked = profile.safeTmdbProductionsEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbProductionsEnabled = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.tmdb_networks"),
subtitle = AppStrings.t(lang, "settings.tmdb_networks_desc"),
checked = profile.safeTmdbNetworksEnabled,
onToggle = { onUpdateProfile(profile.copy(tmdbNetworksEnabled = it)) }
)
}
}
}
}
@Composable
internal fun InterfaceSettings(profile: UserProfile, lang: String, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.interface_c0c2eda7"), AppStrings.t(lang, "auto.tune_language_and_visual_layout")) {
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.interface_language"),
subtitle = AppStrings.t(lang, "auto.app_copy_and_category_labels"),
options = listOf(
ChoiceOption("none", AppStrings.t(lang, "settings.none")),
ChoiceOption("tr", languageDisplayName("tr", lang)),
ChoiceOption("en", languageDisplayName("en", lang))
),
selected = profile.safeLanguage,
onSelect = { onUpdateProfile(profile.copy(language = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.card_layout"),
subtitle = AppStrings.t(lang, "auto.how_rows_and_home_shelves_look"),
options = listOf(
ChoiceOption("vertical", AppStrings.t(lang, "auto.vertical_layout")),
ChoiceOption("horizontal", AppStrings.t(lang, "auto.horizontal"))
),
selected = profile.safeCardLayout,
onSelect = { onUpdateProfile(profile.copy(cardLayout = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.continue_watching_layout"),
subtitle = AppStrings.t(lang, "auto.show_that_shelf_as_posters_or_episode_cards"),
options = listOf(
ChoiceOption("vertical", AppStrings.t(lang, "auto.vertical_layout")),
ChoiceOption("horizontal", AppStrings.t(lang, "auto.horizontal")),
ChoiceOption("inherit", AppStrings.t(lang, "auto.match_global"))
),
selected = profile.safeContinueWatchingLayout,
onSelect = { onUpdateProfile(profile.copy(continueWatchingLayout = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.series_artwork"),
subtitle = AppStrings.t(lang, "auto.choose_episode_stills_or_regular_artwork_for"),
options = listOf(
ChoiceOption("episode", AppStrings.t(lang, "auto.episode_cover")),
ChoiceOption("poster", AppStrings.t(lang, "auto.poster")),
ChoiceOption("background", AppStrings.t(lang, "auto.backdrop"))
),
selected = profile.safeContinueWatchingArtwork,
onSelect = { onUpdateProfile(profile.copy(continueWatchingArtwork = it)) }
)
}
}
@Composable
internal fun PlaybackSettings(profile: UserProfile, lang: String, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.playback"), AppStrings.t(lang, "auto.shape_how_playback_behaves")) {
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.preferred_audio_language"),
subtitle = AppStrings.t(lang, "auto.default_audio_track_preference"),
options = audioLanguageOptions(lang),
selected = profile.safePreferredAudioLanguage,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(preferredAudioLanguage = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.skip_forward"),
subtitle = AppStrings.t(lang, "auto.how_far_right_arrow_should_jump"),
options = listOf(ChoiceOption("10", "10s"), ChoiceOption("15", "15s"), ChoiceOption("30", "30s")),
selected = profile.safeSeekForwardSeconds.toString(),
onSelect = { onUpdateProfile(profile.sanitizedUpdate(seekForwardSeconds = it.toInt())) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.skip_back"),
subtitle = AppStrings.t(lang, "auto.how_far_left_arrow_should_rewind"),
options = listOf(ChoiceOption("10", "10s"), ChoiceOption("15", "15s"), ChoiceOption("30", "30s")),
selected = profile.safeSeekBackwardSeconds.toString(),
onSelect = { onUpdateProfile(profile.sanitizedUpdate(seekBackwardSeconds = it.toInt())) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "auto.auto_skip_intros"),
subtitle = AppStrings.t(lang, "auto.prefer_skipping_intros_where_supported"),
checked = profile.safeAutoSkipIntro,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(autoSkipIntro = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.stream_source_selection"),
subtitle = AppStrings.t(lang, "settings.stream_source_selection_desc"),
options = streamSourceSelectionOptions(lang),
selected = profile.safeStreamSourceSelectionMode,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(streamSourceSelectionMode = it)) }
)
if (profile.safeStreamSourceSelectionMode == STREAM_SOURCE_MODE_REGEX) {
SettingsTextFieldTile(
title = AppStrings.t(lang, "settings.regex_pattern"),
subtitle = AppStrings.t(lang, "settings.regex_pattern_desc"),
value = profile.safeStreamSourceRegexPattern,
placeholder = AppStrings.t(lang, "settings.regex_pattern_placeholder"),
onValueChange = { onUpdateProfile(profile.sanitizedUpdate(streamSourceRegexPattern = it)) }
)
}
SettingsToggleTile(
title = AppStrings.t(lang, "auto.auto_play_next_episode"),
subtitle = AppStrings.t(lang, "auto.queue_the_next_episode_automatically"),
checked = profile.safeAutoPlayNextEpisode,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(autoPlayNextEpisode = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.try_binge_group"),
subtitle = AppStrings.t(lang, "settings.try_binge_group_desc"),
checked = profile.safeTryBingeGroup,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(tryBingeGroup = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.preferred_player"),
subtitle = AppStrings.t(lang, "auto.choose_between_internal_or_external_playback"),
options = listOf(
ChoiceOption("exoplayer", "ExoPlayer"),
ChoiceOption("mpv", "MPV")
),
selected = profile.safePreferredPlayer,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(preferredPlayer = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.dv_fallback"),
subtitle = AppStrings.t(lang, "settings.dv_fallback_desc"),
options = dolbyVisionFallbackOptions(lang),
selected = profile.safeDolbyVisionFallbackMode,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(dolbyVisionFallbackMode = it)) }
)
if (profile.safeDolbyVisionFallbackMode == "auto") {
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.dv_rpu_mode"),
subtitle = AppStrings.t(lang, "settings.dv_rpu_mode_desc"),
options = dvRpuModeOptions(lang),
selected = profile.safeDvRpuMode.toString(),
onSelect = { onUpdateProfile(profile.sanitizedUpdate(dvRpuMode = it.toIntOrNull() ?: 2)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "settings.dv_zero_level5"),
subtitle = AppStrings.t(lang, "settings.dv_zero_level5_desc"),
checked = profile.safeDvZeroLevel5,
onToggle = { onUpdateProfile(profile.sanitizedUpdate(dvZeroLevel5 = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.dv_hdr10plus_mode"),
subtitle = AppStrings.t(lang, "settings.dv_hdr10plus_mode_desc"),
options = dvHdr10PlusModeOptions(lang),
selected = profile.safeDvHdr10PlusMode,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(dvHdr10PlusMode = it)) }
)
}
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.audio_decoder_mode"),
subtitle = AppStrings.t(lang, "settings.audio_decoder_mode_desc"),
options = audioDecoderModeOptions(lang),
selected = profile.safeAudioDecoderMode,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(audioDecoderMode = it)) }
)
}
SubtitleSettings(profile, lang, onUpdateProfile)
}
@Composable
internal fun DownloadSettings(profile: UserProfile, lang: String, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.downloads"), AppStrings.t(lang, "settings.downloads_desc")) {
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.download_source_selection"),
subtitle = AppStrings.t(lang, "settings.download_source_selection_desc"),
options = streamSourceSelectionOptions(lang),
selected = profile.safeDownloadSourceSelectionMode,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(downloadSourceSelectionMode = it)) }
)
if (profile.safeDownloadSourceSelectionMode == STREAM_SOURCE_MODE_REGEX) {
SettingsTextFieldTile(
title = AppStrings.t(lang, "settings.regex_pattern"),
subtitle = AppStrings.t(lang, "settings.regex_pattern_desc"),
value = profile.safeDownloadSourceRegexPattern,
placeholder = AppStrings.t(lang, "settings.regex_pattern_placeholder"),
onValueChange = { onUpdateProfile(profile.sanitizedUpdate(downloadSourceRegexPattern = it)) }
)
}
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.download_subtitle"),
subtitle = AppStrings.t(lang, "settings.download_subtitle_desc"),
options = downloadSubtitleOptions(lang),
selected = profile.safeDownloadSubtitleLanguage,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(downloadSubtitleLanguage = it)) }
)
}
}
@Composable
internal fun SubtitleSettings(profile: UserProfile, lang: String, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.subtitles_fc449c82"), AppStrings.t(lang, "auto.subtitle_language_size_and_readability")) {
SettingsToggleTile(
title = AppStrings.t(lang, "auto.auto_enable_subtitles"),
subtitle = AppStrings.t(lang, "auto.enable_subtitles_automatically_when_availabl"),
checked = profile.safeAutoEnableSubtitles,
onToggle = { onUpdateProfile(profile.copy(autoEnableSubtitles = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.preferred_subtitle_language"),
subtitle = AppStrings.t(lang, "auto.default_subtitle_track_preference"),
options = subtitleLanguageOptions(lang),
selected = profile.safePreferredSubtitleLanguage,
onSelect = { onUpdateProfile(profile.copy(preferredSubtitleLanguage = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.subtitle_size"),
subtitle = AppStrings.t(lang, "auto.tune_readability_on_tv_and_mobile"),
options = listOf(
ChoiceOption("18", "18"),
ChoiceOption("20", "20"),
ChoiceOption("24", "24"),
ChoiceOption("28", "28")
),
selected = profile.safeSubtitleSize.toInt().toString(),
onSelect = { onUpdateProfile(profile.copy(subtitleSize = it.toFloat())) }
)
// Preview Background
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.Black.copy(alpha = 0.6f))
.padding(24.dp),
contentAlignment = Alignment.Center
) {
SubtitlePreviewText(
text = AppStrings.t(lang, "settings.subtitle_preview"),
profile = profile
)
}
}
}
@Composable
internal fun AddonSettings(profile: UserProfile, lang: String, onManageAddons: () -> Unit, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.add_ons_and_torrent"), AppStrings.t(lang, "auto.sources_and_stream_engine_preferences")) {
SettingsActionTile(
title = AppStrings.t(lang, "auto.manage_add_ons"),
subtitle = AppStrings.t(lang, "auto.installed_add_ons_and_settings"),
icon = FluxaIcons.Extension,
onClick = onManageAddons
)
}
}
@Composable
internal fun SystemSettings(profile: UserProfile, lang: String, onReboot: () -> Unit, onLogout: () -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.system"), AppStrings.t(lang, "auto.app_and_device_actions")) {
SettingsInfoTile(AppStrings.t(lang, "auto.version"), "1.2.5", FluxaIcons.Settings)
SettingsActionTile(
title = AppStrings.t(lang, "auto.restart_app"),
subtitle = AppStrings.t(lang, "auto.refresh_the_app_after_playback_or_network_ch"),
icon = FluxaIcons.Settings,
onClick = onReboot
)
SettingsActionTile(
title = AppStrings.t(lang, "auto.log_out"),
subtitle = AppStrings.t(lang, "auto.sign_out_from_the_active_profile_on_this_dev"),
icon = FluxaIcons.Logout,
accent = Color(0xFFFF6B6B),
onClick = onLogout
)
}
}

View file

@ -0,0 +1,181 @@
@file:OptIn(androidx.tv.material3.ExperimentalTvMaterial3Api::class, androidx.compose.material3.ExperimentalMaterial3Api::class)
package com.fluxa.app.ui.catalog
import com.fluxa.app.common.AppStrings
import com.fluxa.app.data.local.*
import com.fluxa.app.data.remote.*
import com.fluxa.app.data.repository.*
import com.fluxa.app.domain.discovery.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.*
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun DownloadSettings(profile: UserProfile, lang: String, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.downloads"), AppStrings.t(lang, "settings.downloads_desc")) {
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.download_source_selection"),
subtitle = AppStrings.t(lang, "settings.download_source_selection_desc"),
options = streamSourceSelectionOptions(lang),
selected = profile.safeDownloadSourceSelectionMode,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(downloadSourceSelectionMode = it)) }
)
if (profile.safeDownloadSourceSelectionMode == STREAM_SOURCE_MODE_REGEX) {
SettingsTextFieldTile(
title = AppStrings.t(lang, "settings.regex_pattern"),
subtitle = AppStrings.t(lang, "settings.regex_pattern_desc"),
value = profile.safeDownloadSourceRegexPattern,
placeholder = AppStrings.t(lang, "settings.regex_pattern_placeholder"),
onValueChange = { onUpdateProfile(profile.sanitizedUpdate(downloadSourceRegexPattern = it)) }
)
}
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.download_subtitle"),
subtitle = AppStrings.t(lang, "settings.download_subtitle_desc"),
options = downloadSubtitleOptions(lang),
selected = profile.safeDownloadSubtitleLanguage,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(downloadSubtitleLanguage = it)) }
)
}
}
@Composable
internal fun SubtitleSettings(profile: UserProfile, lang: String, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.subtitles_fc449c82"), AppStrings.t(lang, "auto.subtitle_language_size_and_readability")) {
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.preferred_audio_language"),
subtitle = AppStrings.t(lang, "settings.preferred_audio_language_desc"),
options = audioLanguageOptions(lang),
selected = profile.safePreferredAudioLanguage,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(preferredAudioLanguage = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.secondary_audio_language"),
subtitle = AppStrings.t(lang, "settings.secondary_audio_language_desc"),
options = audioLanguageOptions(lang),
selected = profile.safeSecondaryAudioLanguage,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(secondaryAudioLanguage = it)) }
)
SettingsToggleTile(
title = AppStrings.t(lang, "auto.auto_enable_subtitles"),
subtitle = AppStrings.t(lang, "auto.enable_subtitles_automatically_when_availabl"),
checked = profile.safeAutoEnableSubtitles,
onToggle = { onUpdateProfile(profile.copy(autoEnableSubtitles = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.preferred_subtitle_language"),
subtitle = AppStrings.t(lang, "auto.default_subtitle_track_preference"),
options = subtitleLanguageOptions(lang),
selected = profile.safePreferredSubtitleLanguage,
onSelect = { onUpdateProfile(profile.copy(preferredSubtitleLanguage = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "settings.secondary_subtitle_language"),
subtitle = AppStrings.t(lang, "settings.secondary_subtitle_language_desc"),
options = subtitleLanguageOptions(lang),
selected = profile.safeSecondarySubtitleLanguage,
onSelect = { onUpdateProfile(profile.sanitizedUpdate(secondarySubtitleLanguage = it)) }
)
SettingsChoiceTile(
title = AppStrings.t(lang, "auto.subtitle_size"),
subtitle = AppStrings.t(lang, "auto.tune_readability_on_tv_and_mobile"),
options = listOf(
ChoiceOption("18", "18"),
ChoiceOption("20", "20"),
ChoiceOption("24", "24"),
ChoiceOption("28", "28")
),
selected = profile.safeSubtitleSize.toInt().toString(),
onSelect = { onUpdateProfile(profile.copy(subtitleSize = it.toFloat())) }
)
SettingsColorOpacityTile(
title = AppStrings.t(lang, "settings.subtitle_text"),
subtitle = AppStrings.t(lang, "settings.subtitle_text_desc"),
opacityTitle = AppStrings.t(lang, "auto.text_transparency"),
colorOptions = mobileSubtitleColorOptions(lang),
selectedColor = profile.safeSubtitleColor,
opacity = profile.safeSubtitleTextOpacity,
onColorSelect = { onUpdateProfile(profile.copy(subtitleColor = it)) },
onOpacity = { onUpdateProfile(profile.copy(subtitleTextOpacity = it)) }
)
SettingsColorOpacityTile(
title = AppStrings.t(lang, "settings.subtitle_background"),
subtitle = AppStrings.t(lang, "settings.subtitle_background_desc"),
opacityTitle = AppStrings.t(lang, "auto.background_transparency"),
colorOptions = mobileSubtitleColorOptions(lang),
selectedColor = profile.safeSubtitleBackgroundColor,
opacity = profile.safeSubtitleBackgroundOpacity,
onColorSelect = { onUpdateProfile(profile.copy(subtitleBackgroundColor = it)) },
onOpacity = { onUpdateProfile(profile.copy(subtitleBackgroundOpacity = it)) }
)
SettingsColorOpacityTile(
title = AppStrings.t(lang, "settings.subtitle_outline"),
subtitle = AppStrings.t(lang, "settings.subtitle_outline_desc"),
opacityTitle = AppStrings.t(lang, "settings.subtitle.outline_opacity"),
colorOptions = mobileSubtitleColorOptions(lang),
selectedColor = profile.safeSubtitleOutlineColor,
opacity = profile.safeSubtitleOutlineOpacity,
onColorSelect = { onUpdateProfile(profile.copy(subtitleOutlineColor = it)) },
onOpacity = { onUpdateProfile(profile.copy(subtitleOutlineOpacity = it)) }
)
androidx.compose.foundation.layout.Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.Black.copy(alpha = 0.6f))
.padding(24.dp),
contentAlignment = Alignment.Center
) {
SubtitlePreviewText(
text = AppStrings.t(lang, "settings.subtitle_preview"),
profile = profile
)
}
}
}
@Composable
internal fun AddonSettings(profile: UserProfile, lang: String, onManageAddons: () -> Unit, onUpdateProfile: (UserProfile) -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.add_ons_and_torrent"), AppStrings.t(lang, "auto.sources_and_stream_engine_preferences")) {
SettingsActionTile(
title = AppStrings.t(lang, "auto.manage_add_ons"),
subtitle = AppStrings.t(lang, "auto.installed_add_ons_and_settings"),
icon = FluxaIcons.Extension,
onClick = onManageAddons
)
}
}
@Composable
internal fun SystemSettings(profile: UserProfile, lang: String, onReboot: () -> Unit, onLogout: () -> Unit) {
SettingsSection(AppStrings.t(lang, "auto.system"), AppStrings.t(lang, "auto.app_and_device_actions")) {
SettingsInfoTile(AppStrings.t(lang, "auto.version"), "1.2.5", FluxaIcons.Settings)
SettingsActionTile(
title = AppStrings.t(lang, "auto.restart_app"),
subtitle = AppStrings.t(lang, "auto.refresh_the_app_after_playback_or_network_ch"),
icon = FluxaIcons.Settings,
onClick = onReboot
)
SettingsActionTile(
title = AppStrings.t(lang, "auto.log_out"),
subtitle = AppStrings.t(lang, "auto.sign_out_from_the_active_profile_on_this_dev"),
icon = FluxaIcons.Logout,
accent = FluxaColors.errorRed,
onClick = onLogout
)
}
}