From 3d620394cad752b7fa2babc7cd0956641fcb191e Mon Sep 17 00:00:00 2001
From: tapframe <85391825+tapframe@users.noreply.github.com>
Date: Fri, 8 May 2026 20:15:43 +0530
Subject: [PATCH] feat: adding search option on settings screen
---
.../composeResources/values/strings.xml | 3 +
.../app/features/settings/SettingsScreen.kt | 258 ++++-
.../app/features/settings/SettingsSearch.kt | 971 ++++++++++++++++++
iosApp/Configuration/Version.xcconfig | 2 +-
4 files changed, 1199 insertions(+), 35 deletions(-)
create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index f235570b9..78461786c 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -391,6 +391,9 @@
Change to a different profile.
Switch Profile
Open Trakt connection screen
+ No settings found.
+ Search settings...
+ RESULTS
Loading your Trakt lists…
Choose where to save this title on Trakt
Donate
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt
index b625c9dc1..45a52febb 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
@@ -29,10 +30,19 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.Modifier
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
@@ -66,8 +76,14 @@ import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepositor
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesUiState
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.compose_settings_page_root
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
+private val SettingsSearchRevealThreshold = 28.dp
+private const val SettingsSearchRevealAnimationMillis = 240L
+private const val SettingsSearchRevealHapticDelayMillis = 90L
+
@Composable
fun SettingsScreen(
modifier: Modifier = Modifier,
@@ -328,7 +344,66 @@ private fun MobileSettingsScreen(
) {
val saveableStateHolder = rememberSaveableStateHolder()
saveableStateHolder.SaveableStateProvider(page.name) {
- NuvioScreen {
+ var settingsSearchQuery by rememberSaveable { mutableStateOf("") }
+ var rootSearchVisible by rememberSaveable { mutableStateOf(false) }
+ var rootSearchRevealAnimating by rememberSaveable { mutableStateOf(false) }
+ val listState = rememberLazyListState()
+ val hapticFeedback = LocalHapticFeedback.current
+ val hapticScope = rememberCoroutineScope()
+ val rootSearchRevealConnection = rememberSettingsRootSearchRevealConnection(
+ page = page,
+ listState = listState,
+ query = settingsSearchQuery,
+ searchVisible = rootSearchVisible,
+ ) {
+ rootSearchVisible = true
+ rootSearchRevealAnimating = true
+ hapticScope.launch {
+ delay(SettingsSearchRevealHapticDelayMillis)
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ }
+ }
+ val searchEntries = settingsSearchEntries(
+ pluginsEnabled = AppFeaturePolicy.pluginsEnabled,
+ liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported,
+ switchProfileAvailable = onSwitchProfile != null,
+ checkForUpdatesAvailable = onCheckForUpdatesClick != null,
+ )
+
+ fun openSearchTarget(target: SettingsSearchTarget) {
+ when (target) {
+ is SettingsSearchTarget.Page -> when (target.page) {
+ SettingsPage.Account -> onAccountClick()
+ SettingsPage.SupportersContributors -> onSupportersContributorsClick()
+ SettingsPage.ContinueWatching -> onContinueWatchingClick()
+ SettingsPage.Addons -> onAddonsClick()
+ SettingsPage.Plugins -> {
+ if (AppFeaturePolicy.pluginsEnabled) {
+ onPluginsClick()
+ }
+ }
+ SettingsPage.Homescreen -> onHomescreenClick()
+ SettingsPage.MetaScreen -> onMetaScreenClick()
+ else -> onPageChange(target.page)
+ }
+ SettingsSearchTarget.Downloads -> onDownloadsClick()
+ SettingsSearchTarget.Collections -> onCollectionsClick()
+ SettingsSearchTarget.SwitchProfile -> onSwitchProfile?.invoke()
+ SettingsSearchTarget.CheckForUpdates -> onCheckForUpdatesClick?.invoke()
+ }
+ }
+
+ LaunchedEffect(rootSearchRevealAnimating) {
+ if (rootSearchRevealAnimating) {
+ delay(SettingsSearchRevealAnimationMillis)
+ rootSearchRevealAnimating = false
+ }
+ }
+
+ NuvioScreen(
+ modifier = Modifier.nestedScroll(rootSearchRevealConnection),
+ listState = listState,
+ ) {
stickyHeader {
val previousPage = page.previousPage()
NuvioScreenHeader(
@@ -338,20 +413,33 @@ private fun MobileSettingsScreen(
}
when (page) {
- SettingsPage.Root -> settingsRootContent(
- isTablet = false,
- onPlaybackClick = { onPageChange(SettingsPage.Playback) },
- onAppearanceClick = { onPageChange(SettingsPage.Appearance) },
- onNotificationsClick = { onPageChange(SettingsPage.Notifications) },
- onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) },
- onIntegrationsClick = { onPageChange(SettingsPage.Integrations) },
- onTraktClick = { onPageChange(SettingsPage.TraktAuthentication) },
- onSupportersContributorsClick = onSupportersContributorsClick,
- onCheckForUpdatesClick = onCheckForUpdatesClick,
- onDownloadsClick = onDownloadsClick,
- onAccountClick = onAccountClick,
- onSwitchProfileClick = onSwitchProfile,
- )
+ SettingsPage.Root -> {
+ settingsSearchRootContent(
+ query = settingsSearchQuery,
+ entries = searchEntries,
+ isTablet = false,
+ showSearchField = rootSearchVisible,
+ animateSearchField = rootSearchRevealAnimating,
+ onQueryChange = { settingsSearchQuery = it },
+ onTargetClick = { openSearchTarget(it) },
+ )
+ if (settingsSearchQuery.isBlank()) {
+ settingsRootContent(
+ isTablet = false,
+ onPlaybackClick = { onPageChange(SettingsPage.Playback) },
+ onAppearanceClick = { onPageChange(SettingsPage.Appearance) },
+ onNotificationsClick = { onPageChange(SettingsPage.Notifications) },
+ onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) },
+ onIntegrationsClick = { onPageChange(SettingsPage.Integrations) },
+ onTraktClick = { onPageChange(SettingsPage.TraktAuthentication) },
+ onSupportersContributorsClick = onSupportersContributorsClick,
+ onCheckForUpdatesClick = onCheckForUpdatesClick,
+ onDownloadsClick = onDownloadsClick,
+ onAccountClick = onAccountClick,
+ onSwitchProfileClick = onSwitchProfile,
+ )
+ }
+ }
SettingsPage.Account -> accountSettingsContent(
isTablet = false,
)
@@ -453,6 +541,48 @@ private fun MobileSettingsScreen(
}
}
+@Composable
+private fun rememberSettingsRootSearchRevealConnection(
+ page: SettingsPage,
+ listState: LazyListState,
+ query: String,
+ searchVisible: Boolean,
+ onReveal: () -> Unit,
+): NestedScrollConnection {
+ val revealThresholdPx = with(LocalDensity.current) { SettingsSearchRevealThreshold.toPx() }
+ val currentOnReveal by rememberUpdatedState(onReveal)
+ var pullDistancePx by remember(page) { mutableStateOf(0f) }
+ var revealTriggered by remember(page) { mutableStateOf(false) }
+
+ return remember(page, listState, query, searchVisible, revealThresholdPx) {
+ object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource,
+ ): Offset {
+ val isRootAtTop = page == SettingsPage.Root &&
+ listState.firstVisibleItemIndex == 0 &&
+ listState.firstVisibleItemScrollOffset == 0
+ val canRevealSearch = isRootAtTop && !searchVisible && !revealTriggered && query.isBlank()
+
+ if (canRevealSearch && available.y > 0f) {
+ pullDistancePx += available.y
+ if (pullDistancePx >= revealThresholdPx) {
+ pullDistancePx = 0f
+ revealTriggered = true
+ currentOnReveal()
+ }
+ } else if (!isRootAtTop || available.y < 0f) {
+ pullDistancePx = 0f
+ }
+
+ return Offset.Zero
+ }
+ }
+ }
+}
+
@Composable
private fun TabletSettingsScreen(
page: SettingsPage,
@@ -559,11 +689,54 @@ private fun TabletSettingsScreen(
}
saveableStateHolder.SaveableStateProvider(page.name) {
+ var settingsSearchQuery by rememberSaveable { mutableStateOf("") }
+ var rootSearchVisible by rememberSaveable { mutableStateOf(false) }
+ var rootSearchRevealAnimating by rememberSaveable { mutableStateOf(false) }
+ val hapticFeedback = LocalHapticFeedback.current
+ val hapticScope = rememberCoroutineScope()
+ val searchEntries = settingsSearchEntries(
+ pluginsEnabled = AppFeaturePolicy.pluginsEnabled,
+ liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported,
+ switchProfileAvailable = onSwitchProfile != null,
+ checkForUpdatesAvailable = onCheckForUpdatesClick != null,
+ )
+
+ fun openSearchTarget(target: SettingsSearchTarget) {
+ when (target) {
+ is SettingsSearchTarget.Page -> openInlinePage(target.page)
+ SettingsSearchTarget.Downloads -> onDownloadsClick()
+ SettingsSearchTarget.Collections -> onCollectionsClick()
+ SettingsSearchTarget.SwitchProfile -> onSwitchProfile?.invoke()
+ SettingsSearchTarget.CheckForUpdates -> onCheckForUpdatesClick?.invoke()
+ }
+ }
+
val listState = rememberLazyListState()
val bottomOverlayPadding = LocalNuvioBottomNavigationOverlayPadding.current
+ val rootSearchRevealConnection = rememberSettingsRootSearchRevealConnection(
+ page = page,
+ listState = listState,
+ query = settingsSearchQuery,
+ searchVisible = rootSearchVisible,
+ ) {
+ rootSearchVisible = true
+ rootSearchRevealAnimating = true
+ hapticScope.launch {
+ delay(SettingsSearchRevealHapticDelayMillis)
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ }
+ }
+ LaunchedEffect(rootSearchRevealAnimating) {
+ if (rootSearchRevealAnimating) {
+ delay(SettingsSearchRevealAnimationMillis)
+ rootSearchRevealAnimating = false
+ }
+ }
LazyColumn(
state = listState,
- modifier = Modifier.fillMaxSize(),
+ modifier = Modifier
+ .fillMaxSize()
+ .nestedScroll(rootSearchRevealConnection),
contentPadding = PaddingValues(
start = 40.dp,
top = topOffset,
@@ -576,7 +749,11 @@ private fun TabletSettingsScreen(
val previousPage = page.previousPage()
TabletPageHeader(
title = if (page == SettingsPage.Root) {
- stringResource(activeCategory.labelRes)
+ if (settingsSearchQuery.isBlank()) {
+ stringResource(activeCategory.labelRes)
+ } else {
+ stringResource(Res.string.compose_settings_page_root)
+ }
} else {
stringResource(page.titleRes)
},
@@ -585,23 +762,36 @@ private fun TabletSettingsScreen(
)
}
when (page) {
- SettingsPage.Root -> settingsRootContent(
- isTablet = true,
- onPlaybackClick = { openInlinePage(SettingsPage.Playback) },
- onAppearanceClick = { openInlinePage(SettingsPage.Appearance) },
- onNotificationsClick = { openInlinePage(SettingsPage.Notifications) },
- onContentDiscoveryClick = { openInlinePage(SettingsPage.ContentDiscovery) },
- onIntegrationsClick = { openInlinePage(SettingsPage.Integrations) },
- onTraktClick = { openInlinePage(SettingsPage.TraktAuthentication) },
- onSupportersContributorsClick = { openInlinePage(SettingsPage.SupportersContributors) },
- onCheckForUpdatesClick = onCheckForUpdatesClick,
- onDownloadsClick = onDownloadsClick,
- onAccountClick = { openInlinePage(SettingsPage.Account) },
- onSwitchProfileClick = onSwitchProfile,
- showAccountSection = activeCategory == SettingsCategory.Account,
- showGeneralSection = activeCategory == SettingsCategory.General,
- showAboutSection = activeCategory == SettingsCategory.About,
- )
+ SettingsPage.Root -> {
+ settingsSearchRootContent(
+ query = settingsSearchQuery,
+ entries = searchEntries,
+ isTablet = true,
+ showSearchField = rootSearchVisible,
+ animateSearchField = rootSearchRevealAnimating,
+ onQueryChange = { settingsSearchQuery = it },
+ onTargetClick = { openSearchTarget(it) },
+ )
+ if (settingsSearchQuery.isBlank()) {
+ settingsRootContent(
+ isTablet = true,
+ onPlaybackClick = { openInlinePage(SettingsPage.Playback) },
+ onAppearanceClick = { openInlinePage(SettingsPage.Appearance) },
+ onNotificationsClick = { openInlinePage(SettingsPage.Notifications) },
+ onContentDiscoveryClick = { openInlinePage(SettingsPage.ContentDiscovery) },
+ onIntegrationsClick = { openInlinePage(SettingsPage.Integrations) },
+ onTraktClick = { openInlinePage(SettingsPage.TraktAuthentication) },
+ onSupportersContributorsClick = { openInlinePage(SettingsPage.SupportersContributors) },
+ onCheckForUpdatesClick = onCheckForUpdatesClick,
+ onDownloadsClick = onDownloadsClick,
+ onAccountClick = { openInlinePage(SettingsPage.Account) },
+ onSwitchProfileClick = onSwitchProfile,
+ showAccountSection = activeCategory == SettingsCategory.Account,
+ showGeneralSection = activeCategory == SettingsCategory.General,
+ showAboutSection = activeCategory == SettingsCategory.About,
+ )
+ }
+ }
SettingsPage.Account -> accountSettingsContent(
isTablet = true,
)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
new file mode 100644
index 000000000..1a8a2e56c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
@@ -0,0 +1,971 @@
+package com.nuvio.app.features.settings
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.slideInVertically
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.AccountCircle
+import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material.icons.rounded.CloudDownload
+import androidx.compose.material.icons.rounded.CollectionsBookmark
+import androidx.compose.material.icons.rounded.Extension
+import androidx.compose.material.icons.rounded.Favorite
+import androidx.compose.material.icons.rounded.Hub
+import androidx.compose.material.icons.rounded.Home
+import androidx.compose.material.icons.rounded.Language
+import androidx.compose.material.icons.rounded.Link
+import androidx.compose.material.icons.rounded.Notifications
+import androidx.compose.material.icons.rounded.Palette
+import androidx.compose.material.icons.rounded.People
+import androidx.compose.material.icons.rounded.PlayArrow
+import androidx.compose.material.icons.rounded.Search
+import androidx.compose.material.icons.rounded.Style
+import androidx.compose.material.icons.rounded.Tune
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.nuvio.app.isIos
+import nuvio.composeapp.generated.resources.*
+import org.jetbrains.compose.resources.stringResource
+
+internal sealed class SettingsSearchTarget {
+ data class Page(val page: SettingsPage) : SettingsSearchTarget()
+ object Downloads : SettingsSearchTarget()
+ object Collections : SettingsSearchTarget()
+ object SwitchProfile : SettingsSearchTarget()
+ object CheckForUpdates : SettingsSearchTarget()
+}
+
+internal data class SettingsSearchEntry(
+ val key: String,
+ val title: String,
+ val description: String,
+ val page: String,
+ val section: String,
+ val category: String,
+ val icon: ImageVector,
+ val target: SettingsSearchTarget,
+) {
+ val searchableText: String = listOf(title, description, page, section, category)
+ .joinToString(separator = " ")
+ .lowercase()
+
+ val contextLabel: String = listOf(page, section)
+ .filter { it.isNotBlank() }
+ .distinct()
+ .joinToString(separator = " - ")
+}
+
+@Composable
+internal fun settingsSearchEntries(
+ pluginsEnabled: Boolean,
+ liquidGlassNativeTabBarSupported: Boolean,
+ switchProfileAvailable: Boolean,
+ checkForUpdatesAvailable: Boolean,
+): List {
+ val accountCategory = stringResource(SettingsCategory.Account.labelRes)
+ val generalCategory = stringResource(SettingsCategory.General.labelRes)
+ val aboutCategory = stringResource(SettingsCategory.About.labelRes)
+
+ val accountPage = stringResource(Res.string.compose_settings_page_account)
+ val traktPage = stringResource(Res.string.compose_settings_page_trakt)
+ val layoutPage = stringResource(Res.string.compose_settings_page_appearance)
+ val contentDiscoveryPage = stringResource(Res.string.compose_settings_page_content_discovery)
+ val downloadsPage = stringResource(Res.string.compose_settings_root_downloads_title)
+ val playbackPage = stringResource(Res.string.compose_settings_page_playback)
+ val integrationsPage = stringResource(Res.string.compose_settings_page_integrations)
+ val notificationsPage = stringResource(Res.string.compose_settings_page_notifications)
+ val supportersPage = stringResource(Res.string.compose_settings_page_supporters_contributors)
+ val homeLayoutPage = stringResource(Res.string.compose_settings_page_homescreen)
+ val detailPage = stringResource(Res.string.compose_settings_page_meta_screen)
+ val continueWatchingPage = stringResource(Res.string.compose_settings_page_continue_watching)
+ val posterStylePage = stringResource(Res.string.compose_settings_page_poster_customization)
+ val addonsPage = stringResource(Res.string.compose_settings_page_addons)
+ val pluginsPage = stringResource(Res.string.compose_settings_page_plugins)
+ val collectionsPage = stringResource(Res.string.collections_header)
+ val tmdbPage = stringResource(Res.string.compose_settings_page_tmdb_enrichment)
+ val mdbListPage = stringResource(Res.string.compose_settings_page_mdblist_ratings)
+
+ val entries = mutableListOf()
+
+ fun add(
+ key: String,
+ title: String,
+ description: String = "",
+ page: String = title,
+ section: String = "",
+ category: String = generalCategory,
+ icon: ImageVector,
+ target: SettingsSearchTarget,
+ ) {
+ entries += SettingsSearchEntry(
+ key = key,
+ title = title,
+ description = description,
+ page = page,
+ section = section,
+ category = category,
+ icon = icon,
+ target = target,
+ )
+ }
+
+ fun addPage(
+ page: SettingsPage,
+ key: String,
+ title: String,
+ description: String,
+ category: String = generalCategory,
+ icon: ImageVector,
+ ) {
+ add(
+ key = key,
+ title = title,
+ description = description,
+ page = title,
+ category = category,
+ icon = icon,
+ target = SettingsSearchTarget.Page(page),
+ )
+ }
+
+ fun addRow(
+ page: SettingsPage,
+ key: String,
+ title: String,
+ description: String = "",
+ pageLabel: String,
+ section: String,
+ category: String = generalCategory,
+ icon: ImageVector,
+ ) {
+ add(
+ key = key,
+ title = title,
+ description = description,
+ page = pageLabel,
+ section = section,
+ category = category,
+ icon = icon,
+ target = SettingsSearchTarget.Page(page),
+ )
+ }
+
+ if (switchProfileAvailable) {
+ add(
+ key = "switch-profile",
+ title = stringResource(Res.string.compose_settings_root_switch_profile_title),
+ description = stringResource(Res.string.compose_settings_root_switch_profile_description),
+ page = accountPage,
+ section = stringResource(Res.string.compose_settings_root_account_section),
+ category = accountCategory,
+ icon = Icons.Rounded.People,
+ target = SettingsSearchTarget.SwitchProfile,
+ )
+ }
+ addPage(
+ page = SettingsPage.Account,
+ key = "account",
+ title = accountPage,
+ description = stringResource(Res.string.compose_settings_root_account_description),
+ category = accountCategory,
+ icon = Icons.Rounded.AccountCircle,
+ )
+ addPage(
+ page = SettingsPage.TraktAuthentication,
+ key = "trakt",
+ title = traktPage,
+ description = stringResource(Res.string.compose_settings_root_trakt_description),
+ category = accountCategory,
+ icon = Icons.Rounded.Link,
+ )
+ addPage(
+ page = SettingsPage.Appearance,
+ key = "layout",
+ title = layoutPage,
+ description = stringResource(Res.string.compose_settings_root_appearance_description),
+ icon = Icons.Rounded.Palette,
+ )
+ addPage(
+ page = SettingsPage.ContentDiscovery,
+ key = "content-discovery",
+ title = contentDiscoveryPage,
+ description = stringResource(Res.string.compose_settings_root_content_discovery_description),
+ icon = Icons.Rounded.Extension,
+ )
+ add(
+ key = "downloads",
+ title = downloadsPage,
+ description = stringResource(Res.string.compose_settings_root_downloads_description),
+ category = generalCategory,
+ icon = Icons.Rounded.CloudDownload,
+ target = SettingsSearchTarget.Downloads,
+ )
+ addPage(
+ page = SettingsPage.Playback,
+ key = "playback",
+ title = playbackPage,
+ description = stringResource(Res.string.settings_playback_subtitle),
+ icon = Icons.Rounded.PlayArrow,
+ )
+ addPage(
+ page = SettingsPage.Integrations,
+ key = "integrations",
+ title = integrationsPage,
+ description = stringResource(Res.string.compose_settings_root_integrations_description),
+ icon = Icons.Rounded.Link,
+ )
+ addPage(
+ page = SettingsPage.Notifications,
+ key = "notifications",
+ title = notificationsPage,
+ description = stringResource(Res.string.compose_settings_root_notifications_description),
+ icon = Icons.Rounded.Notifications,
+ )
+ addPage(
+ page = SettingsPage.SupportersContributors,
+ key = "supporters",
+ title = supportersPage,
+ description = stringResource(Res.string.about_supporters_contributors_subtitle),
+ category = aboutCategory,
+ icon = Icons.Rounded.Favorite,
+ )
+ if (checkForUpdatesAvailable) {
+ add(
+ key = "check-updates",
+ title = stringResource(Res.string.compose_settings_root_check_updates_title),
+ description = stringResource(Res.string.compose_settings_root_check_updates_description),
+ page = supportersPage,
+ section = stringResource(Res.string.compose_settings_root_about_section),
+ category = aboutCategory,
+ icon = Icons.Rounded.CloudDownload,
+ target = SettingsSearchTarget.CheckForUpdates,
+ )
+ }
+
+ addRow(
+ page = SettingsPage.Account,
+ key = "account-status",
+ title = stringResource(Res.string.settings_account_status),
+ pageLabel = accountPage,
+ section = accountPage,
+ category = accountCategory,
+ icon = Icons.Rounded.AccountCircle,
+ )
+ addRow(
+ page = SettingsPage.Account,
+ key = "account-sign-out",
+ title = stringResource(Res.string.settings_account_sign_out),
+ pageLabel = accountPage,
+ section = accountPage,
+ category = accountCategory,
+ icon = Icons.Rounded.AccountCircle,
+ )
+ addRow(
+ page = SettingsPage.Account,
+ key = "account-delete",
+ title = stringResource(Res.string.settings_account_delete_account),
+ description = stringResource(Res.string.settings_account_delete_account_description),
+ pageLabel = accountPage,
+ section = accountPage,
+ category = accountCategory,
+ icon = Icons.Rounded.AccountCircle,
+ )
+
+ addRow(
+ page = SettingsPage.Appearance,
+ key = "theme",
+ title = stringResource(Res.string.settings_appearance_section_theme),
+ pageLabel = layoutPage,
+ section = stringResource(Res.string.settings_appearance_section_theme),
+ icon = Icons.Rounded.Palette,
+ )
+ addRow(
+ page = SettingsPage.Appearance,
+ key = "amoled",
+ title = stringResource(Res.string.settings_appearance_amoled_black),
+ description = stringResource(Res.string.settings_appearance_amoled_description),
+ pageLabel = layoutPage,
+ section = stringResource(Res.string.settings_appearance_section_display),
+ icon = Icons.Rounded.Palette,
+ )
+ if (liquidGlassNativeTabBarSupported) {
+ addRow(
+ page = SettingsPage.Appearance,
+ key = "liquid-glass",
+ title = stringResource(Res.string.settings_appearance_liquid_glass),
+ description = stringResource(Res.string.settings_appearance_liquid_glass_description),
+ pageLabel = layoutPage,
+ section = stringResource(Res.string.settings_appearance_section_display),
+ icon = Icons.Rounded.Palette,
+ )
+ }
+ addRow(
+ page = SettingsPage.Appearance,
+ key = "app-language",
+ title = stringResource(Res.string.settings_appearance_app_language),
+ pageLabel = layoutPage,
+ section = stringResource(Res.string.settings_appearance_section_display),
+ icon = Icons.Rounded.Language,
+ )
+ addPage(
+ page = SettingsPage.ContinueWatching,
+ key = "continue-watching",
+ title = continueWatchingPage,
+ description = stringResource(Res.string.settings_appearance_continue_watching_description),
+ icon = Icons.Rounded.Style,
+ )
+ addPage(
+ page = SettingsPage.PosterCustomization,
+ key = "poster-card-style",
+ title = posterStylePage,
+ description = stringResource(Res.string.settings_appearance_poster_customization_description),
+ icon = Icons.Rounded.Tune,
+ )
+
+ addPage(
+ page = SettingsPage.Addons,
+ key = "addons",
+ title = addonsPage,
+ description = stringResource(Res.string.settings_content_discovery_addons_description),
+ icon = Icons.Rounded.Extension,
+ )
+ if (pluginsEnabled) {
+ addPage(
+ page = SettingsPage.Plugins,
+ key = "plugins",
+ title = pluginsPage,
+ description = stringResource(Res.string.settings_content_discovery_plugins_description),
+ icon = Icons.Rounded.Hub,
+ )
+ }
+ addPage(
+ page = SettingsPage.Homescreen,
+ key = "home-layout",
+ title = homeLayoutPage,
+ description = stringResource(Res.string.settings_content_discovery_homescreen_description),
+ icon = Icons.Rounded.Home,
+ )
+ addPage(
+ page = SettingsPage.MetaScreen,
+ key = "detail-page",
+ title = detailPage,
+ description = stringResource(Res.string.settings_content_discovery_meta_screen_description),
+ icon = Icons.Rounded.Tune,
+ )
+ add(
+ key = "collections",
+ title = collectionsPage,
+ description = stringResource(Res.string.settings_content_discovery_collections_description),
+ page = contentDiscoveryPage,
+ section = stringResource(Res.string.settings_content_discovery_section_home),
+ category = generalCategory,
+ icon = Icons.Rounded.CollectionsBookmark,
+ target = SettingsSearchTarget.Collections,
+ )
+
+ val playbackPlayer = stringResource(Res.string.settings_playback_section_player)
+ val playbackSubtitleAudio = stringResource(Res.string.settings_playback_section_subtitle_audio)
+ val playbackStreamSelection = stringResource(Res.string.settings_playback_section_stream_selection)
+ val playbackStreamAutoPlay = stringResource(Res.string.settings_playback_section_stream_auto_play)
+ val playbackDecoder = stringResource(Res.string.settings_playback_section_decoder)
+ val playbackSubtitleRendering = stringResource(Res.string.settings_playback_section_subtitle_rendering)
+ val playbackSkipSegments = stringResource(Res.string.settings_playback_section_skip_segments)
+ val playbackNextEpisode = stringResource(Res.string.settings_playback_section_next_episode)
+ addPlaybackRows(
+ addRow = ::addRow,
+ pageLabel = playbackPage,
+ section = playbackPlayer,
+ icon = Icons.Rounded.PlayArrow,
+ rows = listOf(
+ PlaybackSearchRow(
+ "loading-overlay",
+ stringResource(Res.string.settings_playback_show_loading_overlay),
+ stringResource(Res.string.settings_playback_show_loading_overlay_description),
+ ),
+ PlaybackSearchRow(
+ "hold-to-speed",
+ stringResource(Res.string.settings_playback_hold_to_speed),
+ stringResource(Res.string.settings_playback_hold_to_speed_description),
+ ),
+ PlaybackSearchRow("hold-speed", stringResource(Res.string.settings_playback_hold_speed)),
+ ),
+ )
+ addPlaybackRows(
+ addRow = ::addRow,
+ pageLabel = playbackPage,
+ section = playbackSubtitleAudio,
+ icon = Icons.Rounded.PlayArrow,
+ rows = listOf(
+ PlaybackSearchRow("preferred-audio", stringResource(Res.string.settings_playback_preferred_audio_language)),
+ PlaybackSearchRow("secondary-audio", stringResource(Res.string.settings_playback_secondary_audio_language)),
+ PlaybackSearchRow("preferred-subtitles", stringResource(Res.string.settings_playback_preferred_subtitle_language)),
+ PlaybackSearchRow("secondary-subtitles", stringResource(Res.string.settings_playback_secondary_subtitle_language)),
+ ),
+ )
+ addPlaybackRows(
+ addRow = ::addRow,
+ pageLabel = playbackPage,
+ section = playbackStreamSelection,
+ icon = Icons.Rounded.PlayArrow,
+ rows = listOf(
+ PlaybackSearchRow(
+ "reuse-last-link",
+ stringResource(Res.string.settings_playback_reuse_last_link),
+ stringResource(Res.string.settings_playback_reuse_last_link_description),
+ ),
+ PlaybackSearchRow("last-link-cache", stringResource(Res.string.settings_playback_last_link_cache_duration)),
+ ),
+ )
+ addPlaybackRows(
+ addRow = ::addRow,
+ pageLabel = playbackPage,
+ section = playbackStreamAutoPlay,
+ icon = Icons.Rounded.PlayArrow,
+ rows = buildList {
+ add(PlaybackSearchRow("stream-mode", stringResource(Res.string.settings_playback_stream_selection_mode)))
+ add(PlaybackSearchRow("regex-pattern", stringResource(Res.string.settings_playback_regex_pattern)))
+ add(PlaybackSearchRow("stream-timeout", stringResource(Res.string.settings_playback_stream_timeout), stringResource(Res.string.settings_playback_stream_timeout_description)))
+ add(PlaybackSearchRow("source-scope", stringResource(Res.string.settings_playback_source_scope)))
+ add(PlaybackSearchRow("allowed-addons", stringResource(Res.string.settings_playback_allowed_addons)))
+ if (pluginsEnabled) add(PlaybackSearchRow("allowed-plugins", stringResource(Res.string.settings_playback_allowed_plugins)))
+ },
+ )
+ if (!isIos) {
+ addPlaybackRows(
+ addRow = ::addRow,
+ pageLabel = playbackPage,
+ section = playbackDecoder,
+ icon = Icons.Rounded.PlayArrow,
+ rows = listOf(
+ PlaybackSearchRow("decoder-priority", stringResource(Res.string.settings_playback_decoder_priority)),
+ PlaybackSearchRow("dv7-hevc", stringResource(Res.string.settings_playback_map_dv7_to_hevc), stringResource(Res.string.settings_playback_map_dv7_to_hevc_description)),
+ PlaybackSearchRow("tunneled-playback", stringResource(Res.string.settings_playback_tunneled_playback), stringResource(Res.string.settings_playback_tunneled_playback_description)),
+ ),
+ )
+ addPlaybackRows(
+ addRow = ::addRow,
+ pageLabel = playbackPage,
+ section = playbackSubtitleRendering,
+ icon = Icons.Rounded.PlayArrow,
+ rows = listOf(
+ PlaybackSearchRow("libass", stringResource(Res.string.settings_playback_enable_libass), stringResource(Res.string.settings_playback_enable_libass_description)),
+ PlaybackSearchRow("libass-render", stringResource(Res.string.settings_playback_render_type)),
+ ),
+ )
+ }
+ addPlaybackRows(
+ addRow = ::addRow,
+ pageLabel = playbackPage,
+ section = playbackSkipSegments,
+ icon = Icons.Rounded.PlayArrow,
+ rows = listOf(
+ PlaybackSearchRow("skip-intro", stringResource(Res.string.settings_playback_skip_intro_outro_recap), stringResource(Res.string.settings_playback_skip_intro_outro_recap_description)),
+ PlaybackSearchRow("anime-skip", stringResource(Res.string.settings_playback_anime_skip), stringResource(Res.string.settings_playback_anime_skip_description)),
+ PlaybackSearchRow("anime-skip-client", stringResource(Res.string.settings_playback_anime_skip_client_id), stringResource(Res.string.settings_playback_anime_skip_client_id_description)),
+ PlaybackSearchRow("intro-submit", stringResource(Res.string.settings_playback_intro_submit_enabled), stringResource(Res.string.settings_playback_intro_submit_enabled_description)),
+ PlaybackSearchRow("introdb-key", stringResource(Res.string.settings_playback_introdb_api_key), stringResource(Res.string.settings_playback_introdb_api_key_description)),
+ ),
+ )
+ addPlaybackRows(
+ addRow = ::addRow,
+ pageLabel = playbackPage,
+ section = playbackNextEpisode,
+ icon = Icons.Rounded.PlayArrow,
+ rows = listOf(
+ PlaybackSearchRow("auto-play-next", stringResource(Res.string.settings_playback_auto_play_next_episode), stringResource(Res.string.settings_playback_auto_play_next_episode_description)),
+ PlaybackSearchRow("prefer-binge", stringResource(Res.string.settings_playback_prefer_binge_group), stringResource(Res.string.settings_playback_prefer_binge_group_description)),
+ PlaybackSearchRow("threshold-mode", stringResource(Res.string.settings_playback_threshold_mode)),
+ PlaybackSearchRow("threshold-percent", stringResource(Res.string.settings_playback_threshold_percentage), stringResource(Res.string.settings_playback_threshold_percentage_description)),
+ PlaybackSearchRow("threshold-minutes", stringResource(Res.string.settings_playback_minutes_before_end), stringResource(Res.string.settings_playback_minutes_before_end_description)),
+ ),
+ )
+
+ addContinueWatchingRows(
+ addRow = ::addRow,
+ pageLabel = continueWatchingPage,
+ section = stringResource(Res.string.settings_continue_watching_section_visibility),
+ icon = Icons.Rounded.Style,
+ rows = listOf(
+ PlaybackSearchRow(
+ "show-continue-watching",
+ stringResource(Res.string.settings_continue_watching_show_title),
+ stringResource(Res.string.settings_continue_watching_show_description),
+ ),
+ ),
+ )
+ addContinueWatchingRows(
+ addRow = ::addRow,
+ pageLabel = continueWatchingPage,
+ section = stringResource(Res.string.settings_continue_watching_section_up_next_behavior),
+ icon = Icons.Rounded.Style,
+ rows = listOf(
+ PlaybackSearchRow("episode-thumbnails", stringResource(Res.string.settings_continue_watching_use_episode_thumbnails_title), stringResource(Res.string.settings_continue_watching_use_episode_thumbnails_description)),
+ PlaybackSearchRow("up-next", stringResource(Res.string.settings_continue_watching_up_next_title), stringResource(Res.string.settings_continue_watching_up_next_description)),
+ PlaybackSearchRow("unaired-next-up", stringResource(Res.string.settings_continue_watching_show_unaired_next_up_title), stringResource(Res.string.settings_continue_watching_show_unaired_next_up_description)),
+ PlaybackSearchRow("blur-next-up", stringResource(Res.string.settings_continue_watching_blur_next_up_title), stringResource(Res.string.settings_continue_watching_blur_next_up_description)),
+ ),
+ )
+ addContinueWatchingRows(
+ addRow = ::addRow,
+ pageLabel = continueWatchingPage,
+ section = stringResource(Res.string.settings_continue_watching_section_on_launch),
+ icon = Icons.Rounded.Style,
+ rows = listOf(
+ PlaybackSearchRow("resume-prompt", stringResource(Res.string.settings_continue_watching_resume_prompt_title), stringResource(Res.string.settings_continue_watching_resume_prompt_description)),
+ ),
+ )
+
+ val posterSection = stringResource(Res.string.settings_poster_card_style)
+ listOf(
+ PlaybackSearchRow("poster-width", stringResource(Res.string.settings_poster_card_width)),
+ PlaybackSearchRow("poster-radius", stringResource(Res.string.settings_poster_card_radius)),
+ PlaybackSearchRow("poster-landscape", stringResource(Res.string.settings_poster_landscape_mode)),
+ PlaybackSearchRow("poster-hide-labels", stringResource(Res.string.settings_poster_hide_labels)),
+ ).forEach { row ->
+ addRow(
+ page = SettingsPage.PosterCustomization,
+ key = "poster-${row.key}",
+ title = row.title,
+ description = row.description,
+ pageLabel = posterStylePage,
+ section = posterSection,
+ icon = Icons.Rounded.Tune,
+ )
+ }
+
+ val homeLayoutSection = stringResource(Res.string.settings_homescreen_section_hero)
+ listOf(
+ PlaybackSearchRow("home-hero", stringResource(Res.string.settings_homescreen_show_hero), stringResource(Res.string.settings_homescreen_show_hero_description)),
+ PlaybackSearchRow("home-hide-unreleased", stringResource(Res.string.layout_hide_unreleased), stringResource(Res.string.layout_hide_unreleased_sub)),
+ PlaybackSearchRow("home-hero-sources", stringResource(Res.string.settings_homescreen_section_hero_sources)),
+ PlaybackSearchRow("home-catalogs", stringResource(Res.string.settings_homescreen_section_catalogs)),
+ ).forEach { row ->
+ addRow(
+ page = SettingsPage.Homescreen,
+ key = row.key,
+ title = row.title,
+ description = row.description,
+ pageLabel = homeLayoutPage,
+ section = homeLayoutSection,
+ icon = Icons.Rounded.Home,
+ )
+ }
+
+ val detailAppearanceSection = stringResource(Res.string.settings_meta_section_appearance)
+ listOf(
+ PlaybackSearchRow("meta-cinematic", stringResource(Res.string.settings_meta_cinematic_background), stringResource(Res.string.settings_meta_cinematic_background_description)),
+ PlaybackSearchRow("meta-tabs", stringResource(Res.string.settings_meta_tab_layout), stringResource(Res.string.settings_meta_tab_layout_description)),
+ PlaybackSearchRow("meta-episode-cards", stringResource(Res.string.settings_meta_episode_cards), stringResource(Res.string.settings_meta_episode_cards_description)),
+ PlaybackSearchRow("meta-blur-episodes", stringResource(Res.string.settings_meta_blur_unwatched_episodes), stringResource(Res.string.settings_meta_blur_unwatched_episodes_description)),
+ ).forEach { row ->
+ addRow(
+ page = SettingsPage.MetaScreen,
+ key = row.key,
+ title = row.title,
+ description = row.description,
+ pageLabel = detailPage,
+ section = detailAppearanceSection,
+ icon = Icons.Rounded.Tune,
+ )
+ }
+ val detailSectionsSection = stringResource(Res.string.settings_meta_section_sections)
+ listOf(
+ PlaybackSearchRow("meta-overview", stringResource(Res.string.settings_meta_overview), stringResource(Res.string.settings_meta_overview_description)),
+ PlaybackSearchRow("meta-actions", stringResource(Res.string.settings_meta_actions), stringResource(Res.string.settings_meta_actions_description)),
+ PlaybackSearchRow("meta-details", stringResource(Res.string.settings_meta_details), stringResource(Res.string.settings_meta_details_description)),
+ PlaybackSearchRow("meta-trailers", stringResource(Res.string.settings_meta_trailers), stringResource(Res.string.settings_meta_trailers_description)),
+ PlaybackSearchRow("meta-cast", stringResource(Res.string.settings_meta_cast), stringResource(Res.string.settings_meta_cast_description)),
+ PlaybackSearchRow("meta-episodes", stringResource(Res.string.settings_meta_episodes), stringResource(Res.string.settings_meta_episodes_description)),
+ PlaybackSearchRow("meta-production", stringResource(Res.string.settings_meta_production), stringResource(Res.string.settings_meta_production_description)),
+ PlaybackSearchRow("meta-more-like-this", stringResource(Res.string.settings_meta_more_like_this), stringResource(Res.string.settings_meta_more_like_this_description)),
+ PlaybackSearchRow("meta-collection", stringResource(Res.string.settings_meta_collection), stringResource(Res.string.settings_meta_collection_description)),
+ PlaybackSearchRow("meta-comments", stringResource(Res.string.settings_meta_comments), stringResource(Res.string.settings_meta_comments_description)),
+ ).forEach { row ->
+ addRow(
+ page = SettingsPage.MetaScreen,
+ key = row.key,
+ title = row.title,
+ description = row.description,
+ pageLabel = detailPage,
+ section = detailSectionsSection,
+ icon = Icons.Rounded.Tune,
+ )
+ }
+
+ addPage(
+ page = SettingsPage.TmdbEnrichment,
+ key = "tmdb",
+ title = tmdbPage,
+ description = stringResource(Res.string.settings_integrations_tmdb_description),
+ icon = Icons.Rounded.Link,
+ )
+ addPage(
+ page = SettingsPage.MdbListRatings,
+ key = "mdblist",
+ title = mdbListPage,
+ description = stringResource(Res.string.settings_integrations_mdblist_description),
+ icon = Icons.Rounded.Link,
+ )
+ val tmdbModulesSection = stringResource(Res.string.settings_tmdb_section_modules)
+ listOf(
+ PlaybackSearchRow("tmdb-enable", stringResource(Res.string.settings_tmdb_enable_enrichment), stringResource(Res.string.settings_tmdb_enable_enrichment_description), stringResource(Res.string.settings_tmdb_section_title)),
+ PlaybackSearchRow("tmdb-api-key", stringResource(Res.string.settings_tmdb_personal_api_key), "", stringResource(Res.string.settings_tmdb_section_credentials)),
+ PlaybackSearchRow("tmdb-language", stringResource(Res.string.settings_tmdb_preferred_language), stringResource(Res.string.settings_tmdb_preferred_language_description), stringResource(Res.string.settings_tmdb_section_localization)),
+ PlaybackSearchRow("tmdb-trailers", stringResource(Res.string.settings_tmdb_module_trailers), stringResource(Res.string.settings_tmdb_module_trailers_description), tmdbModulesSection),
+ PlaybackSearchRow("tmdb-artwork", stringResource(Res.string.settings_tmdb_module_artwork), stringResource(Res.string.settings_tmdb_module_artwork_description), tmdbModulesSection),
+ PlaybackSearchRow("tmdb-basic-info", stringResource(Res.string.settings_tmdb_module_basic_info), stringResource(Res.string.settings_tmdb_module_basic_info_description), tmdbModulesSection),
+ PlaybackSearchRow("tmdb-details", stringResource(Res.string.settings_tmdb_module_details), stringResource(Res.string.settings_tmdb_module_details_description), tmdbModulesSection),
+ PlaybackSearchRow("tmdb-credits", stringResource(Res.string.settings_tmdb_module_credits), stringResource(Res.string.settings_tmdb_module_credits_description), tmdbModulesSection),
+ PlaybackSearchRow("tmdb-companies", stringResource(Res.string.settings_tmdb_module_production_companies), stringResource(Res.string.settings_tmdb_module_production_companies_description), tmdbModulesSection),
+ PlaybackSearchRow("tmdb-networks", stringResource(Res.string.settings_tmdb_module_networks), stringResource(Res.string.settings_tmdb_module_networks_description), tmdbModulesSection),
+ PlaybackSearchRow("tmdb-episodes", stringResource(Res.string.settings_tmdb_module_episodes), stringResource(Res.string.settings_tmdb_module_episodes_description), tmdbModulesSection),
+ PlaybackSearchRow("tmdb-season-posters", stringResource(Res.string.settings_tmdb_module_season_posters), stringResource(Res.string.settings_tmdb_module_season_posters_description), tmdbModulesSection),
+ PlaybackSearchRow("tmdb-more-like-this", stringResource(Res.string.settings_tmdb_module_more_like_this), stringResource(Res.string.settings_tmdb_module_more_like_this_description), tmdbModulesSection),
+ PlaybackSearchRow("tmdb-collections", stringResource(Res.string.settings_tmdb_module_collections), stringResource(Res.string.settings_tmdb_module_collections_description), tmdbModulesSection),
+ ).forEach { row ->
+ addRow(
+ page = SettingsPage.TmdbEnrichment,
+ key = row.key,
+ title = row.title,
+ description = row.description,
+ pageLabel = tmdbPage,
+ section = row.sectionOverride ?: tmdbModulesSection,
+ icon = Icons.Rounded.Link,
+ )
+ }
+
+ listOf(
+ PlaybackSearchRow("mdb-enable", stringResource(Res.string.settings_mdb_enable_ratings), stringResource(Res.string.settings_mdb_enable_ratings_description), stringResource(Res.string.settings_mdb_section_title)),
+ PlaybackSearchRow("mdb-api-key", stringResource(Res.string.settings_mdb_api_key_title), stringResource(Res.string.settings_mdb_api_key_description), stringResource(Res.string.settings_mdb_section_api_key)),
+ PlaybackSearchRow("mdb-imdb", stringResource(Res.string.source_imdb), "", stringResource(Res.string.settings_mdb_section_rating_providers)),
+ PlaybackSearchRow("mdb-tmdb", stringResource(Res.string.source_tmdb), "", stringResource(Res.string.settings_mdb_section_rating_providers)),
+ PlaybackSearchRow("mdb-tomatoes", stringResource(Res.string.source_rotten_tomatoes), "", stringResource(Res.string.settings_mdb_section_rating_providers)),
+ PlaybackSearchRow("mdb-metacritic", stringResource(Res.string.source_metacritic), "", stringResource(Res.string.settings_mdb_section_rating_providers)),
+ PlaybackSearchRow("mdb-trakt", stringResource(Res.string.source_trakt), "", stringResource(Res.string.settings_mdb_section_rating_providers)),
+ PlaybackSearchRow("mdb-letterboxd", stringResource(Res.string.source_letterboxd), "", stringResource(Res.string.settings_mdb_section_rating_providers)),
+ PlaybackSearchRow("mdb-audience", stringResource(Res.string.source_audience_score), "", stringResource(Res.string.settings_mdb_section_rating_providers)),
+ ).forEach { row ->
+ addRow(
+ page = SettingsPage.MdbListRatings,
+ key = row.key,
+ title = row.title,
+ description = row.description,
+ pageLabel = mdbListPage,
+ section = row.sectionOverride ?: stringResource(Res.string.settings_mdb_section_title),
+ icon = Icons.Rounded.Link,
+ )
+ }
+
+ val notificationsAlerts = stringResource(Res.string.settings_notifications_section_alerts)
+ addRow(
+ page = SettingsPage.Notifications,
+ key = "episode-release-alerts",
+ title = stringResource(Res.string.settings_notifications_episode_release_alerts),
+ description = stringResource(Res.string.settings_notifications_episode_release_alerts_description),
+ pageLabel = notificationsPage,
+ section = notificationsAlerts,
+ icon = Icons.Rounded.Notifications,
+ )
+ addRow(
+ page = SettingsPage.Notifications,
+ key = "notification-test",
+ title = stringResource(Res.string.settings_notifications_test_title),
+ pageLabel = notificationsPage,
+ section = stringResource(Res.string.settings_notifications_section_test),
+ icon = Icons.Rounded.Notifications,
+ )
+
+ addRow(
+ page = SettingsPage.TraktAuthentication,
+ key = "trakt-authentication",
+ title = stringResource(Res.string.settings_trakt_authentication),
+ description = stringResource(Res.string.settings_trakt_intro_description),
+ pageLabel = traktPage,
+ section = stringResource(Res.string.settings_trakt_authentication),
+ category = accountCategory,
+ icon = Icons.Rounded.Link,
+ )
+ listOf(
+ PlaybackSearchRow("trakt-library-source", stringResource(Res.string.trakt_library_source_title), stringResource(Res.string.trakt_library_source_subtitle)),
+ PlaybackSearchRow("trakt-watch-progress", stringResource(Res.string.trakt_watch_progress_title), stringResource(Res.string.trakt_watch_progress_subtitle)),
+ PlaybackSearchRow("trakt-continue-watching-window", stringResource(Res.string.trakt_continue_watching_window), stringResource(Res.string.trakt_continue_watching_subtitle)),
+ PlaybackSearchRow("trakt-comments", stringResource(Res.string.settings_trakt_comments), stringResource(Res.string.settings_trakt_comments_description)),
+ ).forEach { row ->
+ addRow(
+ page = SettingsPage.TraktAuthentication,
+ key = row.key,
+ title = row.title,
+ description = row.description,
+ pageLabel = traktPage,
+ section = stringResource(Res.string.settings_trakt_features),
+ category = accountCategory,
+ icon = Icons.Rounded.Link,
+ )
+ }
+
+ return entries
+}
+
+private data class PlaybackSearchRow(
+ val key: String,
+ val title: String,
+ val description: String = "",
+ val sectionOverride: String? = null,
+)
+
+private fun addPlaybackRows(
+ addRow: (
+ page: SettingsPage,
+ key: String,
+ title: String,
+ description: String,
+ pageLabel: String,
+ section: String,
+ category: String,
+ icon: ImageVector,
+ ) -> Unit,
+ pageLabel: String,
+ section: String,
+ icon: ImageVector,
+ rows: List,
+) {
+ rows.forEach { row ->
+ addRow(
+ SettingsPage.Playback,
+ "playback-${row.key}",
+ row.title,
+ row.description,
+ pageLabel,
+ section,
+ "",
+ icon,
+ )
+ }
+}
+
+private fun addContinueWatchingRows(
+ addRow: (
+ page: SettingsPage,
+ key: String,
+ title: String,
+ description: String,
+ pageLabel: String,
+ section: String,
+ category: String,
+ icon: ImageVector,
+ ) -> Unit,
+ pageLabel: String,
+ section: String,
+ icon: ImageVector,
+ rows: List,
+) {
+ rows.forEach { row ->
+ addRow(
+ SettingsPage.ContinueWatching,
+ "continue-watching-${row.key}",
+ row.title,
+ row.description,
+ pageLabel,
+ section,
+ "",
+ icon,
+ )
+ }
+}
+
+internal fun LazyListScope.settingsSearchRootContent(
+ query: String,
+ entries: List,
+ isTablet: Boolean,
+ showSearchField: Boolean,
+ animateSearchField: Boolean,
+ onQueryChange: (String) -> Unit,
+ onTargetClick: (SettingsSearchTarget) -> Unit,
+) {
+ if (showSearchField || query.isNotBlank()) {
+ item(key = "settings-search-field") {
+ SettingsSearchRevealItem(animate = animateSearchField) {
+ SettingsSearchField(
+ query = query,
+ onQueryChange = onQueryChange,
+ )
+ }
+ }
+ }
+
+ if (query.isBlank()) return
+
+ val results = settingsSearchResults(
+ query = query,
+ entries = entries,
+ )
+
+ item(key = "settings-search-results") {
+ if (results.isEmpty()) {
+ SettingsSearchEmptyState(isTablet = isTablet)
+ } else {
+ SettingsSection(
+ title = stringResource(Res.string.settings_search_results_section),
+ isTablet = isTablet,
+ ) {
+ SettingsGroup(isTablet = isTablet) {
+ results.forEachIndexed { index, entry ->
+ if (index > 0) {
+ SettingsGroupDivider(isTablet = isTablet)
+ }
+ SettingsNavigationRow(
+ title = entry.title,
+ description = entry.resultDescription(),
+ icon = entry.icon,
+ isTablet = isTablet,
+ onClick = { onTargetClick(entry.target) },
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun SettingsSearchRevealItem(
+ animate: Boolean,
+ content: @Composable () -> Unit,
+) {
+ if (!animate) {
+ content()
+ return
+ }
+
+ val visibleState = remember {
+ MutableTransitionState(false).apply {
+ targetState = true
+ }
+ }
+ AnimatedVisibility(
+ visibleState = visibleState,
+ enter = expandVertically(
+ animationSpec = tween(durationMillis = 220),
+ expandFrom = Alignment.Top,
+ ) + fadeIn(
+ animationSpec = tween(durationMillis = 180),
+ ) + slideInVertically(
+ animationSpec = tween(durationMillis = 220),
+ initialOffsetY = { -it / 4 },
+ ),
+ ) {
+ content()
+ }
+}
+
+@Composable
+private fun SettingsSearchField(
+ query: String,
+ onQueryChange: (String) -> Unit,
+) {
+ OutlinedTextField(
+ value = query,
+ onValueChange = onQueryChange,
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ shape = RoundedCornerShape(14.dp),
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Rounded.Search,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ },
+ trailingIcon = if (query.isNotBlank()) {
+ {
+ IconButton(onClick = { onQueryChange("") }) {
+ Icon(
+ imageVector = Icons.Rounded.Close,
+ contentDescription = stringResource(Res.string.compose_search_clear),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+ } else {
+ null
+ },
+ placeholder = {
+ Text(
+ text = stringResource(Res.string.settings_search_placeholder),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ },
+ textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = MaterialTheme.colorScheme.outline,
+ unfocusedBorderColor = MaterialTheme.colorScheme.outline,
+ focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
+ unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
+ cursorColor = MaterialTheme.colorScheme.primary,
+ ),
+ )
+}
+
+@Composable
+private fun SettingsSearchEmptyState(isTablet: Boolean) {
+ SettingsSection(
+ title = stringResource(Res.string.settings_search_results_section),
+ isTablet = isTablet,
+ ) {
+ SettingsGroup(isTablet = isTablet) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = if (isTablet) 20.dp else 16.dp, vertical = 18.dp),
+ ) {
+ Text(
+ text = stringResource(Res.string.settings_search_empty),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.Medium,
+ )
+ }
+ }
+ }
+}
+
+private fun settingsSearchResults(
+ query: String,
+ entries: List,
+): List {
+ val terms = query
+ .trim()
+ .lowercase()
+ .split(Regex("\\s+"))
+ .filter { it.isNotBlank() }
+
+ if (terms.isEmpty()) return emptyList()
+
+ return entries.filter { entry ->
+ terms.all { term -> entry.searchableText.contains(term) }
+ }
+}
+
+private fun SettingsSearchEntry.resultDescription(): String {
+ return description.ifBlank { contextLabel }
+}
diff --git a/iosApp/Configuration/Version.xcconfig b/iosApp/Configuration/Version.xcconfig
index e29881bae..7e78cee85 100644
--- a/iosApp/Configuration/Version.xcconfig
+++ b/iosApp/Configuration/Version.xcconfig
@@ -1,3 +1,3 @@
CURRENT_PROJECT_VERSION=55
-MARKETING_VERSION=0.1.16
+MARKETING_VERSION=0.1.0