From 68ff6117790727dab67bdc831b42e687b4cf3267 Mon Sep 17 00:00:00 2001 From: David Ly Date: Sat, 24 Jun 2023 22:48:27 -0400 Subject: [PATCH] feat: show open source libraries licenses in settings (#256) --- .../collection/CollectionListScaffoldTest.kt | 3 +- .../collection/CollectionParameterizedTest.kt | 3 +- .../mbjc/ui/navigation/NavigationGraph.kt | 14 ++++- build.gradle.kts | 2 + .../java/ly/david/data/domain/Destination.kt | 3 +- .../data/room/release/ReleaseForListItem.kt | 1 - settings.gradle.kts | 4 ++ ui/common/src/main/res/values/strings.xml | 2 + ui/settings/build.gradle.kts | 3 ++ .../ly/david/ui/settings/SettingsScaffold.kt | 8 +++ .../ui/settings/components/ClickableItem.kt | 54 +++++++++++++++++++ .../ui/settings/components/ProfileCard.kt | 6 +-- .../ui/settings/licenses/LicensesScaffold.kt | 44 +++++++++++++++ .../ui/settings/PaparazziScreenshotTest.kt | 52 ++++++++++++++++++ 14 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 ui/settings/src/main/java/ly/david/ui/settings/components/ClickableItem.kt create mode 100644 ui/settings/src/main/java/ly/david/ui/settings/licenses/LicensesScaffold.kt create mode 100644 ui/settings/src/test/java/ly/david/ui/settings/PaparazziScreenshotTest.kt diff --git a/app/src/androidTest/java/ly/david/mbjc/ui/collection/CollectionListScaffoldTest.kt b/app/src/androidTest/java/ly/david/mbjc/ui/collection/CollectionListScaffoldTest.kt index 99a10f417..54284a0ed 100644 --- a/app/src/androidTest/java/ly/david/mbjc/ui/collection/CollectionListScaffoldTest.kt +++ b/app/src/androidTest/java/ly/david/mbjc/ui/collection/CollectionListScaffoldTest.kt @@ -15,6 +15,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.test.runTest import ly.david.data.network.MusicBrainzResource import ly.david.data.network.resourceUri @@ -52,7 +53,7 @@ internal class CollectionListScaffoldTest : MainActivityTest(), StringReferences } @Test - fun createCollections() = runTest { + fun createCollections() = runTest(timeout = 15.seconds) { composeTestRule .onNodeWithText(collections) .performClick() diff --git a/app/src/androidTest/java/ly/david/mbjc/ui/collection/CollectionParameterizedTest.kt b/app/src/androidTest/java/ly/david/mbjc/ui/collection/CollectionParameterizedTest.kt index 2205b7a43..140b866da 100644 --- a/app/src/androidTest/java/ly/david/mbjc/ui/collection/CollectionParameterizedTest.kt +++ b/app/src/androidTest/java/ly/david/mbjc/ui/collection/CollectionParameterizedTest.kt @@ -13,6 +13,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.test.runTest import ly.david.data.network.MusicBrainzResource import ly.david.data.network.collectableResources @@ -66,7 +67,7 @@ internal class CollectionParameterizedTest( } @Test - fun onlyLocalCollections() = runTest { + fun onlyLocalCollections() = runTest(timeout = 15.seconds) { composeTestRule .onNodeWithText(collections) .performClick() diff --git a/app/src/main/java/ly/david/mbjc/ui/navigation/NavigationGraph.kt b/app/src/main/java/ly/david/mbjc/ui/navigation/NavigationGraph.kt index 201717348..27ae9cf5e 100644 --- a/app/src/main/java/ly/david/mbjc/ui/navigation/NavigationGraph.kt +++ b/app/src/main/java/ly/david/mbjc/ui/navigation/NavigationGraph.kt @@ -38,6 +38,8 @@ import ly.david.mbjc.ui.series.SeriesScaffold import ly.david.mbjc.ui.work.WorkScaffold import ly.david.ui.common.R import ly.david.ui.history.HistoryScaffold +import ly.david.ui.settings.SettingsScaffold +import ly.david.ui.settings.licenses.LicensesScaffold private const val ID = "id" private const val TITLE = "title" @@ -355,6 +357,7 @@ internal fun NavigationGraph( val onSettingsClick: (Destination) -> Unit = { destination -> when (destination) { + Destination.SETTINGS_LICENSES, Destination.EXPERIMENTAL_SPOTIFY -> { navController.goTo(destination) } @@ -368,7 +371,7 @@ internal fun NavigationGraph( composable( Destination.SETTINGS.route ) { - ly.david.ui.settings.SettingsScaffold( + SettingsScaffold( modifier = modifier, onDestinationClick = { destination -> onSettingsClick(destination) @@ -382,6 +385,15 @@ internal fun NavigationGraph( ) } + composable( + Destination.SETTINGS_LICENSES.route + ) { + LicensesScaffold( + modifier = modifier, + onBack = navController::navigateUp + ) + } + composable( Destination.EXPERIMENTAL_SPOTIFY.route ) { diff --git a/build.gradle.kts b/build.gradle.kts index 1550488d6..f8a00a575 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,10 +17,12 @@ plugins { id("com.google.firebase.crashlytics") version "2.9.6" apply false id("dev.iurysouza.modulegraph") version "0.4.0" id("app.cash.paparazzi") version "1.2.0" apply false + id("com.mikepenz.aboutlibraries.plugin") version "10.8.0" apply true } allprojects { apply(plugin = "io.gitlab.arturbosch.detekt") + apply(plugin = "com.mikepenz.aboutlibraries.plugin") repositories { google() diff --git a/data/domain/src/main/java/ly/david/data/domain/Destination.kt b/data/domain/src/main/java/ly/david/data/domain/Destination.kt index c6a626242..8bc8aab90 100644 --- a/data/domain/src/main/java/ly/david/data/domain/Destination.kt +++ b/data/domain/src/main/java/ly/david/data/domain/Destination.kt @@ -21,6 +21,7 @@ private const val TOP_LEVEL_COLLECTIONS = "collections" private const val TOP_LEVEL_SETTINGS = "settings" private const val EXPERIMENTAL = "experimental" +private const val LICENSES = "licenses" /** * This divider should be the same used for dividing parameters such as {artistId} passed to navigation. @@ -54,7 +55,7 @@ enum class Destination(val route: String) { COLLECTIONS(TOP_LEVEL_COLLECTIONS), SETTINGS(TOP_LEVEL_SETTINGS), - + SETTINGS_LICENSES("$TOP_LEVEL_SETTINGS$DIVIDER$LICENSES"), EXPERIMENTAL_SPOTIFY("$TOP_LEVEL_SETTINGS$DIVIDER$EXPERIMENTAL$DIVIDER" + "SPOTIFY") } diff --git a/data/room/src/main/java/ly/david/data/room/release/ReleaseForListItem.kt b/data/room/src/main/java/ly/david/data/room/release/ReleaseForListItem.kt index 10db8acb3..6985271fa 100644 --- a/data/room/src/main/java/ly/david/data/room/release/ReleaseForListItem.kt +++ b/data/room/src/main/java/ly/david/data/room/release/ReleaseForListItem.kt @@ -13,7 +13,6 @@ data class ReleaseForListItem( @ColumnInfo("artist_credit_names") val artistCreditNames: String?, - // TODO: we've lost the ability to filter on this field @Relation( entity = ReleaseFormatTrackCount::class, parentColumn = "id", diff --git a/settings.gradle.kts b/settings.gradle.kts index bddb22ca3..c46f1c91e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ dependencyResolutionManagement { versionCatalogs { create("libs") { + version("aboutlibraries", "10.8.0") version("accompanist", "0.30.1") version("coil", "2.4.0") version("compose-compiler", "1.4.7") @@ -29,6 +30,9 @@ dependencyResolutionManagement { version("retrofit", "2.9.0") version("room", "2.5.2") + library("aboutlibraries-compose", "com.mikepenz", "aboutlibraries-compose").versionRef("aboutlibraries") + library("aboutlibraries-core", "com.mikepenz", "aboutlibraries-core").versionRef("aboutlibraries") + library("accompanist-swiperefresh", "com.google.accompanist", "accompanist-swiperefresh").versionRef("accompanist") library("accompanist-pager", "com.google.accompanist", "accompanist-pager").versionRef("accompanist") diff --git a/ui/common/src/main/res/values/strings.xml b/ui/common/src/main/res/values/strings.xml index 10fe1754a..857d54ce7 100644 --- a/ui/common/src/main/res/values/strings.xml +++ b/ui/common/src/main/res/values/strings.xml @@ -120,4 +120,6 @@ Name Enter name Add to collection + + Open source licenses diff --git a/ui/settings/build.gradle.kts b/ui/settings/build.gradle.kts index 2da7e89c7..3d8402c00 100644 --- a/ui/settings/build.gradle.kts +++ b/ui/settings/build.gradle.kts @@ -34,6 +34,9 @@ dependencies { implementation(projects.dataAndroid) implementation(projects.ui.common) + implementation(libs.aboutlibraries.compose) + implementation(libs.aboutlibraries.core) + implementation(libs.androidx.paging.compose) implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.paging.runtime) diff --git a/ui/settings/src/main/java/ly/david/ui/settings/SettingsScaffold.kt b/ui/settings/src/main/java/ly/david/ui/settings/SettingsScaffold.kt index e632dcf6f..6fc92c90f 100644 --- a/ui/settings/src/main/java/ly/david/ui/settings/SettingsScaffold.kt +++ b/ui/settings/src/main/java/ly/david/ui/settings/SettingsScaffold.kt @@ -18,6 +18,7 @@ import ly.david.ui.common.preview.DefaultPreviews import ly.david.ui.common.text.TextWithHeading import ly.david.ui.common.theme.PreviewTheme import ly.david.ui.common.topappbar.ScrollableTopAppBar +import ly.david.ui.settings.components.ClickableItem import ly.david.ui.settings.components.ProfileCard import ly.david.ui.settings.components.SettingSwitch import ly.david.ui.settings.components.SettingWithDialogChoices @@ -125,6 +126,13 @@ fun SettingsScreen( onCheckedChange = onSortReleaseGroupListItemsChange ) + ClickableItem( + text = stringResource(id = R.string.open_source_licenses), + onClick = { + onDestinationClick(Destination.SETTINGS_LICENSES) + } + ) + val versionKey = stringResource(id = R.string.app_version) val versionName = BuildConfig.VERSION_NAME val versionCode = BuildConfig.VERSION_CODE.toString() diff --git a/ui/settings/src/main/java/ly/david/ui/settings/components/ClickableItem.kt b/ui/settings/src/main/java/ly/david/ui/settings/components/ClickableItem.kt new file mode 100644 index 000000000..11cdb1fa1 --- /dev/null +++ b/ui/settings/src/main/java/ly/david/ui/settings/components/ClickableItem.kt @@ -0,0 +1,54 @@ +package ly.david.ui.settings.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ly.david.ui.common.preview.DefaultPreviews +import ly.david.ui.common.theme.PreviewTheme +import ly.david.ui.common.theme.TextStyles + +@Composable +internal fun ClickableItem( + text: String, + onClick: () -> Unit = {} +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = TextStyles.getCardBodyTextStyle() + ) + + Spacer(modifier = Modifier.weight(1f)) + + Icon(imageVector = Icons.Default.ChevronRight, contentDescription = null) + } +} + +@DefaultPreviews +@Composable +internal fun PreviewClickableItem() { + PreviewTheme { + Surface { + ClickableItem( + text = "Click me" + ) + } + } +} diff --git a/ui/settings/src/main/java/ly/david/ui/settings/components/ProfileCard.kt b/ui/settings/src/main/java/ly/david/ui/settings/components/ProfileCard.kt index 4d2ac3fe7..c478e4d91 100644 --- a/ui/settings/src/main/java/ly/david/ui/settings/components/ProfileCard.kt +++ b/ui/settings/src/main/java/ly/david/ui/settings/components/ProfileCard.kt @@ -82,7 +82,7 @@ internal fun ProfileCard( @DefaultPreviews @Composable -private fun LoggedOut() { +internal fun PreviewProfileCardLoggedOut() { PreviewTheme { Surface { ProfileCard( @@ -95,7 +95,7 @@ private fun LoggedOut() { @DefaultPreviews @Composable -private fun LoggedIn() { +internal fun PreviewProfileCardLoggedIn() { PreviewTheme { Surface { ProfileCard( @@ -108,7 +108,7 @@ private fun LoggedIn() { @DefaultPreviews @Composable -private fun LoggedInWaitingForUsername() { +internal fun PreviewProfileCardLoggedInWaitingForUsername() { PreviewTheme { Surface { ProfileCard( diff --git a/ui/settings/src/main/java/ly/david/ui/settings/licenses/LicensesScaffold.kt b/ui/settings/src/main/java/ly/david/ui/settings/licenses/LicensesScaffold.kt new file mode 100644 index 000000000..ea2f6be69 --- /dev/null +++ b/ui/settings/src/main/java/ly/david/ui/settings/licenses/LicensesScaffold.kt @@ -0,0 +1,44 @@ +package ly.david.ui.settings.licenses + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer +import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults +import ly.david.ui.common.R +import ly.david.ui.common.topappbar.ScrollableTopAppBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LicensesScaffold( + modifier: Modifier = Modifier, + onBack: () -> Unit = {}, +) { + Scaffold( + modifier = modifier, + topBar = { + ScrollableTopAppBar( + onBack = onBack, + title = stringResource(id = R.string.open_source_licenses), + ) + }, + ) { innerPadding -> + + LibrariesContainer( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + colors = LibraryDefaults.libraryColors( + backgroundColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + badgeBackgroundColor = MaterialTheme.colorScheme.primary, + badgeContentColor = MaterialTheme.colorScheme.onPrimary + ) + ) + } +} diff --git a/ui/settings/src/test/java/ly/david/ui/settings/PaparazziScreenshotTest.kt b/ui/settings/src/test/java/ly/david/ui/settings/PaparazziScreenshotTest.kt new file mode 100644 index 000000000..c872b7742 --- /dev/null +++ b/ui/settings/src/test/java/ly/david/ui/settings/PaparazziScreenshotTest.kt @@ -0,0 +1,52 @@ +package ly.david.ui.settings + +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams +import com.android.resources.NightMode +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import ly.david.ui.common.theme.PreviewTheme +import org.junit.Rule +import org.junit.runner.RunWith + +private const val PAPARAZZI_THEME = "android:Theme.Material.Light.NoActionBar" + +/** + * Common setup for running screenshot tests on light and dark mode. + */ +@RunWith(TestParameterInjector::class) +abstract class PaparazziScreenshotTest( + private val isFullScreen: Boolean = false +) { + + @TestParameter + private lateinit var nightMode: NightMode + + // Note we cannot override junit Rule + @get:Rule + val paparazzi: Paparazzi by lazy { + Paparazzi( + deviceConfig = DeviceConfig.PIXEL_5.copy(nightMode = nightMode), + theme = PAPARAZZI_THEME, + renderingMode = if (isFullScreen) { + SessionParams.RenderingMode.NORMAL + } else { + SessionParams.RenderingMode.SHRINK + }, + showSystemUi = false + ) + } + + protected fun snapshot(content: @Composable () -> Unit) { + paparazzi.snapshot { + PreviewTheme { + Surface { + content() + } + } + } + } +}