mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-18 13:26:09 +00:00
feat: added metascreen reordering
This commit is contained in:
parent
bd5077bf97
commit
333063bd1e
19 changed files with 937 additions and 245 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.nuvio.app.features.details
|
||||
|
||||
internal expect object MetaScreenSettingsStorage {
|
||||
fun loadPayload(): String?
|
||||
fun savePayload(payload: String)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Reference in a new issue