feat: added metascreen reordering

This commit is contained in:
tapframe 2026-04-03 00:14:29 +05:30
parent bd5077bf97
commit 333063bd1e
19 changed files with 937 additions and 245 deletions

View file

@ -158,6 +158,7 @@ kotlin {
implementation(libs.supabase.functions)
implementation(libs.quickjs.kt)
implementation(libs.ksoup)
implementation(libs.reorderable)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)

View file

@ -10,6 +10,7 @@ import androidx.core.view.WindowCompat
import com.nuvio.app.core.storage.PlatformLocalAccountDataCleaner
import com.nuvio.app.features.addons.AddonStorage
import com.nuvio.app.features.library.LibraryStorage
import com.nuvio.app.features.details.MetaScreenSettingsStorage
import com.nuvio.app.features.home.HomeCatalogSettingsStorage
import com.nuvio.app.features.mdblist.MdbListSettingsStorage
import com.nuvio.app.features.player.PlayerSettingsStorage
@ -42,6 +43,7 @@ class MainActivity : ComponentActivity() {
AddonStorage.initialize(applicationContext)
LibraryStorage.initialize(applicationContext)
WatchedStorage.initialize(applicationContext)
MetaScreenSettingsStorage.initialize(applicationContext)
HomeCatalogSettingsStorage.initialize(applicationContext)
PlayerSettingsStorage.initialize(applicationContext)
ProfileStorage.initialize(applicationContext)

View file

@ -0,0 +1,26 @@
package com.nuvio.app.features.details
import android.content.Context
import android.content.SharedPreferences
import com.nuvio.app.core.storage.ProfileScopedKey
internal actual object MetaScreenSettingsStorage {
private const val preferencesName = "nuvio_meta_screen_settings"
private const val payloadKey = "meta_screen_settings_payload"
private var preferences: SharedPreferences? = null
fun initialize(context: Context) {
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
}
actual fun loadPayload(): String? =
preferences?.getString(ProfileScopedKey.of(payloadKey), null)
actual fun savePayload(payload: String) {
preferences
?.edit()
?.putString(ProfileScopedKey.of(payloadKey), payload)
?.apply()
}
}

View file

@ -100,6 +100,7 @@ import com.nuvio.app.features.profiles.ProfileSwitcherTab
import com.nuvio.app.features.search.SearchScreen
import com.nuvio.app.features.settings.SettingsScreen
import com.nuvio.app.features.settings.HomescreenSettingsScreen
import com.nuvio.app.features.settings.MetaScreenSettingsScreen
import com.nuvio.app.features.settings.ContinueWatchingSettingsScreen
import com.nuvio.app.features.settings.AddonsSettingsScreen
import com.nuvio.app.features.settings.PluginsSettingsScreen
@ -132,6 +133,9 @@ data class DetailRoute(val type: String, val id: String)
@Serializable
object HomescreenSettingsRoute
@Serializable
object MetaScreenSettingsRoute
@Serializable
object ContinueWatchingSettingsRoute
@ -537,6 +541,7 @@ private fun MainAppContent(
onContinueWatchingLongPress = onContinueWatchingLongPress,
onSwitchProfile = onSwitchProfile,
onHomescreenSettingsClick = { navController.navigate(HomescreenSettingsRoute) },
onMetaScreenSettingsClick = { navController.navigate(MetaScreenSettingsRoute) },
onContinueWatchingSettingsClick = { navController.navigate(ContinueWatchingSettingsRoute) },
onAddonsSettingsClick = { navController.navigate(AddonsSettingsRoute) },
onPluginsSettingsClick = { navController.navigate(PluginsSettingsRoute) },
@ -855,6 +860,11 @@ private fun MainAppContent(
onBack = { navController.popBackStack() },
)
}
composable<MetaScreenSettingsRoute> {
MetaScreenSettingsScreen(
onBack = { navController.popBackStack() },
)
}
composable<ContinueWatchingSettingsRoute> {
ContinueWatchingSettingsScreen(
onBack = { navController.popBackStack() },
@ -935,6 +945,7 @@ private fun AppTabHost(
onContinueWatchingLongPress: ((ContinueWatchingItem) -> Unit)? = null,
onSwitchProfile: (() -> Unit)? = null,
onHomescreenSettingsClick: () -> Unit = {},
onMetaScreenSettingsClick: () -> Unit = {},
onContinueWatchingSettingsClick: () -> Unit = {},
onAddonsSettingsClick: () -> Unit = {},
onPluginsSettingsClick: () -> Unit = {},
@ -980,6 +991,7 @@ private fun AppTabHost(
modifier = Modifier.fillMaxSize(),
onSwitchProfile = onSwitchProfile,
onHomescreenClick = onHomescreenSettingsClick,
onMetaScreenClick = onMetaScreenSettingsClick,
onContinueWatchingClick = onContinueWatchingSettingsClick,
onAddonsClick = onAddonsSettingsClick,
onPluginsClick = onPluginsSettingsClick,

View file

@ -3,6 +3,7 @@ package com.nuvio.app.core.storage
import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.catalog.CatalogRepository
import com.nuvio.app.features.details.MetaDetailsRepository
import com.nuvio.app.features.details.MetaScreenSettingsRepository
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.home.HomeRepository
import com.nuvio.app.features.library.LibraryRepository
@ -29,6 +30,7 @@ internal object LocalAccountDataCleaner {
PluginRepository.clearLocalState()
HomeRepository.clear()
HomeCatalogSettingsRepository.clearLocalState()
MetaScreenSettingsRepository.clearLocalState()
LibraryRepository.clearLocalState()
WatchProgressRepository.clearLocalState()
WatchedRepository.clearLocalState()

View file

@ -82,6 +82,7 @@ import com.nuvio.app.features.watched.WatchedRepository
import com.nuvio.app.features.watched.previousReleasedEpisodesBefore
import com.nuvio.app.features.watched.releasedEpisodesForSeason
import com.nuvio.app.features.watchprogress.CurrentDateProvider
import com.nuvio.app.features.watchprogress.WatchProgressEntry
import com.nuvio.app.features.watchprogress.WatchProgressRepository
import com.nuvio.app.features.watchprogress.buildPlaybackVideoId
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
@ -100,6 +101,10 @@ fun MetaDetailsScreen(
) {
val uiState by MetaDetailsRepository.uiState.collectAsStateWithLifecycle()
val displayedMeta = MetaDetailsRepository.peek(type, id)
val metaScreenSettingsUiState by remember {
MetaScreenSettingsRepository.ensureLoaded()
MetaScreenSettingsRepository.uiState
}.collectAsStateWithLifecycle()
val traktAuthUiState by remember {
TraktAuthRepository.ensureLoaded()
TraktAuthRepository.uiState
@ -343,6 +348,76 @@ fun MetaDetailsScreen(
else -> "Play"
}
}
val onPrimaryPlayClick: () -> Unit = {
when {
(meta.type == "series" || hasEpisodes) && seriesAction != null -> {
onPlay?.invoke(
meta.type,
seriesStreamVideoId ?: seriesAction.videoId,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
seriesAction.seasonNumber,
seriesAction.episodeNumber,
seriesAction.episodeTitle,
seriesAction.episodeThumbnail,
seriesPauseDescription,
seriesAction.resumePositionMs,
)
}
else -> {
onPlay?.invoke(
meta.type,
meta.id,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
null,
null,
null,
null,
meta.description,
movieProgress?.lastPositionMs,
)
}
}
}
val onEpisodePlayClick: (MetaVideo) -> Unit = { video ->
val season = video.season
val episode = video.episode
val playbackVideoId = buildPlaybackVideoId(
parentMetaId = meta.id,
seasonNumber = season,
episodeNumber = episode,
fallbackVideoId = video.id,
)
val streamVideoId = video.id.takeIf { it.isNotBlank() } ?: playbackVideoId
val savedProgress = watchProgressUiState.byVideoId[playbackVideoId]
?.takeUnless { it.isCompleted }
onPlay?.invoke(
meta.type,
streamVideoId,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
season,
episode,
video.title,
video.thumbnail,
video.overview,
savedProgress?.lastPositionMs,
)
}
val scrollState = rememberScrollState()
val density = LocalDensity.current
val safeAreaTopPx = with(density) {
@ -390,179 +465,66 @@ fun MetaDetailsScreen(
verticalArrangement = Arrangement.spacedBy(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
DetailActionButtons(
playLabel = playButtonLabel,
saveLabel = if (isSaved) "Saved" else "Save",
isSaved = isSaved,
ConfiguredMetaSections(
settings = metaScreenSettingsUiState,
meta = meta,
isTablet = isTablet,
onPlayClick = {
when {
(meta.type == "series" || hasEpisodes) && seriesAction != null -> {
onPlay?.invoke(
meta.type,
seriesStreamVideoId ?: seriesAction.videoId,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
seriesAction.seasonNumber,
seriesAction.episodeNumber,
seriesAction.episodeTitle,
seriesAction.episodeThumbnail,
seriesPauseDescription,
seriesAction.resumePositionMs,
)
}
else -> {
onPlay?.invoke(
meta.type,
meta.id,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
null,
null,
null,
null,
meta.description,
movieProgress?.lastPositionMs,
)
playButtonLabel = playButtonLabel,
isSaved = isSaved,
onPrimaryPlayClick = onPrimaryPlayClick,
onSaveClick = toggleSaved,
hasProductionSection = hasProductionSection,
hasTrailersSection = hasTrailersSection,
hasEpisodes = hasEpisodes,
hasAdditionalInfoSection = hasAdditionalInfoSection,
hasCollectionSection = hasCollectionSection,
hasMoreLikeThisSection = hasMoreLikeThisSection,
shouldShowComments = shouldShowComments,
comments = comments,
isCommentsLoading = isCommentsLoading,
isCommentsLoadingMore = isCommentsLoadingMore,
commentsCurrentPage = commentsCurrentPage,
commentsPageCount = commentsPageCount,
commentsError = commentsError,
onRetryComments = {
detailsScope.launch {
isCommentsLoading = true
commentsError = null
try {
val result = TraktCommentsRepository.getCommentsPage(meta, page = 1, forceRefresh = true)
comments = result.items
commentsCurrentPage = result.currentPage
commentsPageCount = result.pageCount
} catch (e: Exception) {
commentsError = e.message ?: "Failed to load comments"
}
isCommentsLoading = false
}
},
onSaveClick = toggleSaved,
)
DetailMetaInfo(meta = meta)
if (hasEpisodes && hasProductionSection) {
DetailProductionSection(meta = meta)
}
DetailCastSection(cast = meta.cast)
if (shouldShowComments && (isCommentsLoading || comments.isNotEmpty() || !commentsError.isNullOrBlank())) {
DetailCommentsSection(
comments = comments,
isLoading = isCommentsLoading,
isLoadingMore = isCommentsLoadingMore,
canLoadMore = commentsCurrentPage < commentsPageCount,
error = commentsError,
onRetry = {
detailsScope.launch {
isCommentsLoading = true
commentsError = null
try {
val result = TraktCommentsRepository.getCommentsPage(meta, page = 1, forceRefresh = true)
comments = result.items
commentsCurrentPage = result.currentPage
commentsPageCount = result.pageCount
} catch (e: Exception) {
commentsError = e.message ?: "Failed to load comments"
}
isCommentsLoading = false
}
},
onLoadMore = {
detailsScope.launch {
isCommentsLoadingMore = true
try {
val nextPage = commentsCurrentPage + 1
val result = TraktCommentsRepository.getCommentsPage(meta, page = nextPage)
val existingIds = comments.map { it.id }.toSet()
val newComments = result.items.filter { it.id !in existingIds }
comments = comments + newComments
commentsCurrentPage = result.currentPage
commentsPageCount = result.pageCount
} catch (_: Exception) { }
isCommentsLoadingMore = false
}
},
onCommentClick = { review -> selectedComment = review },
)
}
if (hasTrailersSection) {
DetailTrailersSection(
trailers = meta.trailers,
onTrailerClick = resolveTrailer,
)
}
if (!hasEpisodes && hasProductionSection) {
DetailProductionSection(meta = meta)
}
DetailSeriesContent(
meta = meta,
onLoadMoreComments = {
detailsScope.launch {
isCommentsLoadingMore = true
try {
val nextPage = commentsCurrentPage + 1
val result = TraktCommentsRepository.getCommentsPage(meta, page = nextPage)
val existingIds = comments.map { it.id }.toSet()
val newComments = result.items.filter { it.id !in existingIds }
comments = comments + newComments
commentsCurrentPage = result.currentPage
commentsPageCount = result.pageCount
} catch (_: Exception) { }
isCommentsLoadingMore = false
}
},
onCommentClick = { review -> selectedComment = review },
onTrailerClick = resolveTrailer,
progressByVideoId = watchProgressUiState.byVideoId,
watchedKeys = watchedUiState.watchedKeys,
onEpisodeClick = { video ->
val season = video.season
val episode = video.episode
val playbackVideoId = buildPlaybackVideoId(
parentMetaId = meta.id,
seasonNumber = season,
episodeNumber = episode,
fallbackVideoId = video.id,
)
val streamVideoId = video.id.takeIf { it.isNotBlank() } ?: playbackVideoId
val savedProgress = watchProgressUiState.byVideoId[playbackVideoId]
?.takeUnless { it.isCompleted }
onPlay?.invoke(
meta.type,
streamVideoId,
meta.id,
meta.type,
meta.name,
meta.logo,
meta.poster,
meta.background,
season,
episode,
video.title,
video.thumbnail,
video.overview,
savedProgress?.lastPositionMs,
)
},
onEpisodeLongPress = { video ->
selectedEpisodeForActions = video
},
onEpisodeClick = onEpisodePlayClick,
onEpisodeLongPress = { video -> selectedEpisodeForActions = video },
onOpenMeta = onOpenMeta,
)
if (hasEpisodes && hasAdditionalInfoSection) {
DetailAdditionalInfoSection(meta = meta)
}
if (!hasEpisodes && hasAdditionalInfoSection) {
DetailAdditionalInfoSection(meta = meta)
}
if (!hasEpisodes && hasCollectionSection) {
DetailPosterRailSection(
title = meta.collectionName.orEmpty(),
items = meta.collectionItems,
watchedKeys = watchedUiState.watchedKeys,
onPosterClick = onOpenMeta,
)
}
if (hasMoreLikeThisSection) {
DetailPosterRailSection(
title = "More Like This",
items = meta.moreLikeThis,
watchedKeys = watchedUiState.watchedKeys,
onPosterClick = onOpenMeta,
)
}
Spacer(modifier = Modifier.height(32.dp + nuvioPlatformExtraBottomPadding))
}
}
@ -768,6 +730,134 @@ fun MetaDetailsScreen(
}
}
@Composable
private fun ConfiguredMetaSections(
settings: MetaScreenSettingsUiState,
meta: MetaDetails,
isTablet: Boolean,
playButtonLabel: String,
isSaved: Boolean,
onPrimaryPlayClick: () -> Unit,
onSaveClick: () -> Unit,
hasProductionSection: Boolean,
hasTrailersSection: Boolean,
hasEpisodes: Boolean,
hasAdditionalInfoSection: Boolean,
hasCollectionSection: Boolean,
hasMoreLikeThisSection: Boolean,
shouldShowComments: Boolean,
comments: List<TraktCommentReview>,
isCommentsLoading: Boolean,
isCommentsLoadingMore: Boolean,
commentsCurrentPage: Int,
commentsPageCount: Int,
commentsError: String?,
onRetryComments: () -> Unit,
onLoadMoreComments: () -> Unit,
onCommentClick: (TraktCommentReview) -> Unit,
onTrailerClick: (MetaTrailer) -> Unit,
progressByVideoId: Map<String, WatchProgressEntry>,
watchedKeys: Set<String>,
onEpisodeClick: (MetaVideo) -> Unit,
onEpisodeLongPress: (MetaVideo) -> Unit,
onOpenMeta: ((MetaPreview) -> Unit)?,
) {
settings.items
.filter { it.enabled }
.forEach { section ->
when (section.key) {
MetaScreenSectionKey.ACTIONS -> {
DetailActionButtons(
playLabel = playButtonLabel,
saveLabel = if (isSaved) "Saved" else "Save",
isSaved = isSaved,
isTablet = isTablet,
onPlayClick = onPrimaryPlayClick,
onSaveClick = onSaveClick,
)
}
MetaScreenSectionKey.OVERVIEW -> {
DetailMetaInfo(meta = meta)
}
MetaScreenSectionKey.PRODUCTION -> {
if (hasProductionSection) {
DetailProductionSection(meta = meta)
}
}
MetaScreenSectionKey.CAST -> {
DetailCastSection(cast = meta.cast)
}
MetaScreenSectionKey.COMMENTS -> {
if (shouldShowComments && (isCommentsLoading || comments.isNotEmpty() || !commentsError.isNullOrBlank())) {
DetailCommentsSection(
comments = comments,
isLoading = isCommentsLoading,
isLoadingMore = isCommentsLoadingMore,
canLoadMore = commentsCurrentPage < commentsPageCount,
error = commentsError,
onRetry = onRetryComments,
onLoadMore = onLoadMoreComments,
onCommentClick = onCommentClick,
)
}
}
MetaScreenSectionKey.TRAILERS -> {
if (hasTrailersSection) {
DetailTrailersSection(
trailers = meta.trailers,
onTrailerClick = onTrailerClick,
)
}
}
MetaScreenSectionKey.EPISODES -> {
if (hasEpisodes) {
DetailSeriesContent(
meta = meta,
progressByVideoId = progressByVideoId,
watchedKeys = watchedKeys,
onEpisodeClick = onEpisodeClick,
onEpisodeLongPress = onEpisodeLongPress,
)
}
}
MetaScreenSectionKey.DETAILS -> {
if (hasAdditionalInfoSection) {
DetailAdditionalInfoSection(meta = meta)
}
}
MetaScreenSectionKey.COLLECTION -> {
if (!hasEpisodes && hasCollectionSection) {
DetailPosterRailSection(
title = meta.collectionName.orEmpty(),
items = meta.collectionItems,
watchedKeys = watchedKeys,
onPosterClick = onOpenMeta,
)
}
}
MetaScreenSectionKey.MORE_LIKE_THIS -> {
if (hasMoreLikeThisSection) {
DetailPosterRailSection(
title = "More Like This",
items = meta.moreLikeThis,
watchedKeys = watchedKeys,
onPosterClick = onOpenMeta,
)
}
}
}
}
}
private fun detailTabletContentMaxWidth(maxWidth: Dp, isTablet: Boolean): Dp =
if (!isTablet) {
maxWidth

View file

@ -0,0 +1,236 @@
package com.nuvio.app.features.details
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
enum class MetaScreenSectionKey {
ACTIONS,
OVERVIEW,
PRODUCTION,
CAST,
COMMENTS,
TRAILERS,
EPISODES,
DETAILS,
COLLECTION,
MORE_LIKE_THIS,
}
data class MetaScreenSectionItem(
val key: MetaScreenSectionKey,
val title: String,
val description: String,
val enabled: Boolean,
val order: Int,
)
data class MetaScreenSettingsUiState(
val items: List<MetaScreenSectionItem> = emptyList(),
)
@Serializable
private data class StoredMetaScreenSectionPreference(
val key: String,
val enabled: Boolean = true,
val order: Int = 0,
)
@Serializable
private data class StoredMetaScreenSettingsPayload(
val items: List<StoredMetaScreenSectionPreference> = emptyList(),
)
private data class MetaScreenSectionDefinition(
val key: MetaScreenSectionKey,
val title: String,
val description: String,
)
object MetaScreenSettingsRepository {
private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
}
private val definitions = listOf(
MetaScreenSectionDefinition(
key = MetaScreenSectionKey.ACTIONS,
title = "Actions",
description = "Play and save controls.",
),
MetaScreenSectionDefinition(
key = MetaScreenSectionKey.OVERVIEW,
title = "Overview",
description = "Synopsis, ratings, genres, and core credits.",
),
MetaScreenSectionDefinition(
key = MetaScreenSectionKey.PRODUCTION,
title = "Production",
description = "Studios and networks.",
),
MetaScreenSectionDefinition(
key = MetaScreenSectionKey.CAST,
title = "Cast",
description = "Principal cast list.",
),
MetaScreenSectionDefinition(
key = MetaScreenSectionKey.COMMENTS,
title = "Comments",
description = "Trakt comments section.",
),
MetaScreenSectionDefinition(
key = MetaScreenSectionKey.TRAILERS,
title = "Trailers",
description = "Trailer rail and playback shortcuts.",
),
MetaScreenSectionDefinition(
key = MetaScreenSectionKey.EPISODES,
title = "Episodes",
description = "Seasons and episode list for series.",
),
MetaScreenSectionDefinition(
key = MetaScreenSectionKey.DETAILS,
title = "Details",
description = "Runtime, status, release, language, and related info.",
),
MetaScreenSectionDefinition(
key = MetaScreenSectionKey.COLLECTION,
title = "Collection",
description = "Related collection or franchise rail.",
),
MetaScreenSectionDefinition(
key = MetaScreenSectionKey.MORE_LIKE_THIS,
title = "More Like This",
description = "Recommendation rail.",
),
)
private val _uiState = MutableStateFlow(MetaScreenSettingsUiState())
val uiState: StateFlow<MetaScreenSettingsUiState> = _uiState.asStateFlow()
private var hasLoaded = false
private var preferences: MutableMap<MetaScreenSectionKey, StoredMetaScreenSectionPreference> = mutableMapOf()
fun ensureLoaded() {
if (hasLoaded) return
hasLoaded = true
val payload = MetaScreenSettingsStorage.loadPayload().orEmpty().trim()
if (payload.isNotEmpty()) {
val parsed = runCatching {
json.decodeFromString<StoredMetaScreenSettingsPayload>(payload)
}.getOrNull()
if (parsed != null) {
preferences = parsed.items.mapNotNull { item ->
val key = runCatching { MetaScreenSectionKey.valueOf(item.key) }.getOrNull() ?: return@mapNotNull null
key to item
}.toMap().toMutableMap()
}
}
normalizePreferences()
publish()
persist()
}
fun onProfileChanged() {
hasLoaded = false
preferences.clear()
_uiState.value = MetaScreenSettingsUiState()
ensureLoaded()
}
fun clearLocalState() {
hasLoaded = false
preferences.clear()
_uiState.value = MetaScreenSettingsUiState()
}
fun setEnabled(key: MetaScreenSectionKey, enabled: Boolean) {
updatePreference(key) { preference ->
preference.copy(enabled = enabled)
}
}
fun resetToDefaults() {
ensureLoaded()
preferences.clear()
normalizePreferences()
publish()
persist()
}
fun moveByIndex(fromIndex: Int, toIndex: Int) {
ensureLoaded()
val orderedKeys = definitions
.sortedBy { definition -> preferences[definition.key]?.order ?: Int.MAX_VALUE }
.map { it.key }
.toMutableList()
if (fromIndex !in orderedKeys.indices || toIndex !in orderedKeys.indices) return
if (fromIndex == toIndex) return
orderedKeys.add(toIndex, orderedKeys.removeAt(fromIndex))
orderedKeys.forEachIndexed { newIndex, sectionKey ->
val current = preferences[sectionKey] ?: return@forEachIndexed
preferences[sectionKey] = current.copy(order = newIndex)
}
publish()
persist()
}
private fun updatePreference(
key: MetaScreenSectionKey,
transform: (StoredMetaScreenSectionPreference) -> StoredMetaScreenSectionPreference,
) {
ensureLoaded()
val current = preferences[key] ?: return
preferences[key] = transform(current)
publish()
persist()
}
private fun normalizePreferences() {
val normalized = mutableMapOf<MetaScreenSectionKey, StoredMetaScreenSectionPreference>()
definitions.sortedBy { definition -> preferences[definition.key]?.order ?: Int.MAX_VALUE }
.forEachIndexed { index, definition ->
val stored = preferences[definition.key]
normalized[definition.key] = StoredMetaScreenSectionPreference(
key = definition.key.name,
enabled = stored?.enabled ?: true,
order = index,
)
}
preferences = normalized
}
private fun publish() {
_uiState.value = MetaScreenSettingsUiState(
items = definitions
.sortedBy { definition -> preferences[definition.key]?.order ?: Int.MAX_VALUE }
.map { definition ->
val preference = preferences[definition.key]
MetaScreenSectionItem(
key = definition.key,
title = definition.title,
description = definition.description,
enabled = preference?.enabled ?: true,
order = preference?.order ?: 0,
)
},
)
}
private fun persist() {
MetaScreenSettingsStorage.savePayload(
json.encodeToString(
StoredMetaScreenSettingsPayload(
items = preferences.values.sortedBy { it.order },
),
),
)
}
}

View file

@ -0,0 +1,6 @@
package com.nuvio.app.features.details
internal expect object MetaScreenSettingsStorage {
fun loadPayload(): String?
fun savePayload(payload: String)
}

View file

@ -152,6 +152,16 @@ object HomeCatalogSettingsRepository {
}
}
fun resetToDefaults() {
ensureLoaded()
heroEnabled = true
preferences.clear()
normalizePreferences()
publish()
persist()
HomeRepository.applyCurrentSettings()
}
fun moveUp(key: String) {
move(key = key, direction = -1)
}
@ -160,6 +170,25 @@ object HomeCatalogSettingsRepository {
move(key = key, direction = 1)
}
fun moveByIndex(fromIndex: Int, toIndex: Int) {
ensureLoaded()
if (definitions.isEmpty()) return
val orderedKeys = definitions
.sortedBy { definition -> preferences[definition.key]?.order ?: Int.MAX_VALUE }
.map { it.key }
.toMutableList()
if (fromIndex !in orderedKeys.indices || toIndex !in orderedKeys.indices) return
if (fromIndex == toIndex) return
orderedKeys.add(toIndex, orderedKeys.removeAt(fromIndex))
orderedKeys.forEachIndexed { index, itemKey ->
val current = preferences[itemKey] ?: return@forEachIndexed
preferences[itemKey] = current.copy(order = index)
}
publish()
persist()
HomeRepository.applyCurrentSettings()
}
private fun ensureLoaded() {
if (hasLoaded) return
hasLoaded = true

View file

@ -5,6 +5,7 @@ import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.details.MetaScreenSettingsRepository
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.library.LibraryRepository
import com.nuvio.app.features.mdblist.MdbListSettingsRepository
@ -127,6 +128,7 @@ object ProfileRepository {
ThemeSettingsRepository.onProfileChanged()
PlayerSettingsRepository.onProfileChanged()
HomeCatalogSettingsRepository.onProfileChanged()
MetaScreenSettingsRepository.onProfileChanged()
ContinueWatchingPreferencesRepository.onProfileChanged()
TmdbSettingsRepository.onProfileChanged()
MdbListSettingsRepository.onProfileChanged()

View file

@ -11,6 +11,7 @@ internal fun LazyListScope.contentDiscoveryContent(
onAddonsClick: () -> Unit,
onPluginsClick: () -> Unit,
onHomescreenClick: () -> Unit,
onMetaScreenClick: () -> Unit,
) {
item {
SettingsSection(
@ -48,6 +49,13 @@ internal fun LazyListScope.contentDiscoveryContent(
isTablet = isTablet,
onClick = onHomescreenClick,
)
SettingsNavigationRow(
title = "Meta Screen",
description = "Disable detail sections and reorder everything below Hero.",
icon = Icons.Rounded.Tune,
isTablet = isTablet,
onClick = onMetaScreenClick,
)
}
}
}

View file

@ -1,30 +1,42 @@
package com.nuvio.app.features.settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.nuvio.app.features.home.HomeCatalogSettingsItem
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.home.components.HomeEmptyStateCard
import sh.calvin.reorderable.ReorderableCollectionItemScope
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
internal fun LazyListScope.homescreenSettingsContent(
isTablet: Boolean,
@ -86,6 +98,11 @@ internal fun LazyListScope.homescreenSettingsContent(
title = "CATALOGS",
isTablet = isTablet,
) {
SettingsActionRow(
label = "Reset",
isTablet = isTablet,
onClick = HomeCatalogSettingsRepository::resetToDefaults,
)
HomescreenCatalogList(
isTablet = isTablet,
items = items,
@ -202,26 +219,46 @@ private fun HomescreenCatalogList(
items: List<HomeCatalogSettingsItem>,
) {
var expandedKey by remember { mutableStateOf<String?>(null) }
val hapticFeedback = LocalHapticFeedback.current
val lazyListState = rememberLazyListState()
val reorderableLazyListState = rememberReorderableLazyListState(
lazyListState = lazyListState,
) { from, to ->
HomeCatalogSettingsRepository.moveByIndex(from.index, to.index)
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
}
SettingsGroup(isTablet = isTablet) {
items.forEachIndexed { index, item ->
if (index > 0) {
SettingsGroupDivider(isTablet = isTablet)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = if (isTablet) 760.dp else 560.dp),
state = lazyListState,
) {
itemsIndexed(items, key = { _, item -> item.key }) { index, item ->
ReorderableItem(reorderableLazyListState, key = item.key) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
Surface(shadowElevation = elevation) {
Column {
if (index > 0) {
SettingsGroupDivider(isTablet = isTablet)
}
HomescreenCatalogRow(
item = item,
isTablet = isTablet,
expanded = expandedKey == item.key,
onExpandedChange = { shouldExpand ->
expandedKey = if (shouldExpand) item.key else null
},
onTitleChange = { HomeCatalogSettingsRepository.setCustomTitle(item.key, it) },
onEnabledChange = { HomeCatalogSettingsRepository.setEnabled(item.key, it) },
dragHandleScope = this@ReorderableItem,
)
}
}
}
}
HomescreenCatalogRow(
item = item,
isTablet = isTablet,
expanded = expandedKey == item.key,
canMoveUp = index > 0,
canMoveDown = index < items.lastIndex,
onExpandedChange = { shouldExpand ->
expandedKey = if (shouldExpand) item.key else null
},
onTitleChange = { HomeCatalogSettingsRepository.setCustomTitle(item.key, it) },
onEnabledChange = { HomeCatalogSettingsRepository.setEnabled(item.key, it) },
onMoveUp = { HomeCatalogSettingsRepository.moveUp(item.key) },
onMoveDown = { HomeCatalogSettingsRepository.moveDown(item.key) },
)
}
}
}

View file

@ -0,0 +1,179 @@
package com.nuvio.app.features.settings
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.nuvio.app.features.details.MetaScreenSectionItem
import com.nuvio.app.features.details.MetaScreenSettingsRepository
import com.nuvio.app.features.details.MetaScreenSettingsUiState
import sh.calvin.reorderable.ReorderableCollectionItemScope
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
internal fun LazyListScope.metaScreenSettingsContent(
isTablet: Boolean,
uiState: MetaScreenSettingsUiState,
) {
item {
SettingsSection(
title = "SECTIONS",
isTablet = isTablet,
) {
SettingsActionRow(
label = "Reset",
isTablet = isTablet,
onClick = MetaScreenSettingsRepository::resetToDefaults,
)
SettingsGroup(isTablet = isTablet) {
MetaSectionReorderableList(
items = uiState.items,
isTablet = isTablet,
)
}
}
}
}
@Composable
private fun MetaSectionReorderableList(
items: List<MetaScreenSectionItem>,
isTablet: Boolean,
) {
val hapticFeedback = LocalHapticFeedback.current
val lazyListState = rememberLazyListState()
val reorderableLazyListState = rememberReorderableLazyListState(
lazyListState = lazyListState,
) { from, to ->
MetaScreenSettingsRepository.moveByIndex(from.index, to.index)
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
}
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = if (isTablet) 680.dp else 520.dp),
state = lazyListState,
) {
itemsIndexed(items, key = { _, item -> item.key.name }) { index, item ->
ReorderableItem(reorderableLazyListState, key = item.key.name) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
Surface(shadowElevation = elevation) {
Column {
if (index > 0) {
SettingsGroupDivider(isTablet = isTablet)
}
MetaSectionRow(
item = item,
isTablet = isTablet,
onEnabledChange = { MetaScreenSettingsRepository.setEnabled(item.key, it) },
dragHandleScope = this@ReorderableItem,
)
}
}
}
}
}
}
@Composable
private fun MetaSectionRow(
item: MetaScreenSectionItem,
isTablet: Boolean,
onEnabledChange: (Boolean) -> Unit,
dragHandleScope: ReorderableCollectionItemScope,
) {
val horizontalPadding = if (isTablet) 20.dp else 16.dp
val verticalPadding = if (isTablet) 18.dp else 16.dp
val hapticFeedback = LocalHapticFeedback.current
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier
.weight(1f)
.padding(end = 12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = item.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = if (item.enabled) "Visible" else "Hidden",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Switch(
checked = item.enabled,
onCheckedChange = onEnabledChange,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
checkedTrackColor = MaterialTheme.colorScheme.primary,
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
uncheckedTrackColor = MaterialTheme.colorScheme.outlineVariant,
),
)
IconButton(
modifier = with(dragHandleScope) {
Modifier.draggableHandle(
onDragStarted = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
},
onDragStopped = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
},
)
},
onClick = {},
) {
Icon(
Icons.Rounded.Menu,
contentDescription = "Reorder",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}

View file

@ -19,9 +19,9 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
@ -30,6 +30,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -37,13 +38,16 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.ui.NuvioBackButton
import com.nuvio.app.core.ui.NuvioSectionLabel
import com.nuvio.app.features.home.HomeCatalogSettingsItem
import sh.calvin.reorderable.ReorderableCollectionItemScope
@Composable
private fun SettingsCard(
@ -180,6 +184,31 @@ internal fun SettingsSection(
}
}
@Composable
internal fun SettingsActionRow(
label: String,
isTablet: Boolean,
onClick: () -> Unit,
) {
val horizontalPadding = if (isTablet) 20.dp else 16.dp
val verticalPadding = if (isTablet) 10.dp else 8.dp
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
horizontalArrangement = Arrangement.End,
) {
TextButton(onClick = onClick) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold,
)
}
}
}
@Composable
internal fun SettingsNavigationRow(
title: String,
@ -321,16 +350,14 @@ internal fun HomescreenCatalogRow(
item: HomeCatalogSettingsItem,
isTablet: Boolean,
expanded: Boolean,
canMoveUp: Boolean,
canMoveDown: Boolean,
onExpandedChange: (Boolean) -> Unit,
onTitleChange: (String) -> Unit,
onEnabledChange: (Boolean) -> Unit,
onMoveUp: () -> Unit,
onMoveDown: () -> Unit,
dragHandleScope: ReorderableCollectionItemScope,
) {
val horizontalPadding = if (isTablet) 20.dp else 16.dp
val verticalPadding = if (isTablet) 18.dp else 16.dp
val hapticFeedback = LocalHapticFeedback.current
Column(
modifier = Modifier
@ -387,11 +414,25 @@ internal fun HomescreenCatalogRow(
uncheckedTrackColor = MaterialTheme.colorScheme.outlineVariant,
),
)
Icon(
imageVector = if (expanded) Icons.Rounded.KeyboardArrowUp else Icons.Rounded.KeyboardArrowDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
IconButton(
modifier = with(dragHandleScope) {
Modifier.draggableHandle(
onDragStarted = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
},
onDragStopped = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
},
)
},
onClick = {},
) {
Icon(
imageVector = Icons.Rounded.Menu,
contentDescription = "Reorder",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
@ -414,59 +455,7 @@ internal fun HomescreenCatalogRow(
disabledContainerColor = MaterialTheme.colorScheme.surface,
),
)
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MoveActionChip(
label = "Move Up",
icon = Icons.Rounded.KeyboardArrowUp,
enabled = canMoveUp,
onClick = onMoveUp,
)
MoveActionChip(
label = "Move Down",
icon = Icons.Rounded.KeyboardArrowDown,
enabled = canMoveDown,
onClick = onMoveDown,
)
}
}
}
}
}
@Composable
private fun MoveActionChip(
label: String,
icon: ImageVector,
enabled: Boolean,
onClick: () -> Unit,
) {
Surface(
modifier = Modifier
.clickable(enabled = enabled, onClick = onClick)
.alpha(if (enabled) 1f else 0.45f),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.10f),
shape = RoundedCornerShape(999.dp),
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium,
)
}
}
}

View file

@ -13,6 +13,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.ui.NuvioScreen
import com.nuvio.app.core.ui.NuvioScreenHeader
import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.details.MetaScreenSettingsRepository
import com.nuvio.app.features.plugins.PluginRepository
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
@ -51,6 +52,33 @@ fun HomescreenSettingsScreen(
}
}
@Composable
fun MetaScreenSettingsScreen(
onBack: () -> Unit,
) {
val metaScreenSettingsUiState by remember {
MetaScreenSettingsRepository.ensureLoaded()
MetaScreenSettingsRepository.uiState
}.collectAsStateWithLifecycle()
NuvioScreen(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
) {
stickyHeader {
NuvioScreenHeader(
title = "Meta Screen",
onBack = onBack,
)
}
metaScreenSettingsContent(
isTablet = false,
uiState = metaScreenSettingsUiState,
)
}
}
@Composable
fun ContinueWatchingSettingsScreen(
onBack: () -> Unit,

View file

@ -63,6 +63,11 @@ internal enum class SettingsPage(
category = SettingsCategory.General,
parentPage = ContentDiscovery,
),
MetaScreen(
title = "Meta Screen",
category = SettingsCategory.General,
parentPage = ContentDiscovery,
),
Integrations(
title = "Integrations",
category = SettingsCategory.General,

View file

@ -38,6 +38,8 @@ import com.nuvio.app.core.ui.NuvioScreen
import com.nuvio.app.core.ui.NuvioScreenHeader
import com.nuvio.app.core.ui.PlatformBackHandler
import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.details.MetaScreenSettingsRepository
import com.nuvio.app.features.details.MetaScreenSettingsUiState
import com.nuvio.app.features.home.HomeCatalogSettingsItem
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.mdblist.MdbListSettings
@ -56,6 +58,7 @@ fun SettingsScreen(
modifier: Modifier = Modifier,
onSwitchProfile: (() -> Unit)? = null,
onHomescreenClick: () -> Unit = {},
onMetaScreenClick: () -> Unit = {},
onContinueWatchingClick: () -> Unit = {},
onAddonsClick: () -> Unit = {},
onPluginsClick: () -> Unit = {},
@ -99,6 +102,10 @@ fun SettingsScreen(
val homescreenSettingsUiState by remember {
HomeCatalogSettingsRepository.uiState
}.collectAsStateWithLifecycle()
val metaScreenSettingsUiState by remember {
MetaScreenSettingsRepository.ensureLoaded()
MetaScreenSettingsRepository.uiState
}.collectAsStateWithLifecycle()
val continueWatchingPreferencesUiState by remember {
ContinueWatchingPreferencesRepository.ensureLoaded()
ContinueWatchingPreferencesRepository.uiState
@ -141,6 +148,7 @@ fun SettingsScreen(
traktCommentsEnabled = traktCommentsEnabled,
homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled,
homescreenItems = homescreenSettingsUiState.items,
metaScreenSettingsUiState = metaScreenSettingsUiState,
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
onSwitchProfile = onSwitchProfile,
)
@ -168,9 +176,11 @@ fun SettingsScreen(
traktCommentsEnabled = traktCommentsEnabled,
homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled,
homescreenItems = homescreenSettingsUiState.items,
metaScreenSettingsUiState = metaScreenSettingsUiState,
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
onSwitchProfile = onSwitchProfile,
onHomescreenClick = onHomescreenClick,
onMetaScreenClick = onMetaScreenClick,
onContinueWatchingClick = onContinueWatchingClick,
onAddonsClick = onAddonsClick,
onPluginsClick = onPluginsClick,
@ -204,9 +214,11 @@ private fun MobileSettingsScreen(
traktCommentsEnabled: Boolean,
homescreenHeroEnabled: Boolean,
homescreenItems: List<HomeCatalogSettingsItem>,
metaScreenSettingsUiState: MetaScreenSettingsUiState,
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
onSwitchProfile: (() -> Unit)? = null,
onHomescreenClick: () -> Unit = {},
onMetaScreenClick: () -> Unit = {},
onContinueWatchingClick: () -> Unit = {},
onAddonsClick: () -> Unit = {},
onPluginsClick: () -> Unit = {},
@ -267,6 +279,7 @@ private fun MobileSettingsScreen(
onAddonsClick = onAddonsClick,
onPluginsClick = onPluginsClick,
onHomescreenClick = onHomescreenClick,
onMetaScreenClick = onMetaScreenClick,
)
SettingsPage.Addons -> addonsSettingsContent()
SettingsPage.Plugins -> pluginsSettingsContent()
@ -275,6 +288,10 @@ private fun MobileSettingsScreen(
heroEnabled = homescreenHeroEnabled,
items = homescreenItems,
)
SettingsPage.MetaScreen -> metaScreenSettingsContent(
isTablet = false,
uiState = metaScreenSettingsUiState,
)
SettingsPage.Integrations -> integrationsContent(
isTablet = false,
onTmdbClick = { onPageChange(SettingsPage.TmdbEnrichment) },
@ -322,6 +339,7 @@ private fun TabletSettingsScreen(
traktCommentsEnabled: Boolean,
homescreenHeroEnabled: Boolean,
homescreenItems: List<HomeCatalogSettingsItem>,
metaScreenSettingsUiState: MetaScreenSettingsUiState,
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
onSwitchProfile: (() -> Unit)? = null,
) {
@ -449,6 +467,7 @@ private fun TabletSettingsScreen(
onAddonsClick = { openInlinePage(SettingsPage.Addons) },
onPluginsClick = { openInlinePage(SettingsPage.Plugins) },
onHomescreenClick = { openInlinePage(SettingsPage.Homescreen) },
onMetaScreenClick = { openInlinePage(SettingsPage.MetaScreen) },
)
SettingsPage.Addons -> addonsSettingsContent()
SettingsPage.Plugins -> pluginsSettingsContent()
@ -457,6 +476,10 @@ private fun TabletSettingsScreen(
heroEnabled = homescreenHeroEnabled,
items = homescreenItems,
)
SettingsPage.MetaScreen -> metaScreenSettingsContent(
isTablet = true,
uiState = metaScreenSettingsUiState,
)
SettingsPage.Integrations -> integrationsContent(
isTablet = true,
onTmdbClick = { onPageChange(SettingsPage.TmdbEnrichment) },

View file

@ -0,0 +1,15 @@
package com.nuvio.app.features.details
import com.nuvio.app.core.storage.ProfileScopedKey
import platform.Foundation.NSUserDefaults
internal actual object MetaScreenSettingsStorage {
private const val payloadKey = "meta_screen_settings_payload"
actual fun loadPayload(): String? =
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(payloadKey))
actual fun savePayload(payload: String) {
NSUserDefaults.standardUserDefaults.setObject(payload, forKey = ProfileScopedKey.of(payloadKey))
}
}

View file

@ -22,6 +22,7 @@ androidx-media3 = "1.10.0-rc01"
supabase = "3.4.1"
quickjsKt = "1.0.1"
ksoup = "0.2.6"
reorderable = "3.0.0"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@ -66,6 +67,7 @@ supabase-auth = { module = "io.github.jan-tennert.supabase:auth-kt", version.ref
supabase-functions = { module = "io.github.jan-tennert.supabase:functions-kt", version.ref = "supabase" }
quickjs-kt = { module = "io.github.dokar3:quickjs-kt", version.ref = "quickjsKt" }
ksoup = { module = "com.fleeksoft.ksoup:ksoup", version.ref = "ksoup" }
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }