From 38c882980c3cbe3b15bc9c3a0be47fb2c0d89d6e Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sun, 10 May 2026 12:36:15 +0530 Subject: [PATCH] feat: adding attributions page --- .../IntegrationLogoPainter.android.kt | 2 + .../drawable/introdb_favicon.png | Bin 0 -> 22040 bytes .../composeResources/values/strings.xml | 24 ++ .../commonMain/kotlin/com/nuvio/app/App.kt | 18 + .../settings/IntegrationLogoPainter.kt | 1 + .../settings/LicensesAttributionsPage.kt | 341 ++++++++++++++++++ .../app/features/settings/SettingsModels.kt | 6 + .../app/features/settings/SettingsRootPage.kt | 11 + .../app/features/settings/SettingsScreen.kt | 14 + .../app/features/settings/SettingsSearch.kt | 42 +++ .../settings/IntegrationLogoPainter.ios.kt | 2 + 11 files changed, 461 insertions(+) create mode 100644 composeApp/src/commonMain/composeResources/drawable/introdb_favicon.png create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/LicensesAttributionsPage.kt diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.android.kt index 23f99ee8..a2140bde 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.android.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import com.nuvio.app.R import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.introdb_favicon import nuvio.composeapp.generated.resources.rating_tmdb import org.jetbrains.compose.resources.painterResource as composePainterResource @@ -14,4 +15,5 @@ internal actual fun integrationLogoPainter(logo: IntegrationLogo): Painter = IntegrationLogo.Tmdb -> composePainterResource(Res.drawable.rating_tmdb) IntegrationLogo.Trakt -> painterResource(id = R.drawable.trakt_tv_favicon) IntegrationLogo.MdbList -> painterResource(id = R.drawable.mdblist_logo) + IntegrationLogo.IntroDb -> composePainterResource(Res.drawable.introdb_favicon) } diff --git a/composeApp/src/commonMain/composeResources/drawable/introdb_favicon.png b/composeApp/src/commonMain/composeResources/drawable/introdb_favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..e3e357b9a3fa035c64711d43d24f16788e057ed5 GIT binary patch literal 22040 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 + Data sources, acknowledgements, and platform licenses Open recognition and project credits Back Cancel @@ -366,6 +367,7 @@ Continue Watching Home Layout Integrations + Licenses & Attribution MDBList Ratings Detail Page Notifications @@ -394,6 +396,28 @@ No settings found. Search settings... RESULTS + APP LICENSE + DATA & SERVICES + PLAYBACK LICENSE + Nuvio Mobile + Source code and license terms are available in the project repository. + Licensed under the GNU General Public License v3.0. + The Movie Database (TMDB) + Nuvio uses the TMDB API for movie and TV metadata, artwork, trailers, cast, production details, collections, and recommendations. This product uses the TMDB API but is not endorsed or certified by TMDB. + IMDb Non-Commercial Datasets + Nuvio uses IMDb Non-Commercial Datasets, including title.ratings.tsv.gz, for IMDb ratings and vote counts. Information courtesy of IMDb (https://www.imdb.com). Used with permission. IMDb data is for personal and non-commercial use under IMDb's terms. + Trakt + Nuvio connects to Trakt for account authentication, watched history, progress sync, library data, ratings, lists, and comments. Nuvio is not affiliated with or endorsed by Trakt. + MDBList + Nuvio uses MDBList for ratings and external score provider data. Nuvio is not affiliated with or endorsed by MDBList. + IntroDB + Nuvio uses the IntroDB API for community-provided intro, recap, credits, and preview timestamps used by skip controls. Nuvio is not affiliated with or endorsed by IntroDB. + MPVKit + Used for playback on iOS builds. + MPVKit source alone is licensed under LGPL v3.0. MPVKit bundles, including libmpv and FFmpeg libraries, are also licensed under LGPL v3.0. + AndroidX Media3 ExoPlayer 1.8.0 + Used for playback on Android builds. + Licensed under the Apache License, Version 2.0. Loading your Trakt lists… Choose where to save this title on Trakt Donate diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 1d605c3c..9a7be453 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -145,6 +145,7 @@ import com.nuvio.app.features.settings.AddonsSettingsScreen import com.nuvio.app.features.settings.PluginsSettingsScreen import com.nuvio.app.features.settings.AccountSettingsScreen import com.nuvio.app.features.settings.SupportersContributorsSettingsScreen +import com.nuvio.app.features.settings.LicensesAttributionsSettingsScreen import com.nuvio.app.features.settings.ThemeSettingsRepository import com.nuvio.app.features.collection.CollectionManagementScreen import com.nuvio.app.features.collection.CollectionEditorScreen @@ -237,6 +238,9 @@ object AccountSettingsRoute @Serializable object SupportersContributorsSettingsRoute +@Serializable +object LicensesAttributionsSettingsRoute + @Serializable object CollectionsRoute @@ -1065,6 +1069,9 @@ private fun MainAppContent( onSupportersContributorsSettingsClick = { navController.navigate(SupportersContributorsSettingsRoute) }, + onLicensesAttributionsSettingsClick = { + navController.navigate(LicensesAttributionsSettingsRoute) + }, onCheckForUpdatesClick = if (AppFeaturePolicy.inAppUpdaterEnabled) { { appUpdaterController.checkForUpdates( @@ -1698,6 +1705,15 @@ private fun MainAppContent( onBack = onBack, ) } + composable { backStackEntry -> + val onBack = rememberGuardedPopBackStack( + navController = navController, + backStackEntry = backStackEntry, + ) + LicensesAttributionsSettingsScreen( + onBack = onBack, + ) + } composable { backStackEntry -> val onBack = rememberGuardedPopBackStack( navController = navController, @@ -1972,6 +1988,7 @@ private fun AppTabHost( onPluginsSettingsClick: () -> Unit = {}, onAccountSettingsClick: () -> Unit = {}, onSupportersContributorsSettingsClick: () -> Unit = {}, + onLicensesAttributionsSettingsClick: () -> Unit = {}, onCheckForUpdatesClick: (() -> Unit)? = null, onCollectionsSettingsClick: () -> Unit = {}, onFolderClick: ((collectionId: String, folderId: String) -> Unit)? = null, @@ -2024,6 +2041,7 @@ private fun AppTabHost( onPluginsClick = onPluginsSettingsClick, onAccountClick = onAccountSettingsClick, onSupportersContributorsClick = onSupportersContributorsSettingsClick, + onLicensesAttributionsClick = onLicensesAttributionsSettingsClick, onCheckForUpdatesClick = onCheckForUpdatesClick, onCollectionsClick = onCollectionsSettingsClick, ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.kt index 4871bb16..8bf7971d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.kt @@ -7,6 +7,7 @@ internal enum class IntegrationLogo { Tmdb, Trakt, MdbList, + IntroDb, } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/LicensesAttributionsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/LicensesAttributionsPage.kt new file mode 100644 index 00000000..7efecdf5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/LicensesAttributionsPage.kt @@ -0,0 +1,341 @@ +package com.nuvio.app.features.settings + +import androidx.compose.foundation.Image +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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.OpenInNew +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalUriHandler +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.NuvioScreen +import com.nuvio.app.core.ui.NuvioScreenHeader +import com.nuvio.app.isIos +import nuvio.composeapp.generated.resources.* +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource + +private const val TmdbUrl = "https://www.themoviedb.org" +private const val ImdbDatasetsUrl = "https://developer.imdb.com/non-commercial-datasets/" +private const val TraktUrl = "https://trakt.tv" +private const val MdbListUrl = "https://mdblist.com" +private const val IntroDbUrl = "https://introdb.app/" +private const val NuvioRepositoryUrl = "https://github.com/NuvioMedia/NuvioMobile" +private const val MpvKitUrl = "https://github.com/mpvkit/MPVKit" +private const val ApacheLicenseUrl = "https://www.apache.org/licenses/LICENSE-2.0" + +private data class AttributionItem( + val titleRes: StringResource, + val bodyRes: StringResource, + val logo: IntegrationLogo?, + val link: String, +) + +private data class LicenseItem( + val titleRes: StringResource, + val bodyRes: StringResource, + val licenseRes: StringResource, + val link: String, +) + +@Composable +fun LicensesAttributionsSettingsScreen( + onBack: () -> Unit, +) { + NuvioScreen( + modifier = Modifier.fillMaxSize(), + ) { + stickyHeader { + NuvioScreenHeader( + title = stringResource(Res.string.compose_settings_page_licenses_attributions), + onBack = onBack, + ) + } + licensesAttributionsContent(isTablet = false) + } +} + +internal fun LazyListScope.licensesAttributionsContent( + isTablet: Boolean, +) { + item { + LicensesAttributionsBody(isTablet = isTablet) + } +} + +@Composable +private fun LicensesAttributionsBody( + isTablet: Boolean, +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(if (isTablet) 28.dp else 24.dp), + ) { + PlainSettingsStack( + title = stringResource(Res.string.settings_licenses_attributions_section_app), + isTablet = isTablet, + ) { + LicenseRow( + item = appLicenseItem(), + isTablet = isTablet, + ) + } + + PlainSettingsStack( + title = stringResource(Res.string.settings_licenses_attributions_section_data), + isTablet = isTablet, + ) { + val items = attributionItems() + items.forEachIndexed { index, item -> + AttributionRow( + item = item, + isTablet = isTablet, + ) + if (index != items.lastIndex) { + PlainStackDivider() + } + } + } + + PlainSettingsStack( + title = stringResource(Res.string.settings_licenses_attributions_section_playback), + isTablet = isTablet, + ) { + LicenseRow( + item = platformLicenseItem(), + isTablet = isTablet, + ) + } + } +} + +@Composable +private fun PlainSettingsStack( + title: String, + isTablet: Boolean, + content: @Composable () -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = title, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.height(if (isTablet) 12.dp else 10.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + ) { + content() + } + } +} + +@Composable +private fun AttributionRow( + item: AttributionItem, + isTablet: Boolean, +) { + val uriHandler = LocalUriHandler.current + val title = stringResource(item.titleRes) + LinkedPlainRow( + title = title, + body = stringResource(item.bodyRes), + link = item.link, + isTablet = isTablet, + leading = item.logo?.let { logo -> + { + IntegrationLogoImage( + painter = integrationLogoPainter(logo), + contentDescription = title, + isTablet = isTablet, + ) + } + }, + onOpen = { uriHandler.openUri(item.link) }, + ) +} + +@Composable +private fun LicenseRow( + item: LicenseItem, + isTablet: Boolean, +) { + val uriHandler = LocalUriHandler.current + val itemBody = stringResource(item.bodyRes) + val itemLicense = stringResource(item.licenseRes) + val body = buildString { + append(itemBody) + append("\n") + append(itemLicense) + } + LinkedPlainRow( + title = stringResource(item.titleRes), + body = body, + link = item.link, + isTablet = isTablet, + onOpen = { uriHandler.openUri(item.link) }, + ) +} + +@Composable +private fun LinkedPlainRow( + title: String, + body: String, + link: String, + isTablet: Boolean, + leading: (@Composable () -> Unit)? = null, + onOpen: () -> Unit, +) { + val verticalPadding = if (isTablet) 18.dp else 16.dp + val horizontalPadding = if (isTablet) 4.dp else 0.dp + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onOpen) + .padding(horizontal = horizontalPadding, vertical = verticalPadding), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(if (isTablet) 18.dp else 14.dp), + ) { + leading?.invoke() + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = body, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = link, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Rounded.OpenInNew, + contentDescription = null, + modifier = Modifier + .padding(top = 2.dp) + .size(if (isTablet) 22.dp else 20.dp) + .alpha(0.72f), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + +@Composable +private fun IntegrationLogoImage( + painter: Painter, + contentDescription: String, + isTablet: Boolean, +) { + Image( + painter = painter, + contentDescription = contentDescription, + modifier = Modifier + .padding(top = 2.dp) + .size(if (isTablet) 46.dp else 40.dp), + contentScale = ContentScale.Fit, + ) +} + +@Composable +private fun PlainStackDivider() { + HorizontalDivider( + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.18f), + ) +} + +private fun attributionItems(): List = listOf( + AttributionItem( + titleRes = Res.string.settings_licenses_attributions_tmdb_title, + bodyRes = Res.string.settings_licenses_attributions_tmdb_body, + logo = IntegrationLogo.Tmdb, + link = TmdbUrl, + ), + AttributionItem( + titleRes = Res.string.settings_licenses_attributions_trakt_title, + bodyRes = Res.string.settings_licenses_attributions_trakt_body, + logo = IntegrationLogo.Trakt, + link = TraktUrl, + ), + AttributionItem( + titleRes = Res.string.settings_licenses_attributions_mdblist_title, + bodyRes = Res.string.settings_licenses_attributions_mdblist_body, + logo = IntegrationLogo.MdbList, + link = MdbListUrl, + ), + AttributionItem( + titleRes = Res.string.settings_licenses_attributions_introdb_title, + bodyRes = Res.string.settings_licenses_attributions_introdb_body, + logo = IntegrationLogo.IntroDb, + link = IntroDbUrl, + ), + AttributionItem( + titleRes = Res.string.settings_licenses_attributions_imdb_title, + bodyRes = Res.string.settings_licenses_attributions_imdb_body, + logo = null, + link = ImdbDatasetsUrl, + ), +) + +private fun appLicenseItem(): LicenseItem = + LicenseItem( + titleRes = Res.string.settings_licenses_attributions_nuvio_title, + bodyRes = Res.string.settings_licenses_attributions_nuvio_body, + licenseRes = Res.string.settings_licenses_attributions_nuvio_license, + link = NuvioRepositoryUrl, + ) + +private fun platformLicenseItem(): LicenseItem = + if (isIos) { + LicenseItem( + titleRes = Res.string.settings_licenses_attributions_mpvkit_title, + bodyRes = Res.string.settings_licenses_attributions_mpvkit_body, + licenseRes = Res.string.settings_licenses_attributions_mpvkit_license, + link = MpvKitUrl, + ) + } else { + LicenseItem( + titleRes = Res.string.settings_licenses_attributions_exoplayer_title, + bodyRes = Res.string.settings_licenses_attributions_exoplayer_body, + licenseRes = Res.string.settings_licenses_attributions_exoplayer_license, + link = ApacheLicenseUrl, + ) + } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsModels.kt index e66779fd..d030a785 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsModels.kt @@ -16,6 +16,7 @@ import nuvio.composeapp.generated.resources.compose_settings_page_content_discov import nuvio.composeapp.generated.resources.compose_settings_page_continue_watching import nuvio.composeapp.generated.resources.compose_settings_page_homescreen import nuvio.composeapp.generated.resources.compose_settings_page_integrations +import nuvio.composeapp.generated.resources.compose_settings_page_licenses_attributions import nuvio.composeapp.generated.resources.compose_settings_page_mdblist_ratings import nuvio.composeapp.generated.resources.compose_settings_page_meta_screen import nuvio.composeapp.generated.resources.compose_settings_page_notifications @@ -58,6 +59,11 @@ internal enum class SettingsPage( category = SettingsCategory.About, parentPage = Root, ), + LicensesAttributions( + titleRes = Res.string.compose_settings_page_licenses_attributions, + category = SettingsCategory.About, + parentPage = Root, + ), Playback( titleRes = Res.string.compose_settings_page_playback, category = SettingsCategory.General, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt index e97576f6..71580c35 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt @@ -26,6 +26,7 @@ import nuvio.composeapp.generated.resources.compose_about_version_format import nuvio.composeapp.generated.resources.compose_settings_page_account import nuvio.composeapp.generated.resources.compose_settings_page_appearance import nuvio.composeapp.generated.resources.compose_settings_page_integrations +import nuvio.composeapp.generated.resources.compose_settings_page_licenses_attributions import nuvio.composeapp.generated.resources.compose_settings_page_notifications import nuvio.composeapp.generated.resources.compose_settings_page_playback import nuvio.composeapp.generated.resources.compose_settings_page_supporters_contributors @@ -48,6 +49,7 @@ import nuvio.composeapp.generated.resources.compose_settings_page_content_discov import nuvio.composeapp.generated.resources.compose_settings_page_trakt import nuvio.composeapp.generated.resources.settings_playback_subtitle import nuvio.composeapp.generated.resources.about_supporters_contributors_subtitle +import nuvio.composeapp.generated.resources.about_licenses_attributions_subtitle import org.jetbrains.compose.resources.stringResource internal fun LazyListScope.settingsRootContent( @@ -59,6 +61,7 @@ internal fun LazyListScope.settingsRootContent( onIntegrationsClick: () -> Unit, onTraktClick: () -> Unit, onSupportersContributorsClick: () -> Unit, + onLicensesAttributionsClick: () -> Unit, onCheckForUpdatesClick: (() -> Unit)? = null, onDownloadsClick: () -> Unit, onAccountClick: () -> Unit, @@ -175,6 +178,14 @@ internal fun LazyListScope.settingsRootContent( isTablet = isTablet, onClick = onSupportersContributorsClick, ) + SettingsGroupDivider(isTablet = isTablet) + SettingsNavigationRow( + title = stringResource(Res.string.compose_settings_page_licenses_attributions), + description = stringResource(Res.string.about_licenses_attributions_subtitle), + icon = Icons.Rounded.Info, + isTablet = isTablet, + onClick = onLicensesAttributionsClick, + ) if (onCheckForUpdatesClick != null) { SettingsGroupDivider(isTablet = isTablet) SettingsNavigationRow( 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 3cb0a92a..7d3b0e04 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 @@ -97,6 +97,7 @@ fun SettingsScreen( onDownloadsClick: () -> Unit = {}, onAccountClick: () -> Unit = {}, onSupportersContributorsClick: () -> Unit = {}, + onLicensesAttributionsClick: () -> Unit = {}, onCheckForUpdatesClick: (() -> Unit)? = null, onCollectionsClick: () -> Unit = {}, ) { @@ -243,6 +244,7 @@ fun SettingsScreen( onSwitchProfile = onSwitchProfile, onDownloadsClick = onDownloadsClick, onSupportersContributorsClick = onSupportersContributorsClick, + onLicensesAttributionsClick = onLicensesAttributionsClick, onCheckForUpdatesClick = onCheckForUpdatesClick, onCollectionsClick = onCollectionsClick, ) @@ -294,6 +296,7 @@ fun SettingsScreen( onDownloadsClick = onDownloadsClick, onAccountClick = onAccountClick, onSupportersContributorsClick = onSupportersContributorsClick, + onLicensesAttributionsClick = onLicensesAttributionsClick, onCheckForUpdatesClick = onCheckForUpdatesClick, onCollectionsClick = onCollectionsClick, ) @@ -349,6 +352,7 @@ private fun MobileSettingsScreen( onDownloadsClick: () -> Unit = {}, onAccountClick: () -> Unit = {}, onSupportersContributorsClick: () -> Unit = {}, + onLicensesAttributionsClick: () -> Unit = {}, onCheckForUpdatesClick: (() -> Unit)? = null, onCollectionsClick: () -> Unit = {}, ) { @@ -385,6 +389,7 @@ private fun MobileSettingsScreen( is SettingsSearchTarget.Page -> when (target.page) { SettingsPage.Account -> onAccountClick() SettingsPage.SupportersContributors -> onSupportersContributorsClick() + SettingsPage.LicensesAttributions -> onLicensesAttributionsClick() SettingsPage.ContinueWatching -> onContinueWatchingClick() SettingsPage.Addons -> onAddonsClick() SettingsPage.Plugins -> { @@ -443,6 +448,7 @@ private fun MobileSettingsScreen( onIntegrationsClick = { onPageChange(SettingsPage.Integrations) }, onTraktClick = { onPageChange(SettingsPage.TraktAuthentication) }, onSupportersContributorsClick = onSupportersContributorsClick, + onLicensesAttributionsClick = onLicensesAttributionsClick, onCheckForUpdatesClick = onCheckForUpdatesClick, onDownloadsClick = onDownloadsClick, onAccountClick = onAccountClick, @@ -456,6 +462,9 @@ private fun MobileSettingsScreen( SettingsPage.SupportersContributors -> supportersContributorsContent( isTablet = false, ) + SettingsPage.LicensesAttributions -> licensesAttributionsContent( + isTablet = false, + ) SettingsPage.Playback -> playbackSettingsContent( isTablet = false, showLoadingOverlay = showLoadingOverlay, @@ -635,6 +644,7 @@ private fun TabletSettingsScreen( onSwitchProfile: (() -> Unit)? = null, onDownloadsClick: () -> Unit = {}, onSupportersContributorsClick: () -> Unit = {}, + onLicensesAttributionsClick: () -> Unit = {}, onCheckForUpdatesClick: (() -> Unit)? = null, onCollectionsClick: () -> Unit = {}, ) { @@ -792,6 +802,7 @@ private fun TabletSettingsScreen( onIntegrationsClick = { openInlinePage(SettingsPage.Integrations) }, onTraktClick = { openInlinePage(SettingsPage.TraktAuthentication) }, onSupportersContributorsClick = { openInlinePage(SettingsPage.SupportersContributors) }, + onLicensesAttributionsClick = { openInlinePage(SettingsPage.LicensesAttributions) }, onCheckForUpdatesClick = onCheckForUpdatesClick, onDownloadsClick = onDownloadsClick, onAccountClick = { openInlinePage(SettingsPage.Account) }, @@ -808,6 +819,9 @@ private fun TabletSettingsScreen( SettingsPage.SupportersContributors -> supportersContributorsContent( isTablet = true, ) + SettingsPage.LicensesAttributions -> licensesAttributionsContent( + isTablet = true, + ) SettingsPage.Playback -> playbackSettingsContent( isTablet = true, showLoadingOverlay = showLoadingOverlay, 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 index bdacf0de..1f3bafee 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt @@ -21,6 +21,7 @@ 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.Info import androidx.compose.material.icons.rounded.Link import androidx.compose.material.icons.rounded.Notifications import androidx.compose.material.icons.rounded.Palette @@ -94,6 +95,7 @@ internal fun settingsSearchEntries( 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 licensesPage = stringResource(Res.string.compose_settings_page_licenses_attributions) 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) @@ -248,6 +250,46 @@ internal fun settingsSearchEntries( category = aboutCategory, icon = Icons.Rounded.Favorite, ) + addPage( + page = SettingsPage.LicensesAttributions, + key = "licenses-attributions", + title = licensesPage, + description = stringResource(Res.string.about_licenses_attributions_subtitle), + category = aboutCategory, + icon = Icons.Rounded.Info, + ) + listOf( + PlaybackSearchRow("nuvio-license", stringResource(Res.string.settings_licenses_attributions_nuvio_title), stringResource(Res.string.settings_licenses_attributions_nuvio_license)), + PlaybackSearchRow("tmdb-attribution", stringResource(Res.string.settings_licenses_attributions_tmdb_title), stringResource(Res.string.settings_licenses_attributions_tmdb_body)), + PlaybackSearchRow("trakt-attribution", stringResource(Res.string.settings_licenses_attributions_trakt_title), stringResource(Res.string.settings_licenses_attributions_trakt_body)), + PlaybackSearchRow("mdblist-attribution", stringResource(Res.string.settings_licenses_attributions_mdblist_title), stringResource(Res.string.settings_licenses_attributions_mdblist_body)), + PlaybackSearchRow("introdb-attribution", stringResource(Res.string.settings_licenses_attributions_introdb_title), stringResource(Res.string.settings_licenses_attributions_introdb_body)), + PlaybackSearchRow("imdb-datasets", stringResource(Res.string.settings_licenses_attributions_imdb_title), stringResource(Res.string.settings_licenses_attributions_imdb_body)), + PlaybackSearchRow( + if (isIos) "mpvkit-license" else "exoplayer-license", + if (isIos) { + stringResource(Res.string.settings_licenses_attributions_mpvkit_title) + } else { + stringResource(Res.string.settings_licenses_attributions_exoplayer_title) + }, + if (isIos) { + stringResource(Res.string.settings_licenses_attributions_mpvkit_license) + } else { + stringResource(Res.string.settings_licenses_attributions_exoplayer_license) + }, + ), + ).forEach { row -> + addRow( + page = SettingsPage.LicensesAttributions, + key = row.key, + title = row.title, + description = row.description, + pageLabel = licensesPage, + section = stringResource(Res.string.compose_settings_root_about_section), + category = aboutCategory, + icon = Icons.Rounded.Info, + ) + } if (checkForUpdatesAvailable) { add( key = "check-updates", diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.ios.kt index dfbe18a4..6aa94391 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/IntegrationLogoPainter.ios.kt @@ -3,6 +3,7 @@ package com.nuvio.app.features.settings import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.painter.Painter import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.introdb_favicon import nuvio.composeapp.generated.resources.mdblist_logo import nuvio.composeapp.generated.resources.rating_tmdb import nuvio.composeapp.generated.resources.trakt_tv_favicon @@ -14,4 +15,5 @@ internal actual fun integrationLogoPainter(logo: IntegrationLogo): Painter = IntegrationLogo.Tmdb -> painterResource(Res.drawable.rating_tmdb) IntegrationLogo.Trakt -> painterResource(Res.drawable.trakt_tv_favicon) IntegrationLogo.MdbList -> painterResource(Res.drawable.mdblist_logo) + IntegrationLogo.IntroDb -> painterResource(Res.drawable.introdb_favicon) }