From 821adf25c4d6110e0d4e952bbd5b0b1d3f710e86 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Mon, 12 Aug 2024 12:47:51 +0100 Subject: [PATCH 1/4] Upgrade to new spotless version for all samples --- JetLagged/.editorconfig | 9 + JetLagged/app/build.gradle.kts | 31 +- .../java/com/example/jetlagged/AppTest.kt | 1 - .../com/example/jetlagged/HomeScreenCards.kt | 134 +- .../com/example/jetlagged/JetLaggedDrawer.kt | 171 +- .../com/example/jetlagged/JetLaggedScreen.kt | 33 +- .../com/example/jetlagged/MainActivity.kt | 1 - .../jetlagged/backgrounds/BubbleBackground.kt | 73 +- .../backgrounds/FadingCircleBackground.kt | 76 +- .../jetlagged/backgrounds/ShaderBackground.kt | 13 +- .../jetlagged/data/FakeHeartRateData.kt | 363 +-- .../example/jetlagged/data/FakeSleepData.kt | 1439 ++++++----- .../data/JetLaggedHomeScreenState.kt | 6 +- .../data/JetLaggedHomeScreenViewModel.kt | 1 - .../jetlagged/heartrate/HeartRateCard.kt | 26 +- .../jetlagged/heartrate/HeartRateGraph.kt | 114 +- .../jetlagged/sleep/JetLaggedHeader.kt | 15 +- .../jetlagged/sleep/JetLaggedHeaderTabs.kt | 29 +- .../jetlagged/sleep/JetLaggedTimeGraph.kt | 50 +- .../com/example/jetlagged/sleep/SleepBar.kt | 241 +- .../com/example/jetlagged/sleep/SleepData.kt | 24 +- .../com/example/jetlagged/sleep/TimeGraph.kt | 41 +- .../com/example/jetlagged/ui/theme/Theme.kt | 29 +- .../com/example/jetlagged/ui/theme/Type.kt | 102 +- .../jetlagged/ui/util/MultiDevicePreview.kt | 4 +- JetLagged/build.gradle.kts | 27 + JetLagged/buildscripts/init.gradle.kts | 72 - JetLagged/gradle/libs.versions.toml | 3 + JetNews/.editorconfig | 9 + JetNews/app/build.gradle.kts | 25 +- .../example/jetnews/data/AppContainerImpl.kt | 5 +- .../java/com/example/jetnews/data/Result.kt | 13 +- .../data/interests/InterestsRepository.kt | 11 +- .../interests/impl/FakeInterestsRepository.kt | 21 +- .../jetnews/data/posts/PostsRepository.kt | 1 - .../posts/impl/BlockingFakePostsRepository.kt | 7 +- .../data/posts/impl/FakePostsRepository.kt | 12 +- .../jetnews/data/posts/impl/PostsData.kt | 2287 +++++++++-------- .../com/example/jetnews/glance/ui/Divider.kt | 11 +- .../glance/ui/JetnewsGlanceAppWidget.kt | 49 +- .../com/example/jetnews/glance/ui/Post.kt | 142 +- .../{Theme.kt => JetnewsGlanceColorScheme.kt} | 18 +- .../{Type.kt => JetnewsGlanceTextStyles.kt} | 0 .../java/com/example/jetnews/model/Post.kt | 12 +- .../java/com/example/jetnews/ui/AppDrawer.kt | 24 +- .../java/com/example/jetnews/ui/JetnewsApp.kt | 11 +- .../com/example/jetnews/ui/JetnewsNavGraph.kt | 36 +- .../example/jetnews/ui/JetnewsNavigation.kt | 4 +- .../com/example/jetnews/ui/MainActivity.kt | 1 - .../jetnews/ui/article/ArticleScreen.kt | 72 +- .../example/jetnews/ui/article/PostContent.kt | 152 +- .../jetnews/ui/components/AppNavRail.kt | 10 +- .../ui/components/JetnewsSnackbarHost.kt | 15 +- .../com/example/jetnews/ui/home/HomeRoute.kt | 53 +- .../example/jetnews/ui/home/HomeScreens.kt | 280 +- .../example/jetnews/ui/home/HomeViewModel.kt | 73 +- .../example/jetnews/ui/home/PostCardTop.kt | 45 +- .../jetnews/ui/home/PostCardYourNetwork.kt | 62 +- .../com/example/jetnews/ui/home/PostCards.kt | 109 +- .../jetnews/ui/interests/InterestsRoute.kt | 11 +- .../jetnews/ui/interests/InterestsScreen.kt | 306 ++- .../ui/interests/InterestsViewModel.kt | 22 +- .../jetnews/ui/interests/SelectTopicButton.kt | 49 +- .../example/jetnews/ui/modifiers/KeyEvents.kt | 12 +- .../com/example/jetnews/ui/theme/Shape.kt | 11 +- .../com/example/jetnews/ui/theme/Theme.kt | 122 +- .../java/com/example/jetnews/ui/theme/Type.kt | 201 +- .../example/jetnews/ui/utils/JetnewsIcons.kt | 28 +- .../com/example/jetnews/utils/ErrorMessage.kt | 5 +- .../example/jetnews/utils/MapExtensions.kt | 15 +- .../jetnews/utils/MultipreviewAnnotations.kt | 12 +- .../com/example/jetnews/HomeScreenTests.kt | 29 +- .../java/com/example/jetnews/JetnewsTests.kt | 11 +- .../com/example/jetnews/TestAppContainer.kt | 5 +- .../java/com/example/jetnews/TestHelper.kt | 2 +- JetNews/build.gradle.kts | 25 + JetNews/buildscripts/init.gradle.kts | 72 - JetNews/gradle/libs.versions.toml | 3 + Jetcaster/.editorconfig | 10 + Jetcaster/build.gradle.kts | 26 + Jetcaster/buildscripts/init.gradle.kts | 72 - Jetcaster/core/data-testing/build.gradle.kts | 29 +- .../testing/repository/TestCategoryStore.kt | 37 +- .../testing/repository/TestEpisodeStore.kt | 47 +- .../testing/repository/TestPodcastStore.kt | 57 +- Jetcaster/core/data/build.gradle.kts | 27 +- .../example/jetcaster/core/data/Dispatcher.kt | 4 +- .../data/database/DateTimeTypeConverters.kt | 24 +- .../core/data/database/JetcasterDatabase.kt | 9 +- .../core/data/database/dao/CategoriesDao.kt | 6 +- .../core/data/database/dao/EpisodesDao.kt | 17 +- .../database/dao/PodcastFollowedEntryDao.kt | 4 +- .../core/data/database/dao/PodcastsDao.kt | 29 +- .../core/data/database/model/Category.kt | 6 +- .../core/data/database/model/Episode.kt | 10 +- .../data/database/model/EpisodeToPodcast.kt | 18 +- .../core/data/database/model/Podcast.kt | 6 +- .../database/model/PodcastCategoryEntry.kt | 12 +- .../database/model/PodcastFollowedEntry.kt | 10 +- .../database/model/PodcastWithExtraInfo.kt | 19 +- .../jetcaster/core/data/di/DataDiModule.kt | 89 +- .../jetcaster/core/data/network/Feeds.kt | 43 +- .../core/data/network/OkHttpExtensions.kt | 53 +- .../core/data/network/PodcastFetcher.kt | 145 +- .../core/data/repository/CategoryStore.kt | 47 +- .../core/data/repository/EpisodeStore.kt | 28 +- .../core/data/repository/PodcastStore.kt | 55 +- .../data/repository/PodcastsRepository.kt | 72 +- .../com/example/jetcaster/core/util/Flows.kt | 13 +- Jetcaster/core/designsystem/build.gradle.kts | 99 +- .../component/HtmlTextContainer.kt | 13 +- .../designsystem/component/ImageBackground.kt | 32 +- .../designsystem/component/PodcastImage.kt | 37 +- .../component/thumbnailPlaceholder.kt | 13 +- .../jetcaster/designsystem/theme/Shape.kt | 11 +- .../jetcaster/designsystem/theme/Type.kt | 218 +- .../designsystem/theme/Typography.kt | 13 +- .../core/domain-testing/build.gradle.kts | 29 +- .../core/domain/testing/PreviewData.kt | 93 +- Jetcaster/core/domain/build.gradle.kts | 29 +- .../jetcaster/core/di/DomainDiModule.kt | 4 +- .../domain/FilterableCategoriesUseCase.kt | 44 +- .../GetLatestFollowedEpisodesUseCase.kt | 33 +- .../domain/PodcastCategoryFilterUseCase.kt | 50 +- .../jetcaster/core/model/CategoryInfo.kt | 4 +- .../core/model/FilterableCategoriesModel.kt | 2 +- .../jetcaster/core/model/LibraryInfo.kt | 2 +- .../core/model/PodcastCategoryFilterResult.kt | 2 +- .../jetcaster/core/player/EpisodePlayer.kt | 8 +- .../core/player/MockEpisodePlayer.kt | 49 +- .../core/player/model/PlayerEpisode.kt | 2 +- .../domain/FilterableCategoriesUseCaseTest.kt | 53 +- .../GetLatestFollowedEpisodesUseCaseTest.kt | 113 +- .../PodcastCategoryFilterUseCaseTest.kt | 203 +- .../domain/player/MockEpisodePlayerTest.kt | 285 +- Jetcaster/glancewidget/build.gradle.kts | 27 +- .../example/jetcaster/glancewidget/Colors.kt | 150 +- .../glancewidget/JetcasterAppWidget.kt | 118 +- Jetcaster/gradle/libs.versions.toml | 3 + Jetcaster/mobile/build.gradle.kts | 22 +- .../example/jetcaster/JetcasterApplication.kt | 5 +- .../com/example/jetcaster/ui/JetcasterApp.kt | 10 +- .../example/jetcaster/ui/JetcasterAppState.kt | 24 +- .../com/example/jetcaster/ui/MainActivity.kt | 2 +- .../com/example/jetcaster/ui/home/Home.kt | 284 +- .../jetcaster/ui/home/HomeViewModel.kt | 244 +- .../ui/home/category/PodcastCategory.kt | 66 +- .../jetcaster/ui/home/discover/Discover.kt | 60 +- .../jetcaster/ui/home/library/Library.kt | 28 +- .../jetcaster/ui/player/PlayerScreen.kt | 354 +-- .../jetcaster/ui/player/PlayerViewModel.kt | 119 +- .../ui/podcast/PodcastDetailsScreen.kt | 141 +- .../ui/podcast/PodcastDetailsViewModel.kt | 68 +- .../jetcaster/ui/shared/EpisodeListItem.kt | 94 +- .../example/jetcaster/ui/shared/Loading.kt | 4 +- .../com/example/jetcaster/ui/theme/Theme.kt | 483 ++-- .../com/example/jetcaster/util/Buttons.kt | 68 +- .../example/jetcaster/util/GradientScrim.kt | 58 +- .../jetcaster/util/LazyVerticalGrid.kt | 4 +- .../example/jetcaster/util/PluralResources.kt | 11 +- .../com/example/jetcaster/util/ViewModel.kt | 11 +- .../example/jetcaster/util/WindowSizeClass.kt | 5 +- Jetcaster/tv/build.gradle.kts | 21 +- .../com/example/jetcaster/tv/MainActivity.kt | 9 +- .../jetcaster/tv/model/CategoryInfoList.kt | 13 +- .../jetcaster/tv/model/CategorySelection.kt | 7 +- .../example/jetcaster/tv/model/EpisodeList.kt | 4 +- .../example/jetcaster/tv/model/PodcastList.kt | 2 +- .../example/jetcaster/tv/ui/JetcasterApp.kt | 68 +- .../jetcaster/tv/ui/JetcasterAppState.kt | 23 +- .../jetcaster/tv/ui/component/Background.kt | 22 +- .../jetcaster/tv/ui/component/Button.kt | 48 +- .../jetcaster/tv/ui/component/Catalog.kt | 58 +- .../jetcaster/tv/ui/component/EpisodeCard.kt | 17 +- .../tv/ui/component/EpisodeDateAndDuration.kt | 13 +- .../tv/ui/component/EpisodeDetails.kt | 12 +- .../jetcaster/tv/ui/component/EpisodeRow.kt | 47 +- .../jetcaster/tv/ui/component/ErrorState.kt | 6 +- .../jetcaster/tv/ui/component/Loading.kt | 142 +- .../tv/ui/component/NotAvailableFeature.kt | 2 +- .../jetcaster/tv/ui/component/PodcastCard.kt | 4 +- .../jetcaster/tv/ui/component/Seekbar.kt | 16 +- .../jetcaster/tv/ui/component/Thumbnail.kt | 77 +- .../jetcaster/tv/ui/component/TwoColumn.kt | 4 +- .../tv/ui/discover/DiscoverScreen.kt | 48 +- .../tv/ui/discover/DiscoverScreenViewModel.kt | 161 +- .../jetcaster/tv/ui/episode/EpisodeScreen.kt | 41 +- .../tv/ui/episode/EpisodeScreenViewModel.kt | 93 +- .../jetcaster/tv/ui/library/LibraryScreen.kt | 39 +- .../tv/ui/library/LibraryScreenViewModel.kt | 94 +- .../jetcaster/tv/ui/player/PlayerScreen.kt | 150 +- .../tv/ui/player/PlayerScreenViewModel.kt | 84 +- .../tv/ui/podcast/PodcastDetailsScreen.kt | 182 +- .../podcast/PodcastDetailsScreenViewModel.kt | 136 +- .../jetcaster/tv/ui/search/SearchScreen.kt | 91 +- .../tv/ui/search/SearchScreenViewModel.kt | 175 +- .../tv/ui/settings/SettingsScreen.kt | 4 +- .../example/jetcaster/tv/ui/theme/Color.kt | 122 +- .../example/jetcaster/tv/ui/theme/Space.kt | 46 +- .../example/jetcaster/tv/ui/theme/Theme.kt | 13 +- .../com/example/jetcaster/tv/ui/theme/Type.kt | 198 +- .../jetcaster/JetcasterWearApplication.kt | 11 +- .../java/com/example/jetcaster/WearApp.kt | 13 +- .../java/com/example/jetcaster/theme/Color.kt | 21 +- .../java/com/example/jetcaster/theme/Type.kt | 16 +- .../example/jetcaster/theme/WearAppTheme.kt | 6 +- .../jetcaster/ui/JetcasterNavController.kt | 25 +- .../jetcaster/ui/components/MediaContent.kt | 29 +- .../ui/components/SettingsButtons.kt | 23 +- .../jetcaster/ui/episode/EpisodeScreen.kt | 107 +- .../jetcaster/ui/episode/EpisodeViewModel.kt | 83 +- .../LatestEpisodeViewModel.kt | 64 +- .../LatestEpisodesScreen.kt | 81 +- .../jetcaster/ui/library/LibraryScreen.kt | 49 +- .../jetcaster/ui/library/LibraryViewModel.kt | 130 +- .../jetcaster/ui/player/PlayerScreen.kt | 30 +- .../jetcaster/ui/player/PlayerViewModel.kt | 112 +- .../ui/podcast/PodcastDetailsScreen.kt | 62 +- .../ui/podcast/PodcastDetailsViewModel.kt | 112 +- .../jetcaster/ui/podcasts/PodcastsScreen.kt | 101 +- .../ui/podcasts/PodcastsViewModel.kt | 50 +- .../example/jetcaster/ui/queue/QueueScreen.kt | 122 +- .../jetcaster/ui/queue/QueueViewModel.kt | 59 +- Jetchat/.editorconfig | 9 + Jetchat/app/build.gradle.kts | 25 +- .../compose/jetchat/ConversationTest.kt | 48 +- .../example/compose/jetchat/NavigationTest.kt | 28 +- .../example/compose/jetchat/UserInputTest.kt | 21 +- .../example/compose/jetchat/MainViewModel.kt | 1 - .../example/compose/jetchat/NavActivity.kt | 8 +- .../com/example/compose/jetchat/UiExtras.kt | 4 +- .../jetchat/components/AnimatingFabContent.kt | 24 +- .../components/BaseLineHeightModifier.kt | 9 +- .../jetchat/components/JetchatAppBar.kt | 13 +- .../jetchat/components/JetchatDrawer.kt | 113 +- .../compose/jetchat/components/JetchatIcon.kt | 21 +- .../jetchat/components/JetchatScaffold.kt | 6 +- .../jetchat/conversation/Conversation.kt | 173 +- .../conversation/ConversationFragment.kt | 42 +- .../conversation/ConversationUiState.kt | 4 +- .../jetchat/conversation/JumpToBottom.kt | 22 +- .../jetchat/conversation/MessageFormatter.kt | 156 +- .../jetchat/conversation/RecordButton.kt | 162 +- .../compose/jetchat/conversation/UserInput.kt | 597 +++-- .../example/compose/jetchat/data/FakeData.kt | 132 +- .../compose/jetchat/profile/Previews.kt | 1 + .../compose/jetchat/profile/Profile.kt | 142 +- .../jetchat/profile/ProfileFragment.kt | 22 +- .../jetchat/profile/ProfileViewModel.kt | 18 +- .../example/compose/jetchat/theme/Themes.kt | 137 +- .../compose/jetchat/theme/Typography.kt | 273 +- Jetchat/build.gradle.kts | 25 + Jetchat/buildscripts/init.gradle.kts | 72 - Jetchat/gradle/libs.versions.toml | 3 + Jetsnack/.editorconfig | 9 + Jetsnack/app/build.gradle.kts | 24 +- .../java/com/example/jetsnack/AppTest.kt | 1 - .../java/com/example/jetsnack/model/Filter.kt | 69 +- .../java/com/example/jetsnack/model/Search.kt | 170 +- .../java/com/example/jetsnack/model/Snack.kt | 385 +-- .../example/jetsnack/model/SnackCollection.kt | 140 +- .../example/jetsnack/model/SnackbarManager.kt | 21 +- .../com/example/jetsnack/ui/JetsnackApp.kt | 83 +- .../jetsnack/ui/SnackSharedElementKey.kt | 4 +- .../example/jetsnack/ui/components/Button.kt | 43 +- .../example/jetsnack/ui/components/Card.kt | 4 +- .../example/jetsnack/ui/components/Divider.kt | 4 +- .../example/jetsnack/ui/components/Filters.kt | 74 +- .../jetsnack/ui/components/Gradient.kt | 37 +- .../ui/components/GradientTintedIconButton.kt | 78 +- .../example/jetsnack/ui/components/Grid.kt | 22 +- .../ui/components/QuantitySelector.kt | 20 +- .../jetsnack/ui/components/Scaffold.kt | 15 +- .../jetsnack/ui/components/Snackbar.kt | 4 +- .../example/jetsnack/ui/components/Snacks.kt | 424 +-- .../example/jetsnack/ui/components/Surface.kt | 28 +- .../jetsnack/ui/home/DestinationBar.kt | 36 +- .../java/com/example/jetsnack/ui/home/Feed.kt | 20 +- .../example/jetsnack/ui/home/FilterScreen.kt | 159 +- .../java/com/example/jetsnack/ui/home/Home.kt | 163 +- .../com/example/jetsnack/ui/home/Profile.kt | 19 +- .../com/example/jetsnack/ui/home/cart/Cart.kt | 304 ++- .../jetsnack/ui/home/cart/CartViewModel.kt | 33 +- .../jetsnack/ui/home/cart/SwipeDismissItem.kt | 8 +- .../jetsnack/ui/home/search/Categories.kt | 69 +- .../jetsnack/ui/home/search/Results.kt | 136 +- .../example/jetsnack/ui/home/search/Search.kt | 106 +- .../jetsnack/ui/home/search/Suggestions.kt | 30 +- .../ui/navigation/JetsnackNavController.kt | 24 +- .../jetsnack/ui/snackdetail/SnackDetail.kt | 454 ++-- .../com/example/jetsnack/ui/theme/Shape.kt | 11 +- .../com/example/jetsnack/ui/theme/Theme.kt | 119 +- .../com/example/jetsnack/ui/theme/Type.kt | 212 +- .../com/example/jetsnack/ui/utils/Currency.kt | 7 +- Jetsnack/build.gradle.kts | 26 + Jetsnack/gradle/libs.versions.toml | 3 + Reply/.editorconfig | 9 + Reply/app/build.gradle.kts | 25 +- .../java/com/example/reply/data/Account.kt | 2 +- .../example/reply/data/AccountsRepository.kt | 2 + .../reply/data/AccountsRepositoryImpl.kt | 22 +- .../main/java/com/example/reply/data/Email.kt | 2 +- .../com/example/reply/data/EmailAttachment.kt | 2 +- .../example/reply/data/EmailsRepository.kt | 3 + .../reply/data/EmailsRepositoryImpl.kt | 28 +- .../com/example/reply/data/MailboxType.kt | 6 +- .../data/local/LocalAccountsDataProvider.kt | 247 +- .../data/local/LocalEmailsDataProvider.kt | 522 ++-- .../com/example/reply/ui/EmptyComingSoon.kt | 10 +- .../java/com/example/reply/ui/MainActivity.kt | 3 +- .../java/com/example/reply/ui/ReplyApp.kt | 61 +- .../example/reply/ui/ReplyHomeViewModel.kt | 62 +- .../com/example/reply/ui/ReplyListContent.kt | 52 +- .../reply/ui/components/ReplyAppBars.kt | 100 +- .../reply/ui/components/ReplyEmailListItem.kt | 85 +- .../ui/components/ReplyEmailThreadItem.kt | 63 +- .../reply/ui/components/ReplyProfileImage.kt | 9 +- .../ui/navigation/ReplyNavigationActions.kt | 59 +- .../navigation/ReplyNavigationComponents.kt | 268 +- .../java/com/example/reply/ui/theme/Shapes.kt | 15 +- .../java/com/example/reply/ui/theme/Theme.kt | 513 ++-- .../java/com/example/reply/ui/theme/Type.kt | 159 +- .../reply/ui/utils/WindowStateUtils.kt | 14 +- Reply/build.gradle.kts | 26 +- Reply/buildscripts/init.gradle.kts | 72 - Reply/gradle/libs.versions.toml | 3 + scripts/libs.versions.toml | 3 + 327 files changed, 13025 insertions(+), 11496 deletions(-) create mode 100644 JetLagged/.editorconfig delete mode 100644 JetLagged/buildscripts/init.gradle.kts create mode 100644 JetNews/.editorconfig rename JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/{Theme.kt => JetnewsGlanceColorScheme.kt} (75%) rename JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/{Type.kt => JetnewsGlanceTextStyles.kt} (100%) delete mode 100644 JetNews/buildscripts/init.gradle.kts create mode 100644 Jetcaster/.editorconfig delete mode 100644 Jetcaster/buildscripts/init.gradle.kts rename Jetcaster/wear/src/main/java/com/example/jetcaster/ui/{latest_episodes => latestepisodes}/LatestEpisodeViewModel.kt (52%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/ui/{latest_episodes => latestepisodes}/LatestEpisodesScreen.kt (82%) create mode 100644 Jetchat/.editorconfig delete mode 100644 Jetchat/buildscripts/init.gradle.kts create mode 100644 Jetsnack/.editorconfig create mode 100644 Reply/.editorconfig delete mode 100644 Reply/buildscripts/init.gradle.kts diff --git a/JetLagged/.editorconfig b/JetLagged/.editorconfig new file mode 100644 index 0000000000..43f0af8237 --- /dev/null +++ b/JetLagged/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{kt,kts}] +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL + +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ktlint_standard_property-naming = disabled +ktlint_function_naming_ignore_when_annotated_with = Composable diff --git a/JetLagged/app/build.gradle.kts b/JetLagged/app/build.gradle.kts index 4bd6a319e1..901d125c9a 100644 --- a/JetLagged/app/build.gradle.kts +++ b/JetLagged/app/build.gradle.kts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,6 +14,7 @@ * limitations under the License. */ + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -22,13 +23,22 @@ plugins { } android { - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() namespace = "com.example.jetlagged" defaultConfig { applicationId = "com.example.jetlagged" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() + targetSdk = + libs.versions.targetSdk + .get() + .toInt() versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -49,22 +59,25 @@ android { buildTypes { getByName("debug") { - } getByName("release") { isMinifyEnabled = true signingConfig = signingConfigs.getByName("release") - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) } create("benchmark") { initWith(getByName("release")) signingConfig = signingConfigs.getByName("release") matchingFallbacks.add("release") - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-benchmark-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-benchmark-rules.pro", + ) isDebuggable = false } } diff --git a/JetLagged/app/src/androidTest/java/com/example/jetlagged/AppTest.kt b/JetLagged/app/src/androidTest/java/com/example/jetlagged/AppTest.kt index 8053bf5483..6bb04a5832 100644 --- a/JetLagged/app/src/androidTest/java/com/example/jetlagged/AppTest.kt +++ b/JetLagged/app/src/androidTest/java/com/example/jetlagged/AppTest.kt @@ -24,7 +24,6 @@ import org.junit.Rule import org.junit.Test class AppTest { - @get:Rule val composeTestRule = createComposeRule() diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/HomeScreenCards.kt b/JetLagged/app/src/main/java/com/example/jetlagged/HomeScreenCards.kt index 8aa993c61a..a4e2afa990 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/HomeScreenCards.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/HomeScreenCards.kt @@ -73,15 +73,16 @@ import com.example.jetlagged.ui.theme.SmallHeadingStyle fun BasicInformationalCard( modifier: Modifier = Modifier, borderColor: Color, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val shape = RoundedCornerShape(24.dp) Card( shape = shape, colors = CardDefaults.cardColors(containerColor = Color.White), - modifier = modifier - .padding(8.dp), - border = BorderStroke(2.dp, borderColor) + modifier = + modifier + .padding(8.dp), + border = BorderStroke(2.dp, borderColor), ) { Box { content() @@ -95,42 +96,48 @@ fun TwoLineInfoCard( firstLineText: String, secondLineText: String, icon: ImageVector, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { BasicInformationalCard( borderColor = borderColor, - modifier = modifier.size(200.dp) + modifier = modifier.size(200.dp), ) { BubbleBackground( modifier = Modifier.fillMaxSize(), - numberBubbles = 3, bubbleColor = borderColor.copy(0.25f) + numberBubbles = 3, + bubbleColor = borderColor.copy(0.25f), ) BoxWithConstraints( - modifier = Modifier - .padding(16.dp) - .fillMaxSize(), + modifier = + Modifier + .padding(16.dp) + .fillMaxSize(), ) { if (maxWidth > 400.dp) { Row( - modifier = Modifier - .wrapContentSize() - .align(CenterStart) + modifier = + Modifier + .wrapContentSize() + .align(CenterStart), ) { Icon( - icon, contentDescription = null, - modifier = Modifier - .size(50.dp) - .align(CenterVertically) + icon, + contentDescription = null, + modifier = + Modifier + .size(50.dp) + .align(CenterVertically), ) Spacer(modifier = Modifier.width(16.dp)) Column( - modifier = Modifier - .align(CenterVertically) - .wrapContentSize() + modifier = + Modifier + .align(CenterVertically) + .wrapContentSize(), ) { Text( firstLineText, - style = SmallHeadingStyle + style = SmallHeadingStyle, ) Text( secondLineText, @@ -140,27 +147,30 @@ fun TwoLineInfoCard( } } else { Column( - modifier = Modifier - .wrapContentSize() - .align(Center) + modifier = + Modifier + .wrapContentSize() + .align(Center), ) { Icon( - icon, contentDescription = null, - modifier = Modifier - .size(50.dp) - .align(CenterHorizontally) + icon, + contentDescription = null, + modifier = + Modifier + .size(50.dp) + .align(CenterHorizontally), ) Spacer(modifier = Modifier.height(16.dp)) Column(modifier = Modifier.align(CenterHorizontally)) { Text( firstLineText, style = SmallHeadingStyle, - modifier = Modifier.align(CenterHorizontally) + modifier = Modifier.align(CenterHorizontally), ) Text( secondLineText, style = HeadingStyle, - modifier = Modifier.align(CenterHorizontally) + modifier = Modifier.align(CenterHorizontally), ) } } @@ -178,9 +188,10 @@ fun AverageTimeInBedCard(modifier: Modifier = Modifier) { firstLineText = stringResource(R.string.ave_time_in_bed_heading), secondLineText = "8h42min", icon = Icons.Default.Watch, - modifier = modifier - .wrapContentWidth() - .heightIn(min = 156.dp) + modifier = + modifier + .wrapContentWidth() + .heightIn(min = 156.dp), ) } @@ -193,9 +204,10 @@ fun AverageTimeAsleepCard(modifier: Modifier = Modifier) { firstLineText = stringResource(R.string.ave_time_sleep_heading), secondLineText = "7h42min", icon = Icons.Default.SingleBed, - modifier = modifier - .wrapContentWidth() - .heightIn(min = 156.dp) + modifier = + modifier + .wrapContentWidth() + .heightIn(min = 156.dp), ) } @@ -204,40 +216,42 @@ fun AverageTimeAsleepCard(modifier: Modifier = Modifier) { @Composable fun WellnessCard( modifier: Modifier = Modifier, - wellnessData: WellnessData = WellnessData(0, 0, 0) + wellnessData: WellnessData = WellnessData(0, 0, 0), ) { BasicInformationalCard( borderColor = LightBlue, - modifier = modifier - .widthIn(max = 400.dp) - .heightIn(min = 200.dp) + modifier = + modifier + .widthIn(max = 400.dp) + .heightIn(min = 200.dp), ) { FadingCircleBackground(36.dp, LightBlue.copy(0.25f)) Column( horizontalAlignment = CenterHorizontally, - modifier = Modifier - .fillMaxWidth() + modifier = + Modifier + .fillMaxWidth(), ) { HomeScreenCardHeading(text = stringResource(R.string.wellness_heading)) FlowRow( horizontalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxHeight() + modifier = Modifier.fillMaxHeight(), ) { WellnessBubble( titleText = stringResource(R.string.snoring_heading), countText = wellnessData.snoring.toString(), - metric = "min" + metric = "min", ) WellnessBubble( titleText = stringResource(R.string.coughing_heading), countText = wellnessData.coughing.toString(), - metric = "times" + metric = "times", ) WellnessBubble( titleText = stringResource(R.string.respiration_heading), countText = wellnessData.respiration.toString(), - metric = "rpm" + metric = "rpm", ) } } @@ -249,18 +263,19 @@ fun WellnessBubble( titleText: String, countText: String, metric: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Column( - modifier = modifier - .padding(4.dp) - .sizeIn(maxHeight = 100.dp) - .aspectRatio(1f) - .drawBehind { - drawCircle(LightBlue) - }, + modifier = + modifier + .padding(4.dp) + .sizeIn(maxHeight = 100.dp) + .aspectRatio(1f) + .drawBehind { + drawCircle(LightBlue) + }, verticalArrangement = Arrangement.Center, - horizontalAlignment = CenterHorizontally + horizontalAlignment = CenterHorizontally, ) { Text(titleText, fontSize = 12.sp) Text(countText, fontSize = 36.sp) @@ -272,10 +287,11 @@ fun WellnessBubble( fun HomeScreenCardHeading(text: String) { Text( text, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), textAlign = TextAlign.Center, - style = HeadingStyle + style = HeadingStyle, ) } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/JetLaggedDrawer.kt b/JetLagged/app/src/main/java/com/example/jetlagged/JetLaggedDrawer.kt index bc69d7403d..151ab826d2 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/JetLaggedDrawer.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/JetLaggedDrawer.kt @@ -57,9 +57,8 @@ import kotlinx.coroutines.launch @Composable fun HomeScreenDrawer(windowSizeClass: WindowSizeClass) { - Surface( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { var drawerState by remember { mutableStateOf(DrawerState.Closed) @@ -68,13 +67,15 @@ fun HomeScreenDrawer(windowSizeClass: WindowSizeClass) { mutableStateOf(Screen.Home) } - val translationX = remember { - Animatable(0f) - } + val translationX = + remember { + Animatable(0f) + } - val drawerWidth = with(LocalDensity.current) { - DrawerWidth.toPx() - } + val drawerWidth = + with(LocalDensity.current) { + DrawerWidth.toPx() + } translationX.updateBounds(0f, drawerWidth) val coroutineScope = rememberCoroutineScope() @@ -86,11 +87,12 @@ fun HomeScreenDrawer(windowSizeClass: WindowSizeClass) { } else { translationX.animateTo(drawerWidth) } - drawerState = if (drawerState == DrawerState.Open) { - DrawerState.Closed - } else { - DrawerState.Open - } + drawerState = + if (drawerState == DrawerState.Open) { + DrawerState.Closed + } else { + DrawerState.Open + } } } @@ -98,72 +100,80 @@ fun HomeScreenDrawer(windowSizeClass: WindowSizeClass) { selectedScreen = screenState, onScreenSelected = { screen -> screenState = screen - } + }, ) - val draggableState = rememberDraggableState(onDelta = { dragAmount -> - coroutineScope.launch { - translationX.snapTo(translationX.value + dragAmount) - } - }) + val draggableState = + rememberDraggableState(onDelta = { dragAmount -> + coroutineScope.launch { + translationX.snapTo(translationX.value + dragAmount) + } + }) val decay = rememberSplineBasedDecay() ScreenContents( windowWidthSizeClass = windowSizeClass.widthSizeClass, selectedScreen = screenState, onDrawerClicked = ::toggleDrawerState, - modifier = Modifier - .graphicsLayer { - this.translationX = translationX.value - val scale = lerp(1f, 0.8f, translationX.value / drawerWidth) - this.scaleX = scale - this.scaleY = scale - val roundedCorners = lerp(0f, 32.dp.toPx(), translationX.value / drawerWidth) - this.shape = RoundedCornerShape(roundedCorners) - this.clip = true - this.shadowElevation = 32f - } - // This example is showing how to use draggable with custom logic on stop to snap to the edges - // You can also use `anchoredDraggable()` to set up anchors and not need to worry about more calculations. - .draggable( - draggableState, Orientation.Horizontal, - onDragStopped = { velocity -> - val targetOffsetX = decay.calculateTargetValue( - translationX.value, - velocity - ) - coroutineScope.launch { - val actualTargetX = if (targetOffsetX > drawerWidth * 0.5) { - drawerWidth - } else { - 0f - } - // checking if the difference between the target and actual is + or - - val targetDifference = (actualTargetX - targetOffsetX) - val canReachTargetWithDecay = - ( - targetOffsetX > actualTargetX && velocity > 0f && - targetDifference > 0f - ) || + modifier = + Modifier + .graphicsLayer { + this.translationX = translationX.value + val scale = lerp(1f, 0.8f, translationX.value / drawerWidth) + this.scaleX = scale + this.scaleY = scale + val roundedCorners = lerp(0f, 32.dp.toPx(), translationX.value / drawerWidth) + this.shape = RoundedCornerShape(roundedCorners) + this.clip = true + this.shadowElevation = 32f + } + // This example is showing how to use draggable with custom logic on stop to snap to the edges + // You can also use `anchoredDraggable()` to set up anchors and not need to worry about more calculations. + .draggable( + draggableState, + Orientation.Horizontal, + onDragStopped = { velocity -> + val targetOffsetX = + decay.calculateTargetValue( + translationX.value, + velocity, + ) + coroutineScope.launch { + val actualTargetX = + if (targetOffsetX > drawerWidth * 0.5) { + drawerWidth + } else { + 0f + } + // checking if the difference between the target and actual is + or - + val targetDifference = (actualTargetX - targetOffsetX) + val canReachTargetWithDecay = ( - targetOffsetX < actualTargetX && velocity < 0 && - targetDifference < 0f + targetOffsetX > actualTargetX && + velocity > 0f && + targetDifference > 0f + ) || + ( + targetOffsetX < actualTargetX && + velocity < 0 && + targetDifference < 0f ) - if (canReachTargetWithDecay) { - translationX.animateDecay( - initialVelocity = velocity, - animationSpec = decay - ) - } else { - translationX.animateTo(actualTargetX, initialVelocity = velocity) - } - drawerState = if (actualTargetX == drawerWidth) { - DrawerState.Open - } else { - DrawerState.Closed + if (canReachTargetWithDecay) { + translationX.animateDecay( + initialVelocity = velocity, + animationSpec = decay, + ) + } else { + translationX.animateTo(actualTargetX, initialVelocity = velocity) + } + drawerState = + if (actualTargetX == drawerWidth) { + DrawerState.Open + } else { + DrawerState.Closed + } } - } - } - ) + }, + ), ) } } @@ -173,7 +183,7 @@ private fun ScreenContents( windowWidthSizeClass: WindowWidthSizeClass, selectedScreen: Screen, onDrawerClicked: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Box(modifier) { when (selectedScreen) { @@ -181,24 +191,24 @@ private fun ScreenContents( JetLaggedScreen( windowSizeClass = windowWidthSizeClass, modifier = Modifier, - onDrawerClicked = onDrawerClicked + onDrawerClicked = onDrawerClicked, ) Screen.SleepDetails -> Surface( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { } Screen.Leaderboard -> Surface( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { } Screen.Settings -> Surface( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { } } @@ -207,20 +217,20 @@ private fun ScreenContents( private enum class DrawerState { Open, - Closed + Closed, } @Composable private fun HomeScreenDrawerContents( selectedScreen: Screen, onScreenSelected: (Screen) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Column( modifier .fillMaxSize() .padding(16.dp), - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { Screen.values().forEach { NavigationDrawerItem( @@ -231,7 +241,7 @@ private fun HomeScreenDrawerContents( Icon(imageVector = it.icon, contentDescription = it.text) }, colors = - NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.White), + NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.White), selected = selectedScreen == it, onClick = { onScreenSelected(it) @@ -243,7 +253,10 @@ private fun HomeScreenDrawerContents( private val DrawerWidth = 300.dp -private enum class Screen(val text: String, val icon: ImageVector) { +private enum class Screen( + val text: String, + val icon: ImageVector, +) { Home("Home", Icons.Default.Home), SleepDetails("Sleep", Icons.Default.Bedtime), Leaderboard("Leaderboard", Icons.Default.Leaderboard), diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/JetLaggedScreen.kt b/JetLagged/app/src/main/java/com/example/jetlagged/JetLaggedScreen.kt index cadc0d9a46..ff08bf7532 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/JetLaggedScreen.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/JetLaggedScreen.kt @@ -49,19 +49,20 @@ fun JetLaggedScreen( modifier: Modifier = Modifier, windowSizeClass: WindowWidthSizeClass = WindowWidthSizeClass.Compact, viewModel: JetLaggedHomeScreenViewModel = viewModel(), - onDrawerClicked: () -> Unit = {} + onDrawerClicked: () -> Unit = {}, ) { Column( - modifier = modifier - .background(Color.White) - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .background(Color.White) + modifier = + modifier + .background(Color.White) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .background(Color.White), ) { Column(modifier = Modifier.yellowBackground()) { JetLaggedHeader( modifier = Modifier.fillMaxWidth(), - onDrawerClicked = onDrawerClicked + onDrawerClicked = onDrawerClicked, ) } @@ -72,7 +73,7 @@ fun JetLaggedScreen( modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center, - maxItemsInEachRow = 3 + maxItemsInEachRow = 3, ) { JetLaggedSleepGraphCard(uiState.value.sleepGraphData, Modifier.widthIn(max = 600.dp)) if (windowSizeClass == WindowWidthSizeClass.Compact) { @@ -87,23 +88,27 @@ fun JetLaggedScreen( if (windowSizeClass == WindowWidthSizeClass.Compact) { WellnessCard( wellnessData = uiState.value.wellnessData, - modifier = Modifier.widthIn(max = 400.dp) - .heightIn(min = 200.dp) + modifier = + Modifier + .widthIn(max = 400.dp) + .heightIn(min = 200.dp), ) HeartRateCard( modifier = Modifier.widthIn(max = 400.dp, min = 200.dp), - uiState.value.heartRateData + uiState.value.heartRateData, ) } else { FlowColumn { WellnessCard( wellnessData = uiState.value.wellnessData, - modifier = Modifier.widthIn(max = 400.dp) - .heightIn(min = 200.dp) + modifier = + Modifier + .widthIn(max = 400.dp) + .heightIn(min = 200.dp), ) HeartRateCard( modifier = Modifier.widthIn(max = 400.dp, min = 200.dp), - uiState.value.heartRateData + uiState.value.heartRateData, ) } } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/MainActivity.kt b/JetLagged/app/src/main/java/com/example/jetlagged/MainActivity.kt index f98b43c06f..fab3f995b3 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/MainActivity.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/MainActivity.kt @@ -25,7 +25,6 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import com.example.jetlagged.ui.theme.JetLaggedTheme class MainActivity : ComponentActivity() { - @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/BubbleBackground.kt b/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/BubbleBackground.kt index 5b6eb82054..e495036ea9 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/BubbleBackground.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/BubbleBackground.kt @@ -41,58 +41,65 @@ import kotlin.random.Random fun BubbleBackground( modifier: Modifier = Modifier, numberBubbles: Int, - bubbleColor: Color + bubbleColor: Color, ) { val infiniteAnimation = rememberInfiniteTransition(label = "bubble position") Box(modifier = modifier) { - val bubbles = remember(numberBubbles) { - List(numberBubbles) { - BackgroundBubbleData( - startPosition = Offset( - x = Random.nextFloat(), - y = Random.nextFloat() - ), - endPosition = Offset( - x = Random.nextFloat(), - y = Random.nextFloat() - ), - durationMillis = Random.nextLong(3000L, 10000L), - easingFunction = EaseInOut, - radius = Random.nextFloat() * 30.dp + 20.dp - ) + val bubbles = + remember(numberBubbles) { + List(numberBubbles) { + BackgroundBubbleData( + startPosition = + Offset( + x = Random.nextFloat(), + y = Random.nextFloat(), + ), + endPosition = + Offset( + x = Random.nextFloat(), + y = Random.nextFloat(), + ), + durationMillis = Random.nextLong(3000L, 10000L), + easingFunction = EaseInOut, + radius = Random.nextFloat() * 30.dp + 20.dp, + ) + } } - } for (bubble in bubbles) { val xValue by infiniteAnimation.animateFloat( initialValue = bubble.startPosition.x, targetValue = bubble.endPosition.x, - animationSpec = infiniteRepeatable( - animation = tween( - bubble.durationMillis.toInt(), - easing = bubble.easingFunction + animationSpec = + infiniteRepeatable( + animation = + tween( + bubble.durationMillis.toInt(), + easing = bubble.easingFunction, + ), + repeatMode = RepeatMode.Reverse, ), - repeatMode = RepeatMode.Reverse - ), - label = "" + label = "", ) val yValue by infiniteAnimation.animateFloat( initialValue = bubble.startPosition.y, targetValue = bubble.endPosition.y, - animationSpec = infiniteRepeatable( - animation = tween( - bubble.durationMillis.toInt(), - easing = bubble.easingFunction + animationSpec = + infiniteRepeatable( + animation = + tween( + bubble.durationMillis.toInt(), + easing = bubble.easingFunction, + ), + repeatMode = RepeatMode.Reverse, ), - repeatMode = RepeatMode.Reverse - ), - label = "" + label = "", ) Canvas(modifier = Modifier.fillMaxSize()) { drawCircle( bubbleColor, radius = bubble.radius.toPx(), - center = Offset(xValue * size.width, yValue * size.height) + center = Offset(xValue * size.width, yValue * size.height), ) } } @@ -104,5 +111,5 @@ data class BackgroundBubbleData( val endPosition: Offset = Offset.Zero, val durationMillis: Long = 2000, val easingFunction: Easing = EaseInOut, - val radius: Dp = 0.dp + val radius: Dp = 0.dp, ) diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/FadingCircleBackground.kt b/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/FadingCircleBackground.kt index b0f5154433..113a448bd5 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/FadingCircleBackground.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/FadingCircleBackground.kt @@ -36,48 +36,60 @@ import androidx.compose.ui.unit.dp import kotlin.math.ceil @Composable -fun FadingCircleBackground(bubbleSize: Dp, color: Color) { - val alphaAnimation = remember { - Animatable(0.5f) - } +fun FadingCircleBackground( + bubbleSize: Dp, + color: Color, +) { + val alphaAnimation = + remember { + Animatable(0.5f) + } LaunchedEffect(Unit) { alphaAnimation.animateTo( 1f, - animationSpec = infiniteRepeatable( - animation = tween(2000, easing = EaseInOut), - repeatMode = RepeatMode.Reverse - ) + animationSpec = + infiniteRepeatable( + animation = tween(2000, easing = EaseInOut), + repeatMode = RepeatMode.Reverse, + ), ) } Box( - modifier = Modifier - .fillMaxSize() - .drawWithCache { - val bubbleSizePx = bubbleSize.toPx() - val paddingPx = 8.dp.toPx() - val numberCols = size.width / bubbleSizePx - val numberRows = size.height / bubbleSizePx + modifier = + Modifier + .fillMaxSize() + .drawWithCache { + val bubbleSizePx = bubbleSize.toPx() + val paddingPx = 8.dp.toPx() + val numberCols = size.width / bubbleSizePx + val numberRows = size.height / bubbleSizePx - onDrawBehind { - repeat(ceil(numberRows).toInt()) { row -> - repeat(ceil(numberCols).toInt()) { col -> - val offset = if (row.mod(2) == 0) - (bubbleSizePx + paddingPx) / 2f else 0f - drawCircle( - color.copy( - alpha = color.alpha * - ((row) / numberRows * alphaAnimation.value) - ), - radius = bubbleSizePx / 2f, - center = Offset( - (bubbleSizePx + paddingPx) * col + offset, - (bubbleSizePx + paddingPx) * row + onDrawBehind { + repeat(ceil(numberRows).toInt()) { row -> + repeat(ceil(numberCols).toInt()) { col -> + val offset = + if (row.mod(2) == 0) { + (bubbleSizePx + paddingPx) / 2f + } else { + 0f + } + drawCircle( + color.copy( + alpha = + color.alpha * + ((row) / numberRows * alphaAnimation.value), + ), + radius = bubbleSizePx / 2f, + center = + Offset( + (bubbleSizePx + paddingPx) * col + offset, + (bubbleSizePx + paddingPx) * row, + ), ) - ) + } } } - } - } + }, ) } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/ShaderBackground.kt b/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/ShaderBackground.kt index 4add69d96f..b5de93c110 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/ShaderBackground.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/backgrounds/ShaderBackground.kt @@ -38,13 +38,15 @@ import org.intellij.lang.annotations.Language private data object YellowBackgroundElement : ModifierNodeElement() { @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun create() = YellowBackgroundNode() + override fun update(node: YellowBackgroundNode) { } } @RequiresApi(Build.VERSION_CODES.TIRAMISU) -private class YellowBackgroundNode : DrawModifierNode, Modifier.Node() { - +private class YellowBackgroundNode : + Modifier.Node(), + DrawModifierNode { private val shader = RuntimeShader(SHADER) private val shaderBrush = ShaderBrush(shader) private val time = mutableFloatStateOf(0f) @@ -52,7 +54,7 @@ private class YellowBackgroundNode : DrawModifierNode, Modifier.Node() { init { shader.setColorUniform( "color", - Color.valueOf(Yellow.red, Yellow.green, Yellow.blue, Yellow.alpha) + Color.valueOf(Yellow.red, Yellow.green, Yellow.blue, Yellow.alpha), ) } @@ -90,7 +92,8 @@ fun Modifier.yellowBackground(): Modifier = } @Language("AGSL") -val SHADER = """ +val SHADER = + """ uniform float2 resolution; uniform float time; uniform float waves; @@ -128,4 +131,4 @@ val SHADER = """ return float4(rgbColor, 1.0); } -""".trimIndent() + """.trimIndent() diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/data/FakeHeartRateData.kt b/JetLagged/app/src/main/java/com/example/jetlagged/data/FakeHeartRateData.kt index 9eddae491b..58dbf80873 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/data/FakeHeartRateData.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/data/FakeHeartRateData.kt @@ -18,185 +18,190 @@ package com.example.jetlagged.data import java.time.LocalTime -data class HeartRateData(val date: LocalTime, val amount: Int) -internal val heartRateGraphData = listOf( - HeartRateData(LocalTime.of(0, 34), 55), - HeartRateData(LocalTime.of(0, 52), 145), - HeartRateData(LocalTime.of(0, 40), 99), - HeartRateData(LocalTime.of(0, 19), 72), - HeartRateData(LocalTime.of(0, 14), 150), - HeartRateData(LocalTime.of(1, 44), 95), - HeartRateData(LocalTime.of(1, 58), 105), - HeartRateData(LocalTime.of(1, 21), 170), - HeartRateData(LocalTime.of(1, 49), 152), - HeartRateData(LocalTime.of(1, 31), 55), - HeartRateData(LocalTime.of(1, 20), 158), - HeartRateData(LocalTime.of(1, 41), 67), - HeartRateData(LocalTime.of(1, 21), 65), - HeartRateData(LocalTime.of(2, 4), 159), - HeartRateData(LocalTime.of(2, 19), 174), - HeartRateData(LocalTime.of(2, 19), 117), - HeartRateData(LocalTime.of(2, 0), 84), - HeartRateData(LocalTime.of(2, 33), 152), - HeartRateData(LocalTime.of(2, 4), 162), - HeartRateData(LocalTime.of(3, 11), 55), - HeartRateData(LocalTime.of(3, 22), 93), - HeartRateData(LocalTime.of(3, 39), 133), - HeartRateData(LocalTime.of(3, 15), 173), - HeartRateData(LocalTime.of(3, 7), 172), - HeartRateData(LocalTime.of(4, 8), 93), - HeartRateData(LocalTime.of(4, 27), 148), - HeartRateData(LocalTime.of(4, 8), 153), - HeartRateData(LocalTime.of(4, 47), 170), - HeartRateData(LocalTime.of(4, 11), 60), - HeartRateData(LocalTime.of(4, 46), 100), - HeartRateData(LocalTime.of(4, 15), 175), - HeartRateData(LocalTime.of(5, 39), 133), - HeartRateData(LocalTime.of(5, 16), 98), - HeartRateData(LocalTime.of(5, 59), 80), - HeartRateData(LocalTime.of(5, 17), 122), - HeartRateData(LocalTime.of(5, 55), 144), - HeartRateData(LocalTime.of(5, 5), 101), - HeartRateData(LocalTime.of(5, 3), 141), - HeartRateData(LocalTime.of(5, 10), 153), - HeartRateData(LocalTime.of(5, 17), 135), - HeartRateData(LocalTime.of(6, 28), 117), - HeartRateData(LocalTime.of(6, 22), 153), - HeartRateData(LocalTime.of(6, 38), 103), - HeartRateData(LocalTime.of(9, 6), 92), - HeartRateData(LocalTime.of(9, 15), 141), - HeartRateData(LocalTime.of(9, 22), 120), - HeartRateData(LocalTime.of(10, 50), 125), - HeartRateData(LocalTime.of(10, 4), 109), - HeartRateData(LocalTime.of(10, 59), 174), - HeartRateData(LocalTime.of(10, 11), 115), - HeartRateData(LocalTime.of(10, 13), 92), - HeartRateData(LocalTime.of(10, 4), 127), - HeartRateData(LocalTime.of(10, 8), 62), - HeartRateData(LocalTime.of(10, 9), 129), - HeartRateData(LocalTime.of(11, 7), 128), - HeartRateData(LocalTime.of(11, 44), 67), - HeartRateData(LocalTime.of(11, 10), 130), - HeartRateData(LocalTime.of(11, 12), 153), - HeartRateData(LocalTime.of(11, 5), 133), - HeartRateData(LocalTime.of(11, 31), 174), - HeartRateData(LocalTime.of(11, 45), 91), - HeartRateData(LocalTime.of(11, 9), 95), - HeartRateData(LocalTime.of(11, 4), 102), - HeartRateData(LocalTime.of(11, 46), 147), - HeartRateData(LocalTime.of(11, 48), 145), - HeartRateData(LocalTime.of(11, 44), 131), - HeartRateData(LocalTime.of(12, 40), 159), - HeartRateData(LocalTime.of(12, 14), 150), - HeartRateData(LocalTime.of(12, 37), 118), - HeartRateData(LocalTime.of(12, 38), 134), - HeartRateData(LocalTime.of(12, 53), 168), - HeartRateData(LocalTime.of(12, 11), 143), - HeartRateData(LocalTime.of(12, 47), 110), - HeartRateData(LocalTime.of(12, 21), 116), - HeartRateData(LocalTime.of(12, 13), 145), - HeartRateData(LocalTime.of(13, 37), 56), - HeartRateData(LocalTime.of(13, 9), 132), - HeartRateData(LocalTime.of(13, 6), 98), - HeartRateData(LocalTime.of(13, 22), 134), - HeartRateData(LocalTime.of(13, 25), 125), - HeartRateData(LocalTime.of(13, 47), 101), - HeartRateData(LocalTime.of(13, 50), 138), - HeartRateData(LocalTime.of(13, 47), 59), - HeartRateData(LocalTime.of(13, 55), 105), - HeartRateData(LocalTime.of(14, 56), 73), - HeartRateData(LocalTime.of(14, 7), 67), - HeartRateData(LocalTime.of(14, 33), 118), - HeartRateData(LocalTime.of(14, 50), 169), - HeartRateData(LocalTime.of(14, 2), 125), - HeartRateData(LocalTime.of(14, 16), 93), - HeartRateData(LocalTime.of(14, 7), 80), - HeartRateData(LocalTime.of(14, 1), 129), - HeartRateData(LocalTime.of(14, 59), 142), - HeartRateData(LocalTime.of(15, 5), 62), - HeartRateData(LocalTime.of(15, 55), 132), - HeartRateData(LocalTime.of(15, 41), 145), - HeartRateData(LocalTime.of(15, 41), 107), - HeartRateData(LocalTime.of(15, 45), 110), - HeartRateData(LocalTime.of(16, 52), 97), - HeartRateData(LocalTime.of(16, 16), 127), - HeartRateData(LocalTime.of(16, 0), 155), - HeartRateData(LocalTime.of(16, 35), 75), - HeartRateData(LocalTime.of(16, 18), 170), - HeartRateData(LocalTime.of(16, 6), 68), - HeartRateData(LocalTime.of(16, 12), 63), - HeartRateData(LocalTime.of(16, 2), 162), - HeartRateData(LocalTime.of(16, 40), 146), - HeartRateData(LocalTime.of(16, 26), 70), - HeartRateData(LocalTime.of(16, 32), 121), - HeartRateData(LocalTime.of(17, 49), 87), - HeartRateData(LocalTime.of(17, 42), 54), - HeartRateData(LocalTime.of(17, 12), 169), - HeartRateData(LocalTime.of(17, 24), 154), - HeartRateData(LocalTime.of(17, 4), 75), - HeartRateData(LocalTime.of(17, 51), 104), - HeartRateData(LocalTime.of(17, 53), 114), - HeartRateData(LocalTime.of(17, 14), 93), - HeartRateData(LocalTime.of(17, 35), 146), - HeartRateData(LocalTime.of(17, 19), 101), - HeartRateData(LocalTime.of(17, 27), 130), - HeartRateData(LocalTime.of(17, 2), 56), - HeartRateData(LocalTime.of(17, 27), 55), - HeartRateData(LocalTime.of(17, 31), 73), - HeartRateData(LocalTime.of(18, 59), 103), - HeartRateData(LocalTime.of(18, 10), 95), - HeartRateData(LocalTime.of(18, 28), 120), - HeartRateData(LocalTime.of(18, 5), 88), - HeartRateData(LocalTime.of(18, 44), 63), - HeartRateData(LocalTime.of(18, 16), 124), - HeartRateData(LocalTime.of(18, 14), 120), - HeartRateData(LocalTime.of(18, 18), 121), - HeartRateData(LocalTime.of(18, 53), 167), - HeartRateData(LocalTime.of(18, 45), 110), - HeartRateData(LocalTime.of(19, 19), 170), - HeartRateData(LocalTime.of(19, 59), 85), - HeartRateData(LocalTime.of(19, 4), 84), - HeartRateData(LocalTime.of(19, 8), 111), - HeartRateData(LocalTime.of(19, 54), 75), - HeartRateData(LocalTime.of(20, 36), 122), - HeartRateData(LocalTime.of(20, 21), 153), - HeartRateData(LocalTime.of(20, 11), 82), - HeartRateData(LocalTime.of(20, 19), 152), - HeartRateData(LocalTime.of(20, 26), 56), - HeartRateData(LocalTime.of(20, 21), 63), - HeartRateData(LocalTime.of(20, 22), 90), - HeartRateData(LocalTime.of(20, 20), 172), - HeartRateData(LocalTime.of(20, 56), 78), - HeartRateData(LocalTime.of(21, 52), 65), - HeartRateData(LocalTime.of(21, 46), 106), - HeartRateData(LocalTime.of(21, 57), 129), - HeartRateData(LocalTime.of(21, 31), 105), - HeartRateData(LocalTime.of(21, 39), 138), - HeartRateData(LocalTime.of(21, 0), 93), - HeartRateData(LocalTime.of(21, 20), 67), - HeartRateData(LocalTime.of(21, 47), 166), - HeartRateData(LocalTime.of(21, 10), 136), - HeartRateData(LocalTime.of(21, 26), 90), - HeartRateData(LocalTime.of(21, 56), 83), - HeartRateData(LocalTime.of(21, 9), 72), - HeartRateData(LocalTime.of(21, 38), 87), - HeartRateData(LocalTime.of(22, 15), 149), - HeartRateData(LocalTime.of(22, 25), 176), - HeartRateData(LocalTime.of(22, 13), 77), - HeartRateData(LocalTime.of(22, 53), 159), - HeartRateData(LocalTime.of(22, 20), 81), - HeartRateData(LocalTime.of(22, 48), 150), - HeartRateData(LocalTime.of(22, 1), 123), - HeartRateData(LocalTime.of(22, 19), 130), - HeartRateData(LocalTime.of(23, 27), 147), - HeartRateData(LocalTime.of(23, 59), 126), - HeartRateData(LocalTime.of(23, 22), 142), - HeartRateData(LocalTime.of(23, 48), 114), - HeartRateData(LocalTime.of(23, 51), 93), - HeartRateData(LocalTime.of(23, 46), 65), - HeartRateData(LocalTime.of(23, 21), 63), - HeartRateData(LocalTime.of(23, 59), 95), -).sortedBy { it.date.toSecondOfDay() } +data class HeartRateData( + val date: LocalTime, + val amount: Int, +) + +internal val heartRateGraphData = + listOf( + HeartRateData(LocalTime.of(0, 34), 55), + HeartRateData(LocalTime.of(0, 52), 145), + HeartRateData(LocalTime.of(0, 40), 99), + HeartRateData(LocalTime.of(0, 19), 72), + HeartRateData(LocalTime.of(0, 14), 150), + HeartRateData(LocalTime.of(1, 44), 95), + HeartRateData(LocalTime.of(1, 58), 105), + HeartRateData(LocalTime.of(1, 21), 170), + HeartRateData(LocalTime.of(1, 49), 152), + HeartRateData(LocalTime.of(1, 31), 55), + HeartRateData(LocalTime.of(1, 20), 158), + HeartRateData(LocalTime.of(1, 41), 67), + HeartRateData(LocalTime.of(1, 21), 65), + HeartRateData(LocalTime.of(2, 4), 159), + HeartRateData(LocalTime.of(2, 19), 174), + HeartRateData(LocalTime.of(2, 19), 117), + HeartRateData(LocalTime.of(2, 0), 84), + HeartRateData(LocalTime.of(2, 33), 152), + HeartRateData(LocalTime.of(2, 4), 162), + HeartRateData(LocalTime.of(3, 11), 55), + HeartRateData(LocalTime.of(3, 22), 93), + HeartRateData(LocalTime.of(3, 39), 133), + HeartRateData(LocalTime.of(3, 15), 173), + HeartRateData(LocalTime.of(3, 7), 172), + HeartRateData(LocalTime.of(4, 8), 93), + HeartRateData(LocalTime.of(4, 27), 148), + HeartRateData(LocalTime.of(4, 8), 153), + HeartRateData(LocalTime.of(4, 47), 170), + HeartRateData(LocalTime.of(4, 11), 60), + HeartRateData(LocalTime.of(4, 46), 100), + HeartRateData(LocalTime.of(4, 15), 175), + HeartRateData(LocalTime.of(5, 39), 133), + HeartRateData(LocalTime.of(5, 16), 98), + HeartRateData(LocalTime.of(5, 59), 80), + HeartRateData(LocalTime.of(5, 17), 122), + HeartRateData(LocalTime.of(5, 55), 144), + HeartRateData(LocalTime.of(5, 5), 101), + HeartRateData(LocalTime.of(5, 3), 141), + HeartRateData(LocalTime.of(5, 10), 153), + HeartRateData(LocalTime.of(5, 17), 135), + HeartRateData(LocalTime.of(6, 28), 117), + HeartRateData(LocalTime.of(6, 22), 153), + HeartRateData(LocalTime.of(6, 38), 103), + HeartRateData(LocalTime.of(9, 6), 92), + HeartRateData(LocalTime.of(9, 15), 141), + HeartRateData(LocalTime.of(9, 22), 120), + HeartRateData(LocalTime.of(10, 50), 125), + HeartRateData(LocalTime.of(10, 4), 109), + HeartRateData(LocalTime.of(10, 59), 174), + HeartRateData(LocalTime.of(10, 11), 115), + HeartRateData(LocalTime.of(10, 13), 92), + HeartRateData(LocalTime.of(10, 4), 127), + HeartRateData(LocalTime.of(10, 8), 62), + HeartRateData(LocalTime.of(10, 9), 129), + HeartRateData(LocalTime.of(11, 7), 128), + HeartRateData(LocalTime.of(11, 44), 67), + HeartRateData(LocalTime.of(11, 10), 130), + HeartRateData(LocalTime.of(11, 12), 153), + HeartRateData(LocalTime.of(11, 5), 133), + HeartRateData(LocalTime.of(11, 31), 174), + HeartRateData(LocalTime.of(11, 45), 91), + HeartRateData(LocalTime.of(11, 9), 95), + HeartRateData(LocalTime.of(11, 4), 102), + HeartRateData(LocalTime.of(11, 46), 147), + HeartRateData(LocalTime.of(11, 48), 145), + HeartRateData(LocalTime.of(11, 44), 131), + HeartRateData(LocalTime.of(12, 40), 159), + HeartRateData(LocalTime.of(12, 14), 150), + HeartRateData(LocalTime.of(12, 37), 118), + HeartRateData(LocalTime.of(12, 38), 134), + HeartRateData(LocalTime.of(12, 53), 168), + HeartRateData(LocalTime.of(12, 11), 143), + HeartRateData(LocalTime.of(12, 47), 110), + HeartRateData(LocalTime.of(12, 21), 116), + HeartRateData(LocalTime.of(12, 13), 145), + HeartRateData(LocalTime.of(13, 37), 56), + HeartRateData(LocalTime.of(13, 9), 132), + HeartRateData(LocalTime.of(13, 6), 98), + HeartRateData(LocalTime.of(13, 22), 134), + HeartRateData(LocalTime.of(13, 25), 125), + HeartRateData(LocalTime.of(13, 47), 101), + HeartRateData(LocalTime.of(13, 50), 138), + HeartRateData(LocalTime.of(13, 47), 59), + HeartRateData(LocalTime.of(13, 55), 105), + HeartRateData(LocalTime.of(14, 56), 73), + HeartRateData(LocalTime.of(14, 7), 67), + HeartRateData(LocalTime.of(14, 33), 118), + HeartRateData(LocalTime.of(14, 50), 169), + HeartRateData(LocalTime.of(14, 2), 125), + HeartRateData(LocalTime.of(14, 16), 93), + HeartRateData(LocalTime.of(14, 7), 80), + HeartRateData(LocalTime.of(14, 1), 129), + HeartRateData(LocalTime.of(14, 59), 142), + HeartRateData(LocalTime.of(15, 5), 62), + HeartRateData(LocalTime.of(15, 55), 132), + HeartRateData(LocalTime.of(15, 41), 145), + HeartRateData(LocalTime.of(15, 41), 107), + HeartRateData(LocalTime.of(15, 45), 110), + HeartRateData(LocalTime.of(16, 52), 97), + HeartRateData(LocalTime.of(16, 16), 127), + HeartRateData(LocalTime.of(16, 0), 155), + HeartRateData(LocalTime.of(16, 35), 75), + HeartRateData(LocalTime.of(16, 18), 170), + HeartRateData(LocalTime.of(16, 6), 68), + HeartRateData(LocalTime.of(16, 12), 63), + HeartRateData(LocalTime.of(16, 2), 162), + HeartRateData(LocalTime.of(16, 40), 146), + HeartRateData(LocalTime.of(16, 26), 70), + HeartRateData(LocalTime.of(16, 32), 121), + HeartRateData(LocalTime.of(17, 49), 87), + HeartRateData(LocalTime.of(17, 42), 54), + HeartRateData(LocalTime.of(17, 12), 169), + HeartRateData(LocalTime.of(17, 24), 154), + HeartRateData(LocalTime.of(17, 4), 75), + HeartRateData(LocalTime.of(17, 51), 104), + HeartRateData(LocalTime.of(17, 53), 114), + HeartRateData(LocalTime.of(17, 14), 93), + HeartRateData(LocalTime.of(17, 35), 146), + HeartRateData(LocalTime.of(17, 19), 101), + HeartRateData(LocalTime.of(17, 27), 130), + HeartRateData(LocalTime.of(17, 2), 56), + HeartRateData(LocalTime.of(17, 27), 55), + HeartRateData(LocalTime.of(17, 31), 73), + HeartRateData(LocalTime.of(18, 59), 103), + HeartRateData(LocalTime.of(18, 10), 95), + HeartRateData(LocalTime.of(18, 28), 120), + HeartRateData(LocalTime.of(18, 5), 88), + HeartRateData(LocalTime.of(18, 44), 63), + HeartRateData(LocalTime.of(18, 16), 124), + HeartRateData(LocalTime.of(18, 14), 120), + HeartRateData(LocalTime.of(18, 18), 121), + HeartRateData(LocalTime.of(18, 53), 167), + HeartRateData(LocalTime.of(18, 45), 110), + HeartRateData(LocalTime.of(19, 19), 170), + HeartRateData(LocalTime.of(19, 59), 85), + HeartRateData(LocalTime.of(19, 4), 84), + HeartRateData(LocalTime.of(19, 8), 111), + HeartRateData(LocalTime.of(19, 54), 75), + HeartRateData(LocalTime.of(20, 36), 122), + HeartRateData(LocalTime.of(20, 21), 153), + HeartRateData(LocalTime.of(20, 11), 82), + HeartRateData(LocalTime.of(20, 19), 152), + HeartRateData(LocalTime.of(20, 26), 56), + HeartRateData(LocalTime.of(20, 21), 63), + HeartRateData(LocalTime.of(20, 22), 90), + HeartRateData(LocalTime.of(20, 20), 172), + HeartRateData(LocalTime.of(20, 56), 78), + HeartRateData(LocalTime.of(21, 52), 65), + HeartRateData(LocalTime.of(21, 46), 106), + HeartRateData(LocalTime.of(21, 57), 129), + HeartRateData(LocalTime.of(21, 31), 105), + HeartRateData(LocalTime.of(21, 39), 138), + HeartRateData(LocalTime.of(21, 0), 93), + HeartRateData(LocalTime.of(21, 20), 67), + HeartRateData(LocalTime.of(21, 47), 166), + HeartRateData(LocalTime.of(21, 10), 136), + HeartRateData(LocalTime.of(21, 26), 90), + HeartRateData(LocalTime.of(21, 56), 83), + HeartRateData(LocalTime.of(21, 9), 72), + HeartRateData(LocalTime.of(21, 38), 87), + HeartRateData(LocalTime.of(22, 15), 149), + HeartRateData(LocalTime.of(22, 25), 176), + HeartRateData(LocalTime.of(22, 13), 77), + HeartRateData(LocalTime.of(22, 53), 159), + HeartRateData(LocalTime.of(22, 20), 81), + HeartRateData(LocalTime.of(22, 48), 150), + HeartRateData(LocalTime.of(22, 1), 123), + HeartRateData(LocalTime.of(22, 19), 130), + HeartRateData(LocalTime.of(23, 27), 147), + HeartRateData(LocalTime.of(23, 59), 126), + HeartRateData(LocalTime.of(23, 22), 142), + HeartRateData(LocalTime.of(23, 48), 114), + HeartRateData(LocalTime.of(23, 51), 93), + HeartRateData(LocalTime.of(23, 46), 65), + HeartRateData(LocalTime.of(23, 21), 63), + HeartRateData(LocalTime.of(23, 59), 95), + ).sortedBy { it.date.toSecondOfDay() } const val numberEntries = 48 // 48 blocks of 30 minutes const val bracketInSeconds = 30 * 60 // 30 minutes time frame diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/data/FakeSleepData.kt b/JetLagged/app/src/main/java/com/example/jetlagged/data/FakeSleepData.kt index 44a508a73c..fe833f0a25 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/data/FakeSleepData.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/data/FakeSleepData.kt @@ -23,625 +23,838 @@ import com.example.jetlagged.sleep.SleepType import java.time.LocalDateTime // In the real world, you should get this data from a backend. -val sleepData = SleepGraphData( - listOf( - SleepDayData( - LocalDateTime.now().minusDays(7), - listOf( - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(7) - .withHour(21) - .withMinute(8), - endTime = LocalDateTime.now() - .minusDays(7) - .withHour(21) - .withMinute(40), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(7) - .withHour(21) - .withMinute(40), - endTime = LocalDateTime.now() - .minusDays(7) - .withHour(22) - .withMinute(20), - type = SleepType.Light - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(7) - .withHour(22) - .withMinute(20), - endTime = LocalDateTime.now() - .minusDays(7) - .withHour(22) - .withMinute(50), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(7) - .withHour(22) - .withMinute(50), - endTime = LocalDateTime.now() - .minusDays(7) - .withHour(23) - .withMinute(30), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(7) - .withHour(23) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(6) - .withHour(1) - .withMinute(10), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(6) - .withHour(1) - .withMinute(10), - endTime = LocalDateTime.now() - .minusDays(6) - .withHour(2) - .withMinute(30), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(6) - .withHour(2) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(6) - .withHour(4) - .withMinute(10), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(6) - .withHour(4) - .withMinute(10), - endTime = LocalDateTime.now() - .minusDays(6) - .withHour(5) - .withMinute(30), - type = SleepType.Awake - ) +val sleepData = + SleepGraphData( + listOf( + SleepDayData( + LocalDateTime.now().minusDays(7), + listOf( + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(7) + .withHour(21) + .withMinute(8), + endTime = + LocalDateTime + .now() + .minusDays(7) + .withHour(21) + .withMinute(40), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(7) + .withHour(21) + .withMinute(40), + endTime = + LocalDateTime + .now() + .minusDays(7) + .withHour(22) + .withMinute(20), + type = SleepType.Light, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(7) + .withHour(22) + .withMinute(20), + endTime = + LocalDateTime + .now() + .minusDays(7) + .withHour(22) + .withMinute(50), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(7) + .withHour(22) + .withMinute(50), + endTime = + LocalDateTime + .now() + .minusDays(7) + .withHour(23) + .withMinute(30), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(7) + .withHour(23) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(1) + .withMinute(10), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(1) + .withMinute(10), + endTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(2) + .withMinute(30), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(2) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(4) + .withMinute(10), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(4) + .withMinute(10), + endTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(5) + .withMinute(30), + type = SleepType.Awake, + ), + ), + sleepScore = 90, ), - sleepScore = 90 - ), - SleepDayData( - LocalDateTime.now().minusDays(6), - listOf( - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(6) - .withHour(22) - .withMinute(38), - endTime = LocalDateTime.now() - .minusDays(6) - .withHour(22) - .withMinute(50), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(6) - .withHour(22) - .withMinute(50), - endTime = LocalDateTime.now() - .minusDays(6) - .withHour(23) - .withMinute(30), - type = SleepType.Light - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(6) - .withHour(23) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(6) - .withHour(23) - .withMinute(55), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(6) - .withHour(23) - .withMinute(55), - endTime = LocalDateTime.now() - .minusDays(5) - .withHour(2) - .withMinute(40), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(5) - .withHour(2) - .withMinute(40), - endTime = LocalDateTime.now() - .minusDays(5) - .withHour(2) - .withMinute(50), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(5) - .withHour(2) - .withMinute(50), - endTime = LocalDateTime.now() - .minusDays(5) - .withHour(4) - .withMinute(12), - type = SleepType.Deep - ) + SleepDayData( + LocalDateTime.now().minusDays(6), + listOf( + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(22) + .withMinute(38), + endTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(22) + .withMinute(50), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(22) + .withMinute(50), + endTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(23) + .withMinute(30), + type = SleepType.Light, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(23) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(23) + .withMinute(55), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(6) + .withHour(23) + .withMinute(55), + endTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(2) + .withMinute(40), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(2) + .withMinute(40), + endTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(2) + .withMinute(50), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(2) + .withMinute(50), + endTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(4) + .withMinute(12), + type = SleepType.Deep, + ), + ), + sleepScore = 70, ), - sleepScore = 70 - ), - SleepDayData( - LocalDateTime.now().minusDays(5), - listOf( - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(5) - .withHour(22) - .withMinute(8), - endTime = LocalDateTime.now() - .minusDays(5) - .withHour(22) - .withMinute(40), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(5) - .withHour(22) - .withMinute(40), - endTime = LocalDateTime.now() - .minusDays(5) - .withHour(22) - .withMinute(50), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(5) - .withHour(22) - .withMinute(50), - endTime = LocalDateTime.now() - .minusDays(5) - .withHour(22) - .withMinute(55), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(5) - .withHour(22) - .withMinute(55), - endTime = LocalDateTime.now() - .minusDays(5) - .withHour(23) - .withMinute(30), - type = SleepType.Light - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(5) - .withHour(23) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(4) - .withHour(1) - .withMinute(10), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(4) - .withHour(1) - .withMinute(10), - endTime = LocalDateTime.now() - .minusDays(4) - .withHour(2) - .withMinute(30), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(4) - .withHour(2) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(4) - .withHour(3) - .withMinute(5), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(4) - .withHour(3) - .withMinute(5), - endTime = LocalDateTime.now() - .minusDays(4) - .withHour(4) - .withMinute(50), - type = SleepType.Light - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(4) - .withHour(4) - .withMinute(50), - endTime = LocalDateTime.now() - .minusDays(4) - .withHour(6) - .withMinute(30), - type = SleepType.REM - ) + SleepDayData( + LocalDateTime.now().minusDays(5), + listOf( + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(22) + .withMinute(8), + endTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(22) + .withMinute(40), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(22) + .withMinute(40), + endTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(22) + .withMinute(50), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(22) + .withMinute(50), + endTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(22) + .withMinute(55), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(22) + .withMinute(55), + endTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(23) + .withMinute(30), + type = SleepType.Light, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(5) + .withHour(23) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(1) + .withMinute(10), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(1) + .withMinute(10), + endTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(2) + .withMinute(30), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(2) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(3) + .withMinute(5), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(3) + .withMinute(5), + endTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(4) + .withMinute(50), + type = SleepType.Light, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(4) + .withMinute(50), + endTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(6) + .withMinute(30), + type = SleepType.REM, + ), + ), + sleepScore = 60, ), - sleepScore = 60 - ), - SleepDayData( - LocalDateTime.now().minusDays(4), - listOf( - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(4) - .withHour(20) - .withMinute(20), - endTime = LocalDateTime.now() - .minusDays(4) - .withHour(22) - .withMinute(40), - type = SleepType.Light - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(4) - .withHour(22) - .withMinute(40), - endTime = LocalDateTime.now() - .minusDays(4) - .withHour(22) - .withMinute(50), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(4) - .withHour(22) - .withMinute(50), - endTime = LocalDateTime.now() - .minusDays(4) - .withHour(23) - .withMinute(55), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(4) - .withHour(23) - .withMinute(55), - endTime = LocalDateTime.now() - .minusDays(3) - .withHour(1) - .withMinute(33), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(3) - .withHour(1) - .withMinute(33), - endTime = LocalDateTime.now() - .minusDays(3) - .withHour(2) - .withMinute(30), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(3) - .withHour(2) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(3) - .withHour(3) - .withMinute(45), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(3) - .withHour(3) - .withMinute(45), - endTime = LocalDateTime.now() - .minusDays(3) - .withHour(7) - .withMinute(15), - type = SleepType.Light - ) + SleepDayData( + LocalDateTime.now().minusDays(4), + listOf( + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(20) + .withMinute(20), + endTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(22) + .withMinute(40), + type = SleepType.Light, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(22) + .withMinute(40), + endTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(22) + .withMinute(50), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(22) + .withMinute(50), + endTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(23) + .withMinute(55), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(4) + .withHour(23) + .withMinute(55), + endTime = + LocalDateTime + .now() + .minusDays(3) + .withHour(1) + .withMinute(33), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(3) + .withHour(1) + .withMinute(33), + endTime = + LocalDateTime + .now() + .minusDays(3) + .withHour(2) + .withMinute(30), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(3) + .withHour(2) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(3) + .withHour(3) + .withMinute(45), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(3) + .withHour(3) + .withMinute(45), + endTime = + LocalDateTime + .now() + .minusDays(3) + .withHour(7) + .withMinute(15), + type = SleepType.Light, + ), + ), + sleepScore = 90, ), - sleepScore = 90 - ), - SleepDayData( - LocalDateTime.now().minusDays(3), - listOf( - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(3) - .withHour(22) - .withMinute(50), - endTime = LocalDateTime.now() - .minusDays(3) - .withHour(23) - .withMinute(30), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(3) - .withHour(23) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(2) - .withHour(0) - .withMinute(10), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(2) - .withHour(0) - .withMinute(10), - endTime = LocalDateTime.now() - .minusDays(2) - .withHour(1) - .withMinute(10), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(2) - .withHour(1) - .withMinute(10), - endTime = LocalDateTime.now() - .minusDays(2) - .withHour(2) - .withMinute(30), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(2) - .withHour(2) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(2) - .withHour(4) - .withMinute(30), - type = SleepType.Light - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(2) - .withHour(4) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(2) - .withHour(4) - .withMinute(45), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(2) - .withHour(4) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(2) - .withHour(4) - .withMinute(45), - type = SleepType.REM - ) + SleepDayData( + LocalDateTime.now().minusDays(3), + listOf( + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(3) + .withHour(22) + .withMinute(50), + endTime = + LocalDateTime + .now() + .minusDays(3) + .withHour(23) + .withMinute(30), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(3) + .withHour(23) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(0) + .withMinute(10), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(0) + .withMinute(10), + endTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(1) + .withMinute(10), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(1) + .withMinute(10), + endTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(2) + .withMinute(30), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(2) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(4) + .withMinute(30), + type = SleepType.Light, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(4) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(4) + .withMinute(45), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(4) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(4) + .withMinute(45), + type = SleepType.REM, + ), + ), + sleepScore = 40, ), - sleepScore = 40 - ), - SleepDayData( - LocalDateTime.now().minusDays(2), - listOf( - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(2) - .withHour(20) - .withMinute(40), - endTime = LocalDateTime.now() - .minusDays(2) - .withHour(21) - .withMinute(40), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(2) - .withHour(21) - .withMinute(40), - endTime = LocalDateTime.now() - .minusDays(2) - .withHour(22) - .withMinute(20), - type = SleepType.Light - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(2) - .withHour(22) - .withMinute(20), - endTime = LocalDateTime.now() - .minusDays(2) - .withHour(22) - .withMinute(50), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(2) - .withHour(22) - .withMinute(50), - endTime = LocalDateTime.now() - .minusDays(2) - .withHour(23) - .withMinute(30), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(2) - .withHour(23) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(1) - .withHour(1) - .withMinute(10), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(1) - .withHour(1) - .withMinute(10), - endTime = LocalDateTime.now() - .minusDays(1) - .withHour(2) - .withMinute(30), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(1) - .withHour(2) - .withMinute(30), - endTime = LocalDateTime.now() - .minusDays(1) - .withHour(4) - .withMinute(10), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(1) - .withHour(4) - .withMinute(10), - endTime = LocalDateTime.now() - .minusDays(1) - .withHour(5) - .withMinute(30), - type = SleepType.Awake - ) + SleepDayData( + LocalDateTime.now().minusDays(2), + listOf( + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(20) + .withMinute(40), + endTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(21) + .withMinute(40), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(21) + .withMinute(40), + endTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(22) + .withMinute(20), + type = SleepType.Light, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(22) + .withMinute(20), + endTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(22) + .withMinute(50), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(22) + .withMinute(50), + endTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(23) + .withMinute(30), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(2) + .withHour(23) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(1) + .withMinute(10), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(1) + .withMinute(10), + endTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(2) + .withMinute(30), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(2) + .withMinute(30), + endTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(4) + .withMinute(10), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(4) + .withMinute(10), + endTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(5) + .withMinute(30), + type = SleepType.Awake, + ), + ), + sleepScore = 82, ), - sleepScore = 82 - ), - SleepDayData( - LocalDateTime.now().minusDays(1), - listOf( - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(1) - .withHour(22) - .withMinute(8), - endTime = LocalDateTime.now() - .minusDays(1) - .withHour(22) - .withMinute(40), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(1) - .withHour(22) - .withMinute(40), - endTime = LocalDateTime.now() - .minusDays(1) - .withHour(22) - .withMinute(50), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(1) - .withHour(22) - .withMinute(50), - endTime = LocalDateTime.now() - .minusDays(1) - .withHour(22) - .withMinute(55), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(1) - .withHour(22) - .withMinute(55), - endTime = LocalDateTime.now() - .minusDays(1) - .withHour(23) - .withMinute(30), - type = SleepType.REM - ), - SleepPeriod( - startTime = LocalDateTime.now() - .minusDays(1) - .withHour(23) - .withMinute(30), - endTime = LocalDateTime.now() - .withHour(1) - .withMinute(10), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .withHour(1) - .withMinute(10), - endTime = LocalDateTime.now() - .withHour(2) - .withMinute(30), - type = SleepType.Awake - ), - SleepPeriod( - startTime = LocalDateTime.now() - .withHour(2) - .withMinute(30), - endTime = LocalDateTime.now() - .withHour(3) - .withMinute(5), - type = SleepType.Deep - ), - SleepPeriod( - startTime = LocalDateTime.now() - .withHour(3) - .withMinute(5), - endTime = LocalDateTime.now() - .withHour(4) - .withMinute(50), - type = SleepType.Light - ) + SleepDayData( + LocalDateTime.now().minusDays(1), + listOf( + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(22) + .withMinute(8), + endTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(22) + .withMinute(40), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(22) + .withMinute(40), + endTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(22) + .withMinute(50), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(22) + .withMinute(50), + endTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(22) + .withMinute(55), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(22) + .withMinute(55), + endTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(23) + .withMinute(30), + type = SleepType.REM, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .minusDays(1) + .withHour(23) + .withMinute(30), + endTime = + LocalDateTime + .now() + .withHour(1) + .withMinute(10), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .withHour(1) + .withMinute(10), + endTime = + LocalDateTime + .now() + .withHour(2) + .withMinute(30), + type = SleepType.Awake, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .withHour(2) + .withMinute(30), + endTime = + LocalDateTime + .now() + .withHour(3) + .withMinute(5), + type = SleepType.Deep, + ), + SleepPeriod( + startTime = + LocalDateTime + .now() + .withHour(3) + .withMinute(5), + endTime = + LocalDateTime + .now() + .withHour(4) + .withMinute(50), + type = SleepType.Light, + ), + ), + sleepScore = 70, ), - sleepScore = 70 ), ) -) diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/data/JetLaggedHomeScreenState.kt b/JetLagged/app/src/main/java/com/example/jetlagged/data/JetLaggedHomeScreenState.kt index 830c5b2c7a..c6a1e2fa7e 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/data/JetLaggedHomeScreenState.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/data/JetLaggedHomeScreenState.kt @@ -21,16 +21,16 @@ import com.example.jetlagged.sleep.SleepGraphData data class JetLaggedHomeScreenState( val sleepGraphData: SleepGraphData = sleepData, val wellnessData: WellnessData = WellnessData(10, 4, 5), - val heartRateData: HeartRateOverallData = HeartRateOverallData() + val heartRateData: HeartRateOverallData = HeartRateOverallData(), ) data class WellnessData( val snoring: Int, val coughing: Int, - val respiration: Int + val respiration: Int, ) data class HeartRateOverallData( val averageBpm: Int = 65, - val listData: List = heartRateGraphData + val listData: List = heartRateGraphData, ) diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/data/JetLaggedHomeScreenViewModel.kt b/JetLagged/app/src/main/java/com/example/jetlagged/data/JetLaggedHomeScreenViewModel.kt index 966e82c726..1953613c8d 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/data/JetLaggedHomeScreenViewModel.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/data/JetLaggedHomeScreenViewModel.kt @@ -21,6 +21,5 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class JetLaggedHomeScreenViewModel : ViewModel() { - val uiState: StateFlow = MutableStateFlow(JetLaggedHomeScreenState()) } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/heartrate/HeartRateCard.kt b/JetLagged/app/src/main/java/com/example/jetlagged/heartrate/HeartRateCard.kt index 6ef8973f47..ffd64a5df2 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/heartrate/HeartRateCard.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/heartrate/HeartRateCard.kt @@ -44,35 +44,37 @@ import com.example.jetlagged.ui.theme.TitleStyle @Composable fun HeartRateCard( modifier: Modifier = Modifier, - heartRateData: HeartRateOverallData = HeartRateOverallData() + heartRateData: HeartRateOverallData = HeartRateOverallData(), ) { BasicInformationalCard( borderColor = Coral, - modifier = modifier - .height(260.dp) + modifier = + modifier + .height(260.dp), ) { Column( - modifier = Modifier - .fillMaxSize() - + modifier = + Modifier + .fillMaxSize(), ) { HomeScreenCardHeading(text = stringResource(R.string.heart_rate_heading)) Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.Center + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.Center, ) { Text( heartRateData.averageBpm.toString(), style = TitleStyle, modifier = Modifier.alignByBaseline(), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Text( "bpm", modifier = Modifier.alignByBaseline(), - style = SmallHeadingStyle + style = SmallHeadingStyle, ) } HeartRateGraph(heartRateData.listData) diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/heartrate/HeartRateGraph.kt b/JetLagged/app/src/main/java/com/example/jetlagged/heartrate/HeartRateGraph.kt index dc7c72f364..e92d335d67 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/heartrate/HeartRateGraph.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/heartrate/HeartRateGraph.kt @@ -56,7 +56,7 @@ fun HeartRateGraph(listData: List) { Box(Modifier.size(width = 400.dp, height = 100.dp)) { Graph( listData = listData, - modifier = Modifier.padding(16.dp) + modifier = Modifier.padding(16.dp), ) } } @@ -64,7 +64,7 @@ fun HeartRateGraph(listData: List) { @Composable private fun Graph( listData: List, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Box( modifier @@ -76,20 +76,21 @@ private fun Graph( drawPath( paths.second, Coral.copy(alpha = 0.2f), - style = Fill + style = Fill, ) drawPath( paths.first, lineBrush, - style = Stroke(2.dp.toPx()) + style = Stroke(2.dp.toPx()), ) } - } + }, ) } sealed class DataPoint { object NoMeasurement : DataPoint() + data class Measurement( val averageMeasurementTime: Int, val minHeartRate: Int, @@ -98,7 +99,10 @@ sealed class DataPoint { ) : DataPoint() } -fun generateSmoothPath(data: List, size: Size): Pair { +fun generateSmoothPath( + data: List, + size: Size, +): Pair { val path = Path() val variancePath = Path() @@ -115,58 +119,76 @@ fun generateSmoothPath(data: List, size: Size): Pair var previousY = size.height var previousMaxX = 0f var previousMaxY = size.height - val groupedMeasurements = (0..numberEntries).map { bracketStart -> - heartRateGraphData.filter { - (bracketStart * bracketInSeconds..(bracketStart + 1) * bracketInSeconds) - .contains(it.date.toSecondOfDay()) - } - }.map { heartRates -> - if (heartRates.isEmpty()) DataPoint.NoMeasurement else - DataPoint.Measurement( - averageMeasurementTime = heartRates.map { it.date.toSecondOfDay() }.average() - .roundToInt(), - minHeartRate = heartRates.minBy { it.amount }.amount, - maxHeartRate = heartRates.maxBy { it.amount }.amount, - averageHeartRate = heartRates.map { it.amount }.average().roundToInt() - ) - } + val groupedMeasurements = + (0..numberEntries) + .map { bracketStart -> + heartRateGraphData.filter { + (bracketStart * bracketInSeconds..(bracketStart + 1) * bracketInSeconds) + .contains(it.date.toSecondOfDay()) + } + }.map { heartRates -> + if (heartRates.isEmpty()) { + DataPoint.NoMeasurement + } else { + DataPoint.Measurement( + averageMeasurementTime = + heartRates + .map { it.date.toSecondOfDay() } + .average() + .roundToInt(), + minHeartRate = heartRates.minBy { it.amount }.amount, + maxHeartRate = heartRates.maxBy { it.amount }.amount, + averageHeartRate = heartRates.map { it.amount }.average().roundToInt(), + ) + } + } groupedMeasurements.forEachIndexed { i, dataPoint -> if (i == 0 && dataPoint is DataPoint.Measurement) { path.moveTo( 0f, size.height - (dataPoint.averageHeartRate - graphBottom).toFloat() * - heightPxPerAmount + heightPxPerAmount, ) variancePath.moveTo( 0f, size.height - (dataPoint.maxHeartRate - graphBottom).toFloat() * - heightPxPerAmount + heightPxPerAmount, ) } if (dataPoint is DataPoint.Measurement) { val x = dataPoint.averageMeasurementTime * widthPerSecond - val y = size.height - (dataPoint.averageHeartRate - graphBottom).toFloat() * - heightPxPerAmount + val y = + size.height - (dataPoint.averageHeartRate - graphBottom).toFloat() * + heightPxPerAmount // to do smooth curve graph - we use cubicTo, uncomment section below for non-curve val controlPoint1 = PointF((x + previousX) / 2f, previousY) val controlPoint2 = PointF((x + previousX) / 2f, y) path.cubicTo( - controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, - x, y + controlPoint1.x, + controlPoint1.y, + controlPoint2.x, + controlPoint2.y, + x, + y, ) previousX = x previousY = y val maxX = dataPoint.averageMeasurementTime * widthPerSecond - val maxY = size.height - (dataPoint.maxHeartRate - graphBottom).toFloat() * - heightPxPerAmount + val maxY = + size.height - (dataPoint.maxHeartRate - graphBottom).toFloat() * + heightPxPerAmount val maxControlPoint1 = PointF((maxX + previousMaxX) / 2f, previousMaxY) val maxControlPoint2 = PointF((maxX + previousMaxX) / 2f, maxY) variancePath.cubicTo( - maxControlPoint1.x, maxControlPoint1.y, maxControlPoint2.x, maxControlPoint2.y, - maxX, maxY + maxControlPoint1.x, + maxControlPoint1.y, + maxControlPoint2.x, + maxControlPoint2.y, + maxX, + maxY, ) previousMaxX = maxX @@ -182,19 +204,24 @@ fun generateSmoothPath(data: List, size: Size): Pair variancePath.moveTo( size.width, size.height - (dataPoint.minHeartRate - graphBottom).toFloat() * - heightPxPerAmount + heightPxPerAmount, ) } if (dataPoint is DataPoint.Measurement) { val minX = dataPoint.averageMeasurementTime * widthPerSecond - val minY = size.height - (dataPoint.minHeartRate - graphBottom).toFloat() * - heightPxPerAmount + val minY = + size.height - (dataPoint.minHeartRate - graphBottom).toFloat() * + heightPxPerAmount val minControlPoint1 = PointF((minX + previousMinX) / 2f, previousMinY) val minControlPoint2 = PointF((minX + previousMinX) / 2f, minY) variancePath.cubicTo( - minControlPoint1.x, minControlPoint1.y, minControlPoint2.x, minControlPoint2.y, - minX, minY + minControlPoint1.x, + minControlPoint1.y, + minControlPoint2.x, + minControlPoint2.y, + minX, + minY, ) previousMinX = minX @@ -209,7 +236,7 @@ fun DrawScope.drawHighlight( highlightedWeek: Int, graphData: List, textMeasurer: TextMeasurer, - labelTextStyle: TextStyle + labelTextStyle: TextStyle, ) { val amount = graphData[highlightedWeek].amount val minAmount = graphData.minBy { it.amount }.amount @@ -223,31 +250,32 @@ fun DrawScope.drawHighlight( start = Offset(x, 0f), end = Offset(x, size.height), strokeWidth = 2.dp.toPx(), - pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)) + pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)), ) // draw hit circle on graph drawCircle( Color.Green, radius = 4.dp.toPx(), - center = Offset(x, pointY) + center = Offset(x, pointY), ) // draw info box val textLayoutResult = textMeasurer.measure("$amount", style = labelTextStyle) val highlightContainerSize = (textLayoutResult.size).toIntRect().inflate(4.dp.roundToPx()).size - val boxTopLeft = (x - (highlightContainerSize.width / 2f)) - .coerceIn(0f, size.width - highlightContainerSize.width) + val boxTopLeft = + (x - (highlightContainerSize.width / 2f)) + .coerceIn(0f, size.width - highlightContainerSize.width) drawRoundRect( Color.White, topLeft = Offset(boxTopLeft, 0f), size = highlightContainerSize.toSize(), - cornerRadius = CornerRadius(8.dp.toPx()) + cornerRadius = CornerRadius(8.dp.toPx()), ) drawText( textLayoutResult, color = Color.Black, - topLeft = Offset(boxTopLeft + 4.dp.toPx(), 4.dp.toPx()) + topLeft = Offset(boxTopLeft + 4.dp.toPx(), 4.dp.toPx()), ) } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedHeader.kt b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedHeader.kt index 894e81d22d..86c243f19e 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedHeader.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedHeader.kt @@ -42,10 +42,10 @@ import com.example.jetlagged.ui.theme.TitleBarStyle @Composable fun JetLaggedHeader( onDrawerClicked: () -> Unit = {}, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Box( - modifier.height(150.dp) + modifier.height(150.dp), ) { Row(modifier = Modifier.windowInsetsPadding(insets = WindowInsets.systemBars)) { IconButton( @@ -53,17 +53,18 @@ fun JetLaggedHeader( ) { Icon( Icons.Default.Menu, - contentDescription = stringResource(R.string.not_implemented) + contentDescription = stringResource(R.string.not_implemented), ) } Text( stringResource(R.string.jetlagged_app_heading), - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), style = TitleBarStyle, - textAlign = TextAlign.Start + textAlign = TextAlign.Start, ) } } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedHeaderTabs.kt b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedHeaderTabs.kt index 1666645a57..fa53dd6d21 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedHeaderTabs.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedHeaderTabs.kt @@ -38,12 +38,14 @@ import com.example.jetlagged.ui.theme.SmallHeadingStyle import com.example.jetlagged.ui.theme.White import com.example.jetlagged.ui.theme.Yellow -enum class SleepTab(val title: Int) { +enum class SleepTab( + val title: Int, +) { Day(R.string.sleep_tab_day_heading), Week(R.string.sleep_tab_week_heading), Month(R.string.sleep_tab_month_heading), SixMonths(R.string.sleep_tab_six_months_heading), - OneYear(R.string.sleep_tab_one_year_heading) + OneYear(R.string.sleep_tab_one_year_heading), } @Composable @@ -63,10 +65,10 @@ fun JetLaggedHeaderTabs( .tabIndicatorOffset(tabPositions[selectedTab.ordinal]) .fillMaxSize() .padding(horizontal = 2.dp) - .border(BorderStroke(2.dp, Yellow), RoundedCornerShape(10.dp)) + .border(BorderStroke(2.dp, Yellow), RoundedCornerShape(10.dp)), ) }, - divider = { } + divider = { }, ) { SleepTab.values().forEachIndexed { index, sleepTab -> val selected = index == selectedTab.ordinal @@ -74,14 +76,16 @@ fun JetLaggedHeaderTabs( sleepTab = sleepTab, selected = selected, onTabSelected = onTabSelected, - index = index + index = index, ) } } } -private val textModifier = Modifier - .padding(vertical = 6.dp, horizontal = 4.dp) +private val textModifier = + Modifier + .padding(vertical = 6.dp, horizontal = 4.dp) + @Composable private fun SleepTabText( sleepTab: SleepTab, @@ -90,20 +94,21 @@ private fun SleepTabText( onTabSelected: (SleepTab) -> Unit, ) { Tab( - modifier = Modifier - .padding(horizontal = 2.dp) - .clip(RoundedCornerShape(16.dp)), + modifier = + Modifier + .padding(horizontal = 2.dp) + .clip(RoundedCornerShape(16.dp)), selected = selected, unselectedContentColor = Color.Black, selectedContentColor = Color.Black, onClick = { onTabSelected(SleepTab.values()[index]) - } + }, ) { Text( modifier = textModifier, text = stringResource(id = sleepTab.title), - style = SmallHeadingStyle + style = SmallHeadingStyle, ) } } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedTimeGraph.kt b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedTimeGraph.kt index 8bb88d671f..59b2cecb3f 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedTimeGraph.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/JetLaggedTimeGraph.kt @@ -51,24 +51,24 @@ import java.util.Locale @Composable fun JetLaggedSleepGraphCard( sleepState: SleepGraphData, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { var selectedTab by remember { mutableStateOf(SleepTab.Week) } BasicInformationalCard( borderColor = Yellow, - modifier = modifier + modifier = modifier, ) { Column { HomeScreenCardHeading(text = "Sleep") JetLaggedHeaderTabs( onTabSelected = { selectedTab = it }, selectedTab = selectedTab, - modifier = Modifier.padding(top = 16.dp) + modifier = Modifier.padding(top = 16.dp), ) Spacer(modifier = Modifier.height(16.dp)) JetLaggedTimeGraph( - sleepState + sleepState, ) } } @@ -77,16 +77,17 @@ fun JetLaggedSleepGraphCard( @Composable private fun JetLaggedTimeGraph( sleepGraphData: SleepGraphData, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val scrollState = rememberScrollState() val hours = (sleepGraphData.earliestStartHour..23) + (0..sleepGraphData.latestEndHour) TimeGraph( - modifier = modifier - .horizontalScroll(scrollState) - .wrapContentSize(), + modifier = + modifier + .horizontalScroll(scrollState) + .wrapContentSize(), dayItemsCount = sleepGraphData.sleepDayData.size, hoursHeader = { HoursHeader(hours) @@ -100,15 +101,16 @@ private fun JetLaggedTimeGraph( // We have access to Modifier.timeGraphBar() as we are now in TimeGraphScope SleepBar( sleepData = data, - modifier = Modifier - .padding(bottom = 8.dp) - .timeGraphBar( - start = data.firstSleepStart, - end = data.lastSleepEnd, - hours = hours, - ) + modifier = + Modifier + .padding(bottom = 8.dp) + .timeGraphBar( + start = data.firstSleepStart, + end = data.lastSleepEnd, + hours = hours, + ), ) - } + }, ) } @@ -116,13 +118,14 @@ private fun JetLaggedTimeGraph( private fun DayLabel(dayOfWeek: DayOfWeek) { Text( dayOfWeek.getDisplayName( - TextStyle.SHORT, Locale.getDefault() + TextStyle.SHORT, + Locale.getDefault(), ), Modifier .height(24.dp) .padding(start = 8.dp, end = 24.dp), style = SmallHeadingStyle, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } @@ -137,16 +140,17 @@ private fun HoursHeader(hours: List) { brush, cornerRadius = CornerRadius(10.dp.toPx(), 10.dp.toPx()), ) - } + }, ) { hours.forEach { Text( text = "$it", textAlign = TextAlign.Center, - modifier = Modifier - .width(50.dp) - .padding(vertical = 4.dp), - style = SmallHeadingStyle + modifier = + Modifier + .width(50.dp) + .padding(vertical = 4.dp), + style = SmallHeadingStyle, ) } } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/SleepBar.kt b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/SleepBar.kt index 675229d814..96df41fbc7 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/SleepBar.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/SleepBar.kt @@ -93,30 +93,35 @@ fun SleepBar( val transition = updateTransition(targetState = isExpanded, label = "expanded") Column( - modifier = modifier - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - isExpanded = !isExpanded - } + modifier = + modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + isExpanded = !isExpanded + }, ) { SleepRoundedBar( sleepData, - transition + transition, ) transition.AnimatedVisibility( - enter = fadeIn(animationSpec = tween(animationDuration)) + expandVertically( - animationSpec = tween(animationDuration) - ), - exit = fadeOut(animationSpec = tween(animationDuration)) + shrinkVertically( - animationSpec = tween(animationDuration) - ), + enter = + fadeIn(animationSpec = tween(animationDuration)) + + expandVertically( + animationSpec = tween(animationDuration), + ), + exit = + fadeOut(animationSpec = tween(animationDuration)) + + shrinkVertically( + animationSpec = tween(animationDuration), + ), content = { DetailLegend() }, - visible = { it } + visible = { it }, ) } } @@ -133,7 +138,7 @@ private fun SleepRoundedBar( spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = - Spring.StiffnessLow + Spring.StiffnessLow, ) }) { targetExpanded -> if (targetExpanded) 100.dp else 24.dp @@ -142,73 +147,81 @@ private fun SleepRoundedBar( spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = - Spring.StiffnessLow + Spring.StiffnessLow, ) }) { target -> if (target) 1f else 0f } Spacer( - modifier = Modifier - .drawWithCache { - val width = this.size.width - val cornerRadiusStartPx = 2.dp.toPx() - val collapsedCornerRadiusPx = 10.dp.toPx() - val animatedCornerRadius = CornerRadius( - lerp(cornerRadiusStartPx, collapsedCornerRadiusPx, (1 - animationProgress)) - ) + modifier = + Modifier + .drawWithCache { + val width = this.size.width + val cornerRadiusStartPx = 2.dp.toPx() + val collapsedCornerRadiusPx = 10.dp.toPx() + val animatedCornerRadius = + CornerRadius( + lerp(cornerRadiusStartPx, collapsedCornerRadiusPx, (1 - animationProgress)), + ) - val lineThicknessPx = lineThickness.toPx() - val roundedRectPath = Path() - roundedRectPath.addRoundRect( - RoundRect( - rect = Rect( - Offset(x = 0f, y = -lineThicknessPx / 2f), - Size( - this.size.width + lineThicknessPx * 2, - this.size.height + lineThicknessPx - ) + val lineThicknessPx = lineThickness.toPx() + val roundedRectPath = Path() + roundedRectPath.addRoundRect( + RoundRect( + rect = + Rect( + Offset(x = 0f, y = -lineThicknessPx / 2f), + Size( + this.size.width + lineThicknessPx * 2, + this.size.height + lineThicknessPx, + ), + ), + cornerRadius = animatedCornerRadius, ), - cornerRadius = animatedCornerRadius - ) - ) - val roundedCornerStroke = Stroke( - lineThicknessPx, - cap = StrokeCap.Round, - join = StrokeJoin.Round, - pathEffect = PathEffect.cornerPathEffect( - cornerRadiusStartPx * animationProgress ) - ) - val barHeightPx = barHeight.toPx() + val roundedCornerStroke = + Stroke( + lineThicknessPx, + cap = StrokeCap.Round, + join = StrokeJoin.Round, + pathEffect = + PathEffect.cornerPathEffect( + cornerRadiusStartPx * animationProgress, + ), + ) + val barHeightPx = barHeight.toPx() - val sleepGraphPath = generateSleepPath( - this.size, - sleepData, width, barHeightPx, animationProgress, - lineThickness.toPx() / 2f - ) - val gradientBrush = - Brush.verticalGradient( - colorStops = sleepGradientBarColorStops.toTypedArray(), - startY = 0f, - endY = SleepType.values().size * barHeightPx - ) - val textResult = textMeasurer.measure(AnnotatedString(sleepData.sleepScoreEmoji)) + val sleepGraphPath = + generateSleepPath( + this.size, + sleepData, + width, + barHeightPx, + animationProgress, + lineThickness.toPx() / 2f, + ) + val gradientBrush = + Brush.verticalGradient( + colorStops = sleepGradientBarColorStops.toTypedArray(), + startY = 0f, + endY = SleepType.values().size * barHeightPx, + ) + val textResult = textMeasurer.measure(AnnotatedString(sleepData.sleepScoreEmoji)) - onDrawBehind { - drawSleepBar( - roundedRectPath, - sleepGraphPath, - gradientBrush, - roundedCornerStroke, - animationProgress, - textResult, - cornerRadiusStartPx - ) - } - } - .height(height) - .fillMaxWidth() + onDrawBehind { + drawSleepBar( + roundedRectPath, + sleepGraphPath, + gradientBrush, + roundedCornerStroke, + animationProgress, + textResult, + cornerRadiusStartPx, + ) + } + }.height(height) + .fillMaxWidth(), ) } @@ -227,14 +240,14 @@ private fun DrawScope.drawSleepBar( drawPath( sleepGraphPath, style = roundedCornerStroke, - brush = gradientBrush + brush = gradientBrush, ) } translate(left = -animationProgress * (textResult.size.width + textPadding.toPx())) { drawText( textResult, - topLeft = Offset(textPadding.toPx(), cornerRadiusStartPx) + topLeft = Offset(textPadding.toPx(), cornerRadiusStartPx), ) } } @@ -259,39 +272,44 @@ private fun generateSleepPath( sleepData.sleepPeriods.forEach { period -> val percentageOfTotal = sleepData.fractionOfTotalTime(period) val periodWidth = percentageOfTotal * width - val startOffsetPercentage = sleepData.minutesAfterSleepStart(period) / - sleepData.totalTimeInBed.toMinutes().toFloat() + val startOffsetPercentage = + sleepData.minutesAfterSleepStart(period) / + sleepData.totalTimeInBed.toMinutes().toFloat() val halfBarHeight = canvasSize.height / SleepType.values().size / 2f - val offset = if (previousPeriod == null) { - 0f - } else { - halfBarHeight - } + val offset = + if (previousPeriod == null) { + 0f + } else { + halfBarHeight + } - val offsetY = lerp( - 0f, - period.type.heightSleepType() * canvasSize.height, heightAnimation - ) + val offsetY = + lerp( + 0f, + period.type.heightSleepType() * canvasSize.height, + heightAnimation, + ) // step 1 - draw a line from previous sleep period to current if (previousPeriod != null) { path.lineTo( x = startOffsetPercentage * width + lineThicknessPx, - y = offsetY + offset + y = offsetY + offset, ) } // step 2 - add the current sleep period as rectangle to path path.addRect( - rect = Rect( - offset = Offset(x = startOffsetPercentage * width + lineThicknessPx, y = offsetY), - size = canvasSize.copy(width = periodWidth, height = barHeightPx) - ) + rect = + Rect( + offset = Offset(x = startOffsetPercentage * width + lineThicknessPx, y = offsetY), + size = canvasSize.copy(width = periodWidth, height = barHeightPx), + ), ) // step 3 - move to the middle of the current sleep period path.moveTo( x = startOffsetPercentage * width + periodWidth + lineThicknessPx, - y = offsetY + halfBarHeight + y = offsetY + halfBarHeight, ) previousPeriod = period @@ -304,7 +322,7 @@ private fun generateSleepPath( private fun DetailLegend() { Row( modifier = Modifier.padding(top = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { SleepType.values().forEach { LegendItem(it) @@ -316,15 +334,16 @@ private fun DetailLegend() { private fun LegendItem(sleepType: SleepType) { Row(verticalAlignment = Alignment.CenterVertically) { Box( - modifier = Modifier - .size(10.dp) - .clip(CircleShape) - .background(sleepType.color) + modifier = + Modifier + .size(10.dp) + .clip(CircleShape) + .background(sleepType.color), ) Text( stringResource(id = sleepType.title), style = LegendHeadingStyle, - modifier = Modifier.padding(start = 4.dp) + modifier = Modifier.padding(start = 4.dp), ) } } @@ -340,23 +359,23 @@ private val barHeight = 24.dp private const val animationDuration = 500 private val textPadding = 4.dp -private val sleepGradientBarColorStops: List> = SleepType.values().map { - Pair( - when (it) { - SleepType.Awake -> 0f - SleepType.REM -> 0.33f - SleepType.Light -> 0.66f - SleepType.Deep -> 1f - }, - it.color - ) -} +private val sleepGradientBarColorStops: List> = + SleepType.values().map { + Pair( + when (it) { + SleepType.Awake -> 0f + SleepType.REM -> 0.33f + SleepType.Light -> 0.66f + SleepType.Deep -> 1f + }, + it.color, + ) + } -private fun SleepType.heightSleepType(): Float { - return when (this) { +private fun SleepType.heightSleepType(): Float = + when (this) { SleepType.Awake -> 0f SleepType.REM -> 0.25f SleepType.Light -> 0.5f SleepType.Deep -> 0.75f } -} diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/SleepData.kt b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/SleepData.kt index e896b52f7f..8efef317e2 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/SleepData.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/SleepData.kt @@ -61,16 +61,14 @@ data class SleepDayData( } } - fun fractionOfTotalTime(sleepPeriod: SleepPeriod): Float { - return sleepPeriod.duration.toMinutes() / totalTimeInBed.toMinutes().toFloat() - } + fun fractionOfTotalTime(sleepPeriod: SleepPeriod): Float = sleepPeriod.duration.toMinutes() / totalTimeInBed.toMinutes().toFloat() - fun minutesAfterSleepStart(sleepPeriod: SleepPeriod): Long { - return Duration.between( - firstSleepStart, - sleepPeriod.startTime - ).toMinutes() - } + fun minutesAfterSleepStart(sleepPeriod: SleepPeriod): Long = + Duration + .between( + firstSleepStart, + sleepPeriod.startTime, + ).toMinutes() } data class SleepPeriod( @@ -78,15 +76,17 @@ data class SleepPeriod( val endTime: LocalDateTime, val type: SleepType, ) { - val duration: Duration by lazy { Duration.between(startTime, endTime) } } -enum class SleepType(val title: Int, val color: Color) { +enum class SleepType( + val title: Int, + val color: Color, +) { Awake(R.string.sleep_type_awake, Yellow_Awake), REM(R.string.sleep_type_rem, Yellow_Rem), Light(R.string.sleep_type_light, Yellow_Light), - Deep(R.string.sleep_type_deep, Yellow_Deep) + Deep(R.string.sleep_type_deep, Yellow_Deep), } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/TimeGraph.kt b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/TimeGraph.kt index a04415a45e..1363f362b6 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/sleep/TimeGraph.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/sleep/TimeGraph.kt @@ -45,7 +45,7 @@ fun TimeGraph( val bars = @Composable { repeat(dayItemsCount) { TimeGraphScope.bar(it) } } Layout( contents = listOf(hoursHeader, dayLabels, bars), - modifier = modifier.padding(bottom = 32.dp) + modifier = modifier.padding(bottom = 32.dp), ) { (hoursHeaderMeasurables, dayLabelMeasurables, barMeasureables), constraints, @@ -55,26 +55,29 @@ fun TimeGraph( } val hoursHeaderPlaceable = hoursHeaderMeasurables.first().measure(constraints) - val dayLabelPlaceables = dayLabelMeasurables.map { measurable -> - val placeable = measurable.measure(constraints) - placeable - } + val dayLabelPlaceables = + dayLabelMeasurables.map { measurable -> + val placeable = measurable.measure(constraints) + placeable + } var totalHeight = hoursHeaderPlaceable.height - val barPlaceables = barMeasureables.map { measurable -> - val barParentData = measurable.parentData as TimeGraphParentData - val barWidth = (barParentData.duration * hoursHeaderPlaceable.width).roundToInt() + val barPlaceables = + barMeasureables.map { measurable -> + val barParentData = measurable.parentData as TimeGraphParentData + val barWidth = (barParentData.duration * hoursHeaderPlaceable.width).roundToInt() - val barPlaceable = measurable.measure( - constraints.copy( - minWidth = barWidth, - maxWidth = barWidth - ) - ) - totalHeight += barPlaceable.height - barPlaceable - } + val barPlaceable = + measurable.measure( + constraints.copy( + minWidth = barWidth, + maxWidth = barWidth, + ), + ) + totalHeight += barPlaceable.height + barPlaceable + } val totalWidth = dayLabelPlaceables.first().width + hoursHeaderPlaceable.width @@ -117,8 +120,8 @@ object TimeGraphScope { return then( TimeGraphParentData( duration = durationInHours / hours.size, - offset = offsetInHours / hours.size - ) + offset = offsetInHours / hours.size, + ), ) } } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/ui/theme/Theme.kt b/JetLagged/app/src/main/java/com/example/jetlagged/ui/theme/Theme.kt index ec96690ddf..0ebae77ae3 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/ui/theme/Theme.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/ui/theme/Theme.kt @@ -22,26 +22,27 @@ import androidx.compose.material3.Shapes import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -private val LightColorScheme = lightColorScheme( - primary = Yellow, - secondary = MintGreen, - tertiary = Coral, - secondaryContainer = Yellow, - surface = White -) +private val LightColorScheme = + lightColorScheme( + primary = Yellow, + secondary = MintGreen, + tertiary = Coral, + secondaryContainer = Yellow, + surface = White, + ) private val shapes: Shapes @Composable - get() = MaterialTheme.shapes.copy( - large = CircleShape - ) + get() = + MaterialTheme.shapes.copy( + large = CircleShape, + ) + @Composable -fun JetLaggedTheme( - content: @Composable () -> Unit, -) { +fun JetLaggedTheme(content: @Composable () -> Unit) { MaterialTheme( colorScheme = LightColorScheme, typography = Typography, shapes = shapes, - content = content + content = content, ) } diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/ui/theme/Type.kt b/JetLagged/app/src/main/java/com/example/jetlagged/ui/theme/Type.kt index 02244d29d3..9a3043fb87 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/ui/theme/Type.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/ui/theme/Type.kt @@ -30,56 +30,66 @@ import com.example.jetlagged.R val fontName = GoogleFont("Lato") -val provider = GoogleFont.Provider( - providerAuthority = "com.google.android.gms.fonts", - providerPackage = "com.google.android.gms", - certificates = R.array.com_google_android_gms_fonts_certs -) -val fontFamily = FontFamily( - Font(googleFont = fontName, fontProvider = provider) -) +val provider = + GoogleFont.Provider( + providerAuthority = "com.google.android.gms.fonts", + providerPackage = "com.google.android.gms", + certificates = R.array.com_google_android_gms_fonts_certs, + ) +val fontFamily = + FontFamily( + Font(googleFont = fontName, fontProvider = provider), + ) + // Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp +val Typography = + Typography( + bodyLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), ) -) -val TitleBarStyle = TextStyle( - fontSize = 22.sp, - fontWeight = FontWeight(700), - letterSpacing = 0.5.sp, - fontFamily = fontFamily -) +val TitleBarStyle = + TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight(700), + letterSpacing = 0.5.sp, + fontFamily = fontFamily, + ) -val HeadingStyle = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight(600), - letterSpacing = 0.5.sp, - fontFamily = fontFamily -) +val HeadingStyle = + TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight(600), + letterSpacing = 0.5.sp, + fontFamily = fontFamily, + ) -val SmallHeadingStyle = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight(600), - letterSpacing = 0.5.sp, - fontFamily = fontFamily -) +val SmallHeadingStyle = + TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight(600), + letterSpacing = 0.5.sp, + fontFamily = fontFamily, + ) -val LegendHeadingStyle = TextStyle( - fontSize = 10.sp, - fontWeight = FontWeight(600), - letterSpacing = 0.5.sp, - fontFamily = fontFamily -) +val LegendHeadingStyle = + TextStyle( + fontSize = 10.sp, + fontWeight = FontWeight(600), + letterSpacing = 0.5.sp, + fontFamily = fontFamily, + ) -val TitleStyle = TextStyle( - fontSize = 36.sp, - fontWeight = FontWeight(500), - letterSpacing = 0.5.sp, - fontFamily = fontFamily -) +val TitleStyle = + TextStyle( + fontSize = 36.sp, + fontWeight = FontWeight(500), + letterSpacing = 0.5.sp, + fontFamily = fontFamily, + ) diff --git a/JetLagged/app/src/main/java/com/example/jetlagged/ui/util/MultiDevicePreview.kt b/JetLagged/app/src/main/java/com/example/jetlagged/ui/util/MultiDevicePreview.kt index 760fda4b79..8f908e3238 100644 --- a/JetLagged/app/src/main/java/com/example/jetlagged/ui/util/MultiDevicePreview.kt +++ b/JetLagged/app/src/main/java/com/example/jetlagged/ui/util/MultiDevicePreview.kt @@ -22,12 +22,12 @@ import androidx.compose.ui.tooling.preview.Preview @Preview( name = "small font", group = "font scales", - fontScale = 0.5f + fontScale = 0.5f, ) @Preview( name = "large font", group = "font scales", - fontScale = 1.5f + fontScale = 1.5f, ) annotation class FontScalePreviews diff --git a/JetLagged/build.gradle.kts b/JetLagged/build.gradle.kts index 0a68895495..ae6e81c536 100644 --- a/JetLagged/build.gradle.kts +++ b/JetLagged/build.gradle.kts @@ -14,6 +14,8 @@ * limitations under the License. */ +import com.diffplug.gradle.spotless.SpotlessExtension + plugins { alias(libs.plugins.gradle.versions) alias(libs.plugins.version.catalog.update) @@ -21,6 +23,31 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.compose) apply false + alias(libs.plugins.spotless) } apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") + +subprojects { + apply { + plugin(rootProject.libs.plugins.spotless.get().pluginId) + } + configure { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt") + ktlint(libs.versions.ktlint.get()) + .setEditorConfigPath("${project.rootDir}/.editorconfig") + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } + // Additional configuration for Kotlin Gradle scripts + kotlinGradle { + target("**/*.kts") + targetExclude("**/build/**/*.kts") + ktlint(libs.versions.ktlint.get()) // Apply ktlint to Gradle Kotlin scripts + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") + } + } +} + diff --git a/JetLagged/buildscripts/init.gradle.kts b/JetLagged/buildscripts/init.gradle.kts deleted file mode 100644 index 1b7a54264c..0000000000 --- a/JetLagged/buildscripts/init.gradle.kts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -val ktlintVersion = "0.46.1" - -initscript { - val spotlessVersion = "6.10.0" - - repositories { - mavenCentral() - } - - dependencies { - classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion") - } -} - -allprojects { - if (this == rootProject) { - return@allprojects - } - apply() - extensions.configure { - kotlin { - target("**/*.kt") - targetExclude("**/build/**/*.kt") - ktlint(ktlintVersion).editorConfigOverride( - mapOf( - "ktlint_code_style" to "android", - "ij_kotlin_allow_trailing_comma" to true, - // These rules were introduced in ktlint 0.46.0 and should not be - // enabled without further discussion. They are disabled for now. - // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 - "disabled_rules" to - "filename," + - "annotation,annotation-spacing," + - "argument-list-wrapping," + - "double-colon-spacing," + - "enum-entry-name-case," + - "multiline-if-else," + - "no-empty-first-line-in-method-block," + - "package-name," + - "trailing-comma," + - "spacing-around-angle-brackets," + - "spacing-between-declarations-with-annotations," + - "spacing-between-declarations-with-comments," + - "unary-op-spacing" - ) - ) - licenseHeaderFile(rootProject.file("spotless/copyright.kt")) - } - format("kts") { - target("**/*.kts") - targetExclude("**/build/**/*.kts") - // Look for the first line that doesn't have a block comment (assumed to be the license) - licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") - } - } -} \ No newline at end of file diff --git a/JetLagged/gradle/libs.versions.toml b/JetLagged/gradle/libs.versions.toml index 2c34e54e54..016f154a0d 100644 --- a/JetLagged/gradle/libs.versions.toml +++ b/JetLagged/gradle/libs.versions.toml @@ -47,6 +47,7 @@ junit = "4.13.2" kotlin = "2.0.0" kotlinx_immutable = "0.3.7" ksp = "2.0.0-1.0.21" +ktlint = "1.3.1" maps-compose = "3.1.1" # @keep minSdk = "21" @@ -57,6 +58,7 @@ roborazzi = "1.12.0" rome = "1.18.0" room = "2.6.0" secrets = "2.0.1" +spotless = "6.25.0" # @keep targetSdk = "33" version-catalog-update = "0.8.4" @@ -179,4 +181,5 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } diff --git a/JetNews/.editorconfig b/JetNews/.editorconfig new file mode 100644 index 0000000000..43f0af8237 --- /dev/null +++ b/JetNews/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{kt,kts}] +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL + +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ktlint_standard_property-naming = disabled +ktlint_function_naming_ignore_when_annotated_with = Composable diff --git a/JetNews/app/build.gradle.kts b/JetNews/app/build.gradle.kts index 82d3559368..57e7eff418 100644 --- a/JetNews/app/build.gradle.kts +++ b/JetNews/app/build.gradle.kts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,6 +14,7 @@ * limitations under the License. */ + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -21,13 +22,22 @@ plugins { } android { - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() namespace = "com.example.jetnews" defaultConfig { applicationId = "com.example.jetnews" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() + targetSdk = + libs.versions.targetSdk + .get() + .toInt() versionCode = 1 versionName = "1.0" vectorDrawables.useSupportLibrary = true @@ -49,14 +59,15 @@ android { buildTypes { getByName("debug") { - } getByName("release") { isMinifyEnabled = true signingConfig = signingConfigs.getByName("release") - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/AppContainerImpl.kt b/JetNews/app/src/main/java/com/example/jetnews/data/AppContainerImpl.kt index ed5aaeda92..1d9219b14d 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/AppContainerImpl.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/AppContainerImpl.kt @@ -35,8 +35,9 @@ interface AppContainer { * * Variables are initialized lazily and the same instance is shared across the whole app. */ -class AppContainerImpl(private val applicationContext: Context) : AppContainer { - +class AppContainerImpl( + private val applicationContext: Context, +) : AppContainer { override val postsRepository: PostsRepository by lazy { FakePostsRepository() } diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/Result.kt b/JetNews/app/src/main/java/com/example/jetnews/data/Result.kt index 9273b0b02e..b9594356ca 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/Result.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/Result.kt @@ -20,10 +20,13 @@ package com.example.jetnews.data * A generic class that holds a value or an exception */ sealed class Result { - data class Success(val data: T) : Result() - data class Error(val exception: Exception) : Result() -} + data class Success( + val data: T, + ) : Result() -fun Result.successOr(fallback: T): T { - return (this as? Result.Success)?.data ?: fallback + data class Error( + val exception: Exception, + ) : Result() } + +fun Result.successOr(fallback: T): T = (this as? Result.Success)?.data ?: fallback diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/interests/InterestsRepository.kt b/JetNews/app/src/main/java/com/example/jetnews/data/interests/InterestsRepository.kt index 9b4ffb1774..3f9d4bc9bf 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/interests/InterestsRepository.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/interests/InterestsRepository.kt @@ -19,13 +19,15 @@ package com.example.jetnews.data.interests import com.example.jetnews.data.Result import kotlinx.coroutines.flow.Flow -data class InterestSection(val title: String, val interests: List) +data class InterestSection( + val title: String, + val interests: List, +) /** * Interface to the Interests data layer. */ interface InterestsRepository { - /** * Get relevant topics to the user. */ @@ -72,4 +74,7 @@ interface InterestsRepository { fun observePublicationSelected(): Flow> } -data class TopicSelection(val section: String, val topic: String) +data class TopicSelection( + val section: String, + val topic: String, +) diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/interests/impl/FakeInterestsRepository.kt b/JetNews/app/src/main/java/com/example/jetnews/data/interests/impl/FakeInterestsRepository.kt index 2f7750740c..9af04dbd24 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/interests/impl/FakeInterestsRepository.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/interests/impl/FakeInterestsRepository.kt @@ -32,15 +32,14 @@ import kotlinx.coroutines.flow.update */ @OptIn(ExperimentalCoroutinesApi::class) class FakeInterestsRepository : InterestsRepository { - private val topics by lazy { listOf( InterestSection("Android", listOf("Jetpack Compose", "Kotlin", "Jetpack")), InterestSection( "Programming", - listOf("Kotlin", "Declarative UIs", "Java", "Unidirectional Data Flow", "C++") + listOf("Kotlin", "Declarative UIs", "Java", "Unidirectional Data Flow", "C++"), ), - InterestSection("Technology", listOf("Pixel", "Google")) + InterestSection("Technology", listOf("Pixel", "Google")), ) } @@ -54,7 +53,7 @@ class FakeInterestsRepository : InterestsRepository { "L'Elij Venonn", "Kraag Solazarn", "Tava Targesh", - "Kemarrin Muuda" + "Kemarrin Muuda", ) } @@ -68,7 +67,7 @@ class FakeInterestsRepository : InterestsRepository { "Jetpack Ark", "Composeshack", "Jetpack Point", - "Compose Tribune" + "Compose Tribune", ) } @@ -77,17 +76,11 @@ class FakeInterestsRepository : InterestsRepository { private val selectedPeople = MutableStateFlow(setOf()) private val selectedPublications = MutableStateFlow(setOf()) - override suspend fun getTopics(): Result> { - return Result.Success(topics) - } + override suspend fun getTopics(): Result> = Result.Success(topics) - override suspend fun getPeople(): Result> { - return Result.Success(people) - } + override suspend fun getPeople(): Result> = Result.Success(people) - override suspend fun getPublications(): Result> { - return Result.Success(publications) - } + override suspend fun getPublications(): Result> = Result.Success(publications) override suspend fun toggleTopicSelection(topic: TopicSelection) { selectedTopics.update { diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt b/JetNews/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt index d880a36e77..cf87574d28 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.Flow * Interface to the Posts data layer. */ interface PostsRepository { - /** * Get a specific JetNews post. */ diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/BlockingFakePostsRepository.kt b/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/BlockingFakePostsRepository.kt index aa95a36d69..41ed95c585 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/BlockingFakePostsRepository.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/BlockingFakePostsRepository.kt @@ -34,14 +34,13 @@ import kotlinx.coroutines.withContext */ @OptIn(ExperimentalCoroutinesApi::class) class BlockingFakePostsRepository : PostsRepository { - // for now, keep the favorites in memory private val favorites = MutableStateFlow>(setOf()) private val postsFeed = MutableStateFlow(null) - override suspend fun getPost(postId: String?): Result { - return withContext(Dispatchers.IO) { + override suspend fun getPost(postId: String?): Result = + withContext(Dispatchers.IO) { val post = posts.allPosts.find { it.id == postId } if (post == null) { Result.Error(IllegalArgumentException("Unable to find post")) @@ -49,7 +48,6 @@ class BlockingFakePostsRepository : PostsRepository { Result.Success(post) } } - } override suspend fun getPostsFeed(): Result { postsFeed.update { posts } @@ -57,6 +55,7 @@ class BlockingFakePostsRepository : PostsRepository { } override fun observeFavorites(): Flow> = favorites + override fun observePostsFeed(): Flow = postsFeed override suspend fun toggleFavorite(postId: String) { diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/FakePostsRepository.kt b/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/FakePostsRepository.kt index 939e095a1b..f9d6ef2101 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/FakePostsRepository.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/FakePostsRepository.kt @@ -33,7 +33,6 @@ import kotlinx.coroutines.withContext * posts with resources after some delay in a background thread. */ class FakePostsRepository : PostsRepository { - // for now, store these in memory private val favorites = MutableStateFlow>(setOf()) @@ -41,8 +40,8 @@ class FakePostsRepository : PostsRepository { // Used to make suspend functions that read and update state safe to call from any thread - override suspend fun getPost(postId: String?): Result { - return withContext(Dispatchers.IO) { + override suspend fun getPost(postId: String?): Result = + withContext(Dispatchers.IO) { val post = posts.allPosts.find { it.id == postId } if (post == null) { Result.Error(IllegalArgumentException("Post not found")) @@ -50,10 +49,9 @@ class FakePostsRepository : PostsRepository { Result.Success(post) } } - } - override suspend fun getPostsFeed(): Result { - return withContext(Dispatchers.IO) { + override suspend fun getPostsFeed(): Result = + withContext(Dispatchers.IO) { delay(800) // pretend we're on a slow network if (shouldRandomlyFail()) { Result.Error(IllegalStateException()) @@ -62,9 +60,9 @@ class FakePostsRepository : PostsRepository { Result.Success(posts) } } - } override fun observeFavorites(): Flow> = favorites + override fun observePostsFeed(): Flow = postsFeed override suspend fun toggleFavorite(postId: String) { diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/PostsData.kt b/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/PostsData.kt index 862fc03b27..a38677c02f 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/PostsData.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/PostsData.kt @@ -14,7 +14,8 @@ * limitations under the License. */ -@file:Suppress("ktlint:max-line-length") // String constants read better +@file:Suppress("ktlint:standard:max-line-length") // String constants read better + package com.example.jetnews.data.posts.impl import com.example.jetnews.R @@ -34,1165 +35,1183 @@ import com.example.jetnews.model.Publication val pietro = PostAuthor("Pietro Maggi", "https://medium.com/@pmaggi") val manuel = PostAuthor("Manuel Vivo", "https://medium.com/@manuelvicnt") -val florina = PostAuthor( - "Florina Muntenescu", - "https://medium.com/@florina.muntenescu" -) +val florina = + PostAuthor( + "Florina Muntenescu", + "https://medium.com/@florina.muntenescu", + ) val jose = PostAuthor("Jose Alcérreca", "https://medium.com/@JoseAlcerreca") val androidstudioteam = PostAuthor("Android Studio Team", "https://twitter.com/androidstudio") -val publication = Publication( - "Android Developers", - "https://cdn-images-1.medium.com/max/258/1*u7oZc2_5mrkcFaxkXEyfYA@2x.png" -) -val paragraphsPost1 = listOf( - Paragraph( - ParagraphType.Text, - "Working to make our Android application more modular, I ended up with a sample that included a set of on-demand features grouped inside a folder:" - ), - Paragraph( - ParagraphType.Text, - "Pretty standard setup, all the on-demand modules, inside a “features” folder; clean." - ), - Paragraph( - ParagraphType.Text, - "These modules are included in the settings.gradle file as:" - ), - Paragraph( - ParagraphType.CodeBlock, - "include ':app'\n" + - "include ':features:module1'\n" + - "include ':features:module2'\n" + - "include ':features:module3'\n" + - "include ':features:module4'" - ), - Paragraph( - ParagraphType.Text, - "These setup works nicely with a single “minor” issue: an empty module named features in the Android view in Android Studio:" - ), - Paragraph( - ParagraphType.Text, - "I can live with that, but I would much prefer to remove that empty module from my project!" - ), - Paragraph( - ParagraphType.Header, - "If you cannot remove it, just rename it!" - ), - - Paragraph( - ParagraphType.Text, - "At I/O I was lucky enough to attend the “Android Studio: Tips and Tricks” talk where Ivan Gravilovic, from Google, shared some amazing tips. One of these was a possible solution for my problem: setting a custom path for my modules.", - listOf( - Markup( - MarkupType.Italic, - 41, - 72 - ) - ) - ), - - Paragraph( - ParagraphType.Text, - "In this particular case our settings.gradle becomes:", - listOf(Markup(MarkupType.Code, 28, 43)) - ), - Paragraph( - ParagraphType.CodeBlock, - """ - include ':app' - include ':module1' - include ':module1' - include ':module1' - include ':module1' - """.trimIndent() - ), - Paragraph( - ParagraphType.CodeBlock, - """ - // Set a custom path for the four features modules. - // This avoid to have an empty "features" module in Android Studio. - project(":module1").projectDir=new File(rootDir, "features/module1") - project(":module2").projectDir=new File(rootDir, "features/module2") - project(":module3").projectDir=new File(rootDir, "features/module3") - project(":module4").projectDir=new File(rootDir, "features/module4") - """.trimIndent() - ), - Paragraph( - ParagraphType.Text, - "And the layout in Android Studio is now:" - ), - Paragraph( - ParagraphType.Header, - "Conclusion" - ), - Paragraph( - ParagraphType.Text, - "As the title says, this is really a small thing, but it helps keep my project in order and it shows how a small Gradle configuration can help keep your project tidy." - ), - Paragraph( - ParagraphType.Quote, - "You can find this update in the latest version of the on-demand modules codelab.", - listOf( - Markup( - MarkupType.Link, - 54, - 79, - "https://codelabs.developers.google.com/codelabs/on-demand-dynamic-delivery/index.html" - ) - ) - ), - Paragraph( - ParagraphType.Header, - "Resources" - ), - Paragraph( - ParagraphType.Bullet, - "Android Studio: Tips and Tricks (Google I/O’19)", - listOf( - Markup( - MarkupType.Link, - 0, - 47, - "https://www.youtube.com/watch?v=ihF-PwDfRZ4&list=PLWz5rJ2EKKc9FfSQIRXEWyWpHD6TtwxMM&index=32&t=0s" - ) - ) - ), - - Paragraph( - ParagraphType.Bullet, - "On Demand module codelab", - listOf( - Markup( - MarkupType.Link, - 0, - 24, - "https://codelabs.developers.google.com/codelabs/on-demand-dynamic-delivery/index.html" - ) - ) - ), - Paragraph( - ParagraphType.Bullet, - "Patchwork Plaid — A modularization story", - listOf( - Markup( - MarkupType.Link, - 0, - 40, - "https://medium.com/androiddevelopers/a-patchwork-plaid-monolith-to-modularized-app-60235d9f212e" - ) - ) +val publication = + Publication( + "Android Developers", + "https://cdn-images-1.medium.com/max/258/1*u7oZc2_5mrkcFaxkXEyfYA@2x.png", ) -) - -val paragraphsPost2 = listOf( - Paragraph( - ParagraphType.Text, - "Dagger is a popular Dependency Injection framework commonly used in Android. It provides fully static and compile-time dependencies addressing many of the development and performance issues that have reflection-based solutions.", - listOf( - Markup( - MarkupType.Link, - 0, - 6, - "https://dagger.dev/" - ) - ) - ), - Paragraph( - ParagraphType.Text, - "This month, a new tutorial was released to help you better understand how it works. This article focuses on using Dagger with Kotlin, including best practices to optimize your build time and gotchas you might encounter.", - listOf( - Markup( - MarkupType.Link, - 14, - 26, - "https://dagger.dev/tutorial/" - ), - Markup(MarkupType.Bold, 114, 132), - Markup(MarkupType.Bold, 144, 159), - Markup(MarkupType.Bold, 191, 198) - ) - ), - Paragraph( - ParagraphType.Text, - "Dagger is implemented using Java’s annotations model and annotations in Kotlin are not always directly parallel with how equivalent Java code would be written. This post will highlight areas where they differ and how you can use Dagger with Kotlin without having a headache." - ), - Paragraph( - ParagraphType.Text, - "This post was inspired by some of the suggestions in this Dagger issue that goes through best practices and pain points of Dagger in Kotlin. Thanks to all of the contributors that commented there!", - listOf( - Markup( - MarkupType.Link, - 58, - 70, - "https://github.com/google/dagger/issues/900" - ) - ) - ), - Paragraph( - ParagraphType.Header, - "kapt build improvements" - ), - Paragraph( - ParagraphType.Text, - "To improve your build time, Dagger added support for gradle’s incremental annotation processing in v2.18! This is enabled by default in Dagger v2.24. In case you’re using a lower version, you need to add a few lines of code (as shown below) if you want to benefit from it.", - listOf( - Markup( - MarkupType.Link, - 99, - 104, - "https://github.com/google/dagger/releases/tag/dagger-2.18" - ), - Markup( - MarkupType.Link, - 143, - 148, - "https://github.com/google/dagger/releases/tag/dagger-2.24" - ), - Markup(MarkupType.Bold, 53, 95) - ) - ), - Paragraph( - ParagraphType.Text, - "Also, you can tell Dagger not to format the generated code. This option was added in Dagger v2.18 and it’s the default behavior (doesn’t generate formatted code) in v2.23. If you’re using a lower version, disable code formatting to improve your build time (see code below).", - listOf( - Markup( - MarkupType.Link, - 92, - 97, - "https://github.com/google/dagger/releases/tag/dagger-2.18" - ), - Markup( - MarkupType.Link, - 165, - 170, - "https://github.com/google/dagger/releases/tag/dagger-2.23" - ) - ) - ), - Paragraph( - ParagraphType.Text, - "Include these compiler arguments in your build.gradle file to make Dagger more performant at build time:", - listOf(Markup(MarkupType.Code, 41, 53)) - ), - Paragraph( - ParagraphType.Text, - "Alternatively, if you use Kotlin DSL script files, include them like this in the build.gradle.kts file of the modules that use Dagger:", - listOf(Markup(MarkupType.Code, 81, 97)) - ), - Paragraph( - ParagraphType.Text, - "Qualifiers for field attributes" - ), - Paragraph( - ParagraphType.Text, - "", - listOf(Markup(MarkupType.Link, 0, 0)) - ), - Paragraph( - ParagraphType.Text, - "When an annotation is placed on a property in Kotlin, it’s not clear whether Java will see that annotation on the field of the property or the method for that property. Setting the field: prefix on the annotation ensures that the qualifier ends up in the right place (See documentation for more details).", - listOf( - Markup(MarkupType.Code, 181, 187), - Markup( - MarkupType.Link, - 268, - 285, - "http://frogermcs.github.io/dependency-injection-with-dagger-2-custom-scopes/" - ), - Markup(MarkupType.Italic, 114, 119), - Markup(MarkupType.Italic, 143, 149) - ) - ), - Paragraph( - ParagraphType.Text, - "✅ The way to apply qualifiers on an injected field is:" - ), - Paragraph( - ParagraphType.CodeBlock, - "@Inject @field:MinimumBalance lateinit var minimumBalance: BigDecimal", - listOf(Markup(MarkupType.Bold, 8, 29)) - ), - Paragraph( - ParagraphType.Text, - "❌ As opposed to:" - ), - Paragraph( - ParagraphType.CodeBlock, - """ - @Inject @MinimumBalance lateinit var minimumBalance: BigDecimal - // @MinimumBalance is ignored! - """.trimIndent(), - listOf(Markup(MarkupType.Bold, 65, 95)) - ), - Paragraph( - ParagraphType.Text, - "Forgetting to add field: could lead to injecting the wrong object if there’s an unqualified instance of that type available in the Dagger graph.", - listOf(Markup(MarkupType.Code, 18, 24)) - ), - Paragraph( - ParagraphType.Header, - "Static @Provides functions optimization" - ), - Paragraph( - ParagraphType.Text, - "Dagger’s generated code will be more performant if @Provides methods are static. To achieve this in Kotlin, use a Kotlin object instead of a class and annotate your methods with @JvmStatic. This is a best practice that you should follow as much as possible.", - listOf( - Markup(MarkupType.Code, 51, 60), - Markup(MarkupType.Code, 73, 79), - Markup(MarkupType.Code, 121, 127), - Markup(MarkupType.Code, 141, 146), - Markup(MarkupType.Code, 178, 188), - Markup(MarkupType.Bold, 200, 213), - Markup(MarkupType.Italic, 200, 213) - ) - ), - Paragraph( - ParagraphType.Text, - "In case you need an abstract method, you’ll need to add the @JvmStatic method to a companion object and annotate it with @Module too.", - listOf( - Markup(MarkupType.Code, 60, 70), - Markup(MarkupType.Code, 121, 128) - ) - ), - Paragraph( - ParagraphType.Text, - "Alternatively, you can extract the object module out and include it in the abstract one:" - ), - Paragraph( - ParagraphType.Header, - "Injecting Generics" - ), - Paragraph( - ParagraphType.Text, - "Kotlin compiles generics with wildcards to make Kotlin APIs work with Java. These are generated when a type appears as a parameter (more info here) or as fields. For example, a Kotlin List parameter shows up as List in Java.", - listOf( - Markup(MarkupType.Code, 184, 193), - Markup(MarkupType.Code, 216, 233), - Markup( - MarkupType.Link, - 132, - 146, - "https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#variant-generics" - ) - ) - ), - Paragraph( - ParagraphType.Text, - "This causes problems with Dagger because it expects an exact (aka invariant) type match. Using @JvmSuppressWildcards will ensure that Dagger sees the type without wildcards.", - listOf( - Markup(MarkupType.Code, 95, 116), - Markup( - MarkupType.Link, - 66, - 75, - "https://en.wikipedia.org/wiki/Class_invariant" - ), - Markup( - MarkupType.Link, - 96, - 116, - "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-suppress-wildcards/index.html" - ) - ) - ), - Paragraph( - ParagraphType.Text, - "This is a common issue when you inject collections using Dagger’s multibinding feature, for example:", - listOf( - Markup( - MarkupType.Link, - 57, - 86, - "https://dagger.dev/multibindings.html" - ) - ) - ), - Paragraph( - ParagraphType.CodeBlock, - """ - class MyVMFactory @Inject constructor( - private val vmMap: Map> - ) { - ... - } - """.trimIndent(), - listOf(Markup(MarkupType.Bold, 72, 93)) - ), - Paragraph( - ParagraphType.Header, - "Inline method bodies" - ), - Paragraph( - ParagraphType.Text, - "Dagger determines the types that are configured by @Provides methods by inspecting the return type. Specifying the return type in Kotlin functions is optional and even the IDE sometimes encourages you to refactor your code to have inline method bodies that hide the return type declaration.", - listOf(Markup(MarkupType.Code, 51, 60)) - ), - Paragraph( - ParagraphType.Text, - "This can lead to bugs if the inferred type is different from the one you meant. Let’s see some examples:" - ), - Paragraph( - ParagraphType.Text, - "If you want to add a specific type to the graph, inlining works as expected. See the different ways to do the same in Kotlin:" - ), - Paragraph( - ParagraphType.Text, - "If you want to provide an implementation of an interface, then you must explicitly specify the return type. Not doing it can lead to problems and bugs:" - ), - Paragraph( - ParagraphType.Text, - "Dagger mostly works with Kotlin out of the box. However, you have to watch out for a few things just to make sure you’re doing what you really mean to do: @field: for qualifiers on field attributes, inline method bodies, and @JvmSuppressWildcards when injecting collections.", - listOf( - Markup(MarkupType.Code, 155, 162), - Markup(MarkupType.Code, 225, 246) - ) - ), - Paragraph( - ParagraphType.Text, - "Dagger optimizations come with no cost, add them and follow best practices to improve your build time: enabling incremental annotation processing, disabling formatting and using static @Provides methods in your Dagger modules.", - listOf( - Markup( - MarkupType.Code, - 185, - 194 - ) - ) +val paragraphsPost1 = + listOf( + Paragraph( + ParagraphType.Text, + "Working to make our Android application more modular, I ended up with a sample that included a set of on-demand features grouped inside a folder:", + ), + Paragraph( + ParagraphType.Text, + "Pretty standard setup, all the on-demand modules, inside a “features” folder; clean.", + ), + Paragraph( + ParagraphType.Text, + "These modules are included in the settings.gradle file as:", + ), + Paragraph( + ParagraphType.CodeBlock, + "include ':app'\n" + + "include ':features:module1'\n" + + "include ':features:module2'\n" + + "include ':features:module3'\n" + + "include ':features:module4'", + ), + Paragraph( + ParagraphType.Text, + "These setup works nicely with a single “minor” issue: an empty module named features in the Android view in Android Studio:", + ), + Paragraph( + ParagraphType.Text, + "I can live with that, but I would much prefer to remove that empty module from my project!", + ), + Paragraph( + ParagraphType.Header, + "If you cannot remove it, just rename it!", + ), + Paragraph( + ParagraphType.Text, + "At I/O I was lucky enough to attend the “Android Studio: Tips and Tricks” talk where Ivan Gravilovic, from Google, shared some amazing tips. One of these was a possible solution for my problem: setting a custom path for my modules.", + listOf( + Markup( + MarkupType.Italic, + 41, + 72, + ), + ), + ), + Paragraph( + ParagraphType.Text, + "In this particular case our settings.gradle becomes:", + listOf(Markup(MarkupType.Code, 28, 43)), + ), + Paragraph( + ParagraphType.CodeBlock, + """ + include ':app' + include ':module1' + include ':module1' + include ':module1' + include ':module1' + """.trimIndent(), + ), + Paragraph( + ParagraphType.CodeBlock, + """ + // Set a custom path for the four features modules. + // This avoid to have an empty "features" module in Android Studio. + project(":module1").projectDir=new File(rootDir, "features/module1") + project(":module2").projectDir=new File(rootDir, "features/module2") + project(":module3").projectDir=new File(rootDir, "features/module3") + project(":module4").projectDir=new File(rootDir, "features/module4") + """.trimIndent(), + ), + Paragraph( + ParagraphType.Text, + "And the layout in Android Studio is now:", + ), + Paragraph( + ParagraphType.Header, + "Conclusion", + ), + Paragraph( + ParagraphType.Text, + "As the title says, this is really a small thing, but it helps keep my project in order and it shows how a small Gradle configuration can help keep your project tidy.", + ), + Paragraph( + ParagraphType.Quote, + "You can find this update in the latest version of the on-demand modules codelab.", + listOf( + Markup( + MarkupType.Link, + 54, + 79, + "https://codelabs.developers.google.com/codelabs/on-demand-dynamic-delivery/index.html", + ), + ), + ), + Paragraph( + ParagraphType.Header, + "Resources", + ), + Paragraph( + ParagraphType.Bullet, + "Android Studio: Tips and Tricks (Google I/O’19)", + listOf( + Markup( + MarkupType.Link, + 0, + 47, + "https://www.youtube.com/watch?v=ihF-PwDfRZ4&list=PLWz5rJ2EKKc9FfSQIRXEWyWpHD6TtwxMM&index=32&t=0s", + ), + ), + ), + Paragraph( + ParagraphType.Bullet, + "On Demand module codelab", + listOf( + Markup( + MarkupType.Link, + 0, + 24, + "https://codelabs.developers.google.com/codelabs/on-demand-dynamic-delivery/index.html", + ), + ), + ), + Paragraph( + ParagraphType.Bullet, + "Patchwork Plaid — A modularization story", + listOf( + Markup( + MarkupType.Link, + 0, + 40, + "https://medium.com/androiddevelopers/a-patchwork-plaid-monolith-to-modularized-app-60235d9f212e", + ), + ), + ), ) -) -val paragraphsPost3 = listOf( - Paragraph( - ParagraphType.Text, - "Learn how to get started converting Java Programming Language code to Kotlin, making it more idiomatic and avoid common pitfalls, by following our new Refactoring to Kotlin codelab, available in English \uD83C\uDDEC\uD83C\uDDE7, Chinese \uD83C\uDDE8\uD83C\uDDF3 and Brazilian Portuguese \uD83C\uDDE7\uD83C\uDDF7.", - listOf( - Markup( - MarkupType.Link, - 151, - 172, - "https://codelabs.developers.google.com/codelabs/java-to-kotlin/#0" - ), - Markup( - MarkupType.Link, - 209, - 216, - "https://clmirror.storage.googleapis.com/codelabs/java-to-kotlin-zh/index.html#0" - ), - Markup( - MarkupType.Link, - 226, - 246, - "https://codelabs.developers.google.com/codelabs/java-to-kotlin-pt-br/#0" - ) - ) - ), - Paragraph( - ParagraphType.Text, - "When you first get started writing Kotlin code, you tend to follow Java Programming Language idioms. The automatic converter, part of both Android Studio and Intellij IDEA, can do a pretty good job of automatically refactoring your code, but sometimes, it needs a little help. This is where our new Refactoring to Kotlin codelab comes in.", - listOf( - Markup( - MarkupType.Link, - 105, - 124, - "https://www.jetbrains.com/help/idea/converting-a-java-file-to-kotlin-file.html" - ) - ) - ), - Paragraph( - ParagraphType.Text, - "We’ll take two classes (a User and a Repository) in Java Programming Language and convert them to Kotlin, check out what the automatic converter did and why. Then we go to the next level — make it idiomatic, teaching best practices and useful tips along the way.", - listOf( - Markup(MarkupType.Code, 26, 30), - Markup(MarkupType.Code, 37, 47) - ) - ), - Paragraph( - ParagraphType.Text, - "The Refactoring to Kotlin codelab starts with basic topics — understand how nullability is declared in Kotlin, what types of equality are defined or how to best handle classes whose role is just to hold data. We then continue with how to handle static fields and functions in Kotlin and how to apply the Singleton pattern, with the help of one handy keyword: object. We’ll see how Kotlin helps us model our classes better, how it differentiates between a property of a class and an action the class can do. Finally, we’ll learn how to execute code only in the context of a specific object with the scope functions.", - listOf( - Markup(MarkupType.Code, 245, 251), - Markup(MarkupType.Code, 359, 365), - Markup( - MarkupType.Link, - 4, - 25, - "https://codelabs.developers.google.com/codelabs/java-to-kotlin/#0" - ) - ) - ), - Paragraph( - ParagraphType.Text, - "Thanks to Walmyr Carvalho and Nelson Glauber for translating the codelab in Brazilian Portuguese!", - listOf( - Markup( - MarkupType.Link, - 21, - 42, - "https://codelabs.developers.google.com/codelabs/java-to-kotlin/#0" - ) - ) - ), - Paragraph( - ParagraphType.Text, - "", - listOf( - Markup( - MarkupType.Link, - 76, - 96, - "https://codelabs.developers.google.com/codelabs/java-to-kotlin-pt-br/#0" - ) - ) +val paragraphsPost2 = + listOf( + Paragraph( + ParagraphType.Text, + "Dagger is a popular Dependency Injection framework commonly used in Android. It provides fully static and compile-time dependencies addressing many of the development and performance issues that have reflection-based solutions.", + listOf( + Markup( + MarkupType.Link, + 0, + 6, + "https://dagger.dev/", + ), + ), + ), + Paragraph( + ParagraphType.Text, + "This month, a new tutorial was released to help you better understand how it works. This article focuses on using Dagger with Kotlin, including best practices to optimize your build time and gotchas you might encounter.", + listOf( + Markup( + MarkupType.Link, + 14, + 26, + "https://dagger.dev/tutorial/", + ), + Markup(MarkupType.Bold, 114, 132), + Markup(MarkupType.Bold, 144, 159), + Markup(MarkupType.Bold, 191, 198), + ), + ), + Paragraph( + ParagraphType.Text, + "Dagger is implemented using Java’s annotations model and annotations in Kotlin are not always directly parallel with how equivalent Java code would be written. This post will highlight areas where they differ and how you can use Dagger with Kotlin without having a headache.", + ), + Paragraph( + ParagraphType.Text, + "This post was inspired by some of the suggestions in this Dagger issue that goes through best practices and pain points of Dagger in Kotlin. Thanks to all of the contributors that commented there!", + listOf( + Markup( + MarkupType.Link, + 58, + 70, + "https://github.com/google/dagger/issues/900", + ), + ), + ), + Paragraph( + ParagraphType.Header, + "kapt build improvements", + ), + Paragraph( + ParagraphType.Text, + "To improve your build time, Dagger added support for gradle’s incremental annotation processing in v2.18! This is enabled by default in Dagger v2.24. In case you’re using a lower version, you need to add a few lines of code (as shown below) if you want to benefit from it.", + listOf( + Markup( + MarkupType.Link, + 99, + 104, + "https://github.com/google/dagger/releases/tag/dagger-2.18", + ), + Markup( + MarkupType.Link, + 143, + 148, + "https://github.com/google/dagger/releases/tag/dagger-2.24", + ), + Markup(MarkupType.Bold, 53, 95), + ), + ), + Paragraph( + ParagraphType.Text, + "Also, you can tell Dagger not to format the generated code. This option was added in Dagger v2.18 and it’s the default behavior (doesn’t generate formatted code) in v2.23. If you’re using a lower version, disable code formatting to improve your build time (see code below).", + listOf( + Markup( + MarkupType.Link, + 92, + 97, + "https://github.com/google/dagger/releases/tag/dagger-2.18", + ), + Markup( + MarkupType.Link, + 165, + 170, + "https://github.com/google/dagger/releases/tag/dagger-2.23", + ), + ), + ), + Paragraph( + ParagraphType.Text, + "Include these compiler arguments in your build.gradle file to make Dagger more performant at build time:", + listOf(Markup(MarkupType.Code, 41, 53)), + ), + Paragraph( + ParagraphType.Text, + "Alternatively, if you use Kotlin DSL script files, include them like this in the build.gradle.kts file of the modules that use Dagger:", + listOf(Markup(MarkupType.Code, 81, 97)), + ), + Paragraph( + ParagraphType.Text, + "Qualifiers for field attributes", + ), + Paragraph( + ParagraphType.Text, + "", + listOf(Markup(MarkupType.Link, 0, 0)), + ), + Paragraph( + ParagraphType.Text, + "When an annotation is placed on a property in Kotlin, it’s not clear whether Java will see that annotation on the field of the property or the method for that property. Setting the field: prefix on the annotation ensures that the qualifier ends up in the right place (See documentation for more details).", + listOf( + Markup(MarkupType.Code, 181, 187), + Markup( + MarkupType.Link, + 268, + 285, + "http://frogermcs.github.io/dependency-injection-with-dagger-2-custom-scopes/", + ), + Markup(MarkupType.Italic, 114, 119), + Markup(MarkupType.Italic, 143, 149), + ), + ), + Paragraph( + ParagraphType.Text, + "✅ The way to apply qualifiers on an injected field is:", + ), + Paragraph( + ParagraphType.CodeBlock, + "@Inject @field:MinimumBalance lateinit var minimumBalance: BigDecimal", + listOf(Markup(MarkupType.Bold, 8, 29)), + ), + Paragraph( + ParagraphType.Text, + "❌ As opposed to:", + ), + Paragraph( + ParagraphType.CodeBlock, + """ + @Inject @MinimumBalance lateinit var minimumBalance: BigDecimal + // @MinimumBalance is ignored! + """.trimIndent(), + listOf(Markup(MarkupType.Bold, 65, 95)), + ), + Paragraph( + ParagraphType.Text, + "Forgetting to add field: could lead to injecting the wrong object if there’s an unqualified instance of that type available in the Dagger graph.", + listOf(Markup(MarkupType.Code, 18, 24)), + ), + Paragraph( + ParagraphType.Header, + "Static @Provides functions optimization", + ), + Paragraph( + ParagraphType.Text, + "Dagger’s generated code will be more performant if @Provides methods are static. To achieve this in Kotlin, use a Kotlin object instead of a class and annotate your methods with @JvmStatic. This is a best practice that you should follow as much as possible.", + listOf( + Markup(MarkupType.Code, 51, 60), + Markup(MarkupType.Code, 73, 79), + Markup(MarkupType.Code, 121, 127), + Markup(MarkupType.Code, 141, 146), + Markup(MarkupType.Code, 178, 188), + Markup(MarkupType.Bold, 200, 213), + Markup(MarkupType.Italic, 200, 213), + ), + ), + Paragraph( + ParagraphType.Text, + "In case you need an abstract method, you’ll need to add the @JvmStatic method to a companion object and annotate it with @Module too.", + listOf( + Markup(MarkupType.Code, 60, 70), + Markup(MarkupType.Code, 121, 128), + ), + ), + Paragraph( + ParagraphType.Text, + "Alternatively, you can extract the object module out and include it in the abstract one:", + ), + Paragraph( + ParagraphType.Header, + "Injecting Generics", + ), + Paragraph( + ParagraphType.Text, + "Kotlin compiles generics with wildcards to make Kotlin APIs work with Java. These are generated when a type appears as a parameter (more info here) or as fields. For example, a Kotlin List parameter shows up as List in Java.", + listOf( + Markup(MarkupType.Code, 184, 193), + Markup(MarkupType.Code, 216, 233), + Markup( + MarkupType.Link, + 132, + 146, + "https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#variant-generics", + ), + ), + ), + Paragraph( + ParagraphType.Text, + "This causes problems with Dagger because it expects an exact (aka invariant) type match. Using @JvmSuppressWildcards will ensure that Dagger sees the type without wildcards.", + listOf( + Markup(MarkupType.Code, 95, 116), + Markup( + MarkupType.Link, + 66, + 75, + "https://en.wikipedia.org/wiki/Class_invariant", + ), + Markup( + MarkupType.Link, + 96, + 116, + "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-suppress-wildcards/index.html", + ), + ), + ), + Paragraph( + ParagraphType.Text, + "This is a common issue when you inject collections using Dagger’s multibinding feature, for example:", + listOf( + Markup( + MarkupType.Link, + 57, + 86, + "https://dagger.dev/multibindings.html", + ), + ), + ), + Paragraph( + ParagraphType.CodeBlock, + """ + class MyVMFactory @Inject constructor( + private val vmMap: Map> + ) { + ... + } + """.trimIndent(), + listOf(Markup(MarkupType.Bold, 72, 93)), + ), + Paragraph( + ParagraphType.Header, + "Inline method bodies", + ), + Paragraph( + ParagraphType.Text, + "Dagger determines the types that are configured by @Provides methods by inspecting the return type. Specifying the return type in Kotlin functions is optional and even the IDE sometimes encourages you to refactor your code to have inline method bodies that hide the return type declaration.", + listOf(Markup(MarkupType.Code, 51, 60)), + ), + Paragraph( + ParagraphType.Text, + "This can lead to bugs if the inferred type is different from the one you meant. Let’s see some examples:", + ), + Paragraph( + ParagraphType.Text, + "If you want to add a specific type to the graph, inlining works as expected. See the different ways to do the same in Kotlin:", + ), + Paragraph( + ParagraphType.Text, + "If you want to provide an implementation of an interface, then you must explicitly specify the return type. Not doing it can lead to problems and bugs:", + ), + Paragraph( + ParagraphType.Text, + "Dagger mostly works with Kotlin out of the box. However, you have to watch out for a few things just to make sure you’re doing what you really mean to do: @field: for qualifiers on field attributes, inline method bodies, and @JvmSuppressWildcards when injecting collections.", + listOf( + Markup(MarkupType.Code, 155, 162), + Markup(MarkupType.Code, 225, 246), + ), + ), + Paragraph( + ParagraphType.Text, + "Dagger optimizations come with no cost, add them and follow best practices to improve your build time: enabling incremental annotation processing, disabling formatting and using static @Provides methods in your Dagger modules.", + listOf( + Markup( + MarkupType.Code, + 185, + 194, + ), + ), + ), ) -) -val paragraphsPost4 = listOf( - Paragraph( - ParagraphType.Text, - "TL;DR: Expose resource IDs from ViewModels to avoid showing obsolete data." - ), - Paragraph( - ParagraphType.Text, - "In a ViewModel, if you’re exposing data coming from resources (strings, drawables, colors…), you have to take into account that ViewModel objects ignore configuration changes such as locale changes. When the user changes their locale, activities are recreated but the ViewModel objects are not.", - listOf( - Markup( - MarkupType.Bold, - 183, - 197 - ) - ) - ), - Paragraph( - ParagraphType.Text, - "AndroidViewModel is a subclass of ViewModel that is aware of the Application context. However, having access to a context can be dangerous if you’re not observing or reacting to the lifecycle of that context. The recommended practice is to avoid dealing with objects that have a lifecycle in ViewModels.", - listOf( - Markup(MarkupType.Code, 0, 16), - Markup(MarkupType.Code, 34, 43), - Markup(MarkupType.Bold, 209, 303) - ) - ), - Paragraph( - ParagraphType.Text, - "Let’s look at an example based on this issue in the tracker: Updating ViewModel on system locale change.", - listOf( - Markup( - MarkupType.Link, - 61, - 103, - "https://issuetracker.google.com/issues/111961971" - ), - Markup(MarkupType.Italic, 61, 104) - ) - ), - Paragraph( - ParagraphType.Text, - "The problem is that the string is resolved in the constructor only once. If there’s a locale change, the ViewModel won’t be recreated. This will result in our app showing obsolete data and therefore being only partially localized.", - listOf(Markup(MarkupType.Bold, 73, 133)) - ), - Paragraph( - ParagraphType.Text, - "As Sergey points out in the comments to the issue, the recommended approach is to expose the ID of the resource you want to load and do so in the view. As the view (activity, fragment, etc.) is lifecycle-aware it will be recreated after a configuration change so the resource will be reloaded correctly.", - listOf( - Markup( - MarkupType.Link, - 3, - 9, - "https://twitter.com/ZelenetS" - ), - Markup( - MarkupType.Link, - 28, - 36, - "https://issuetracker.google.com/issues/111961971#comment2" - ), - Markup(MarkupType.Bold, 82, 150) - ) - ), - Paragraph( - ParagraphType.Text, - "Even if you don’t plan to localize your app, it makes testing much easier and cleans up your ViewModel objects so there’s no reason not to future-proof." - ), - Paragraph( - ParagraphType.Text, - "We fixed this issue in the android-architecture repository in the Java and Kotlin branches and we offloaded resource loading to the Data Binding layout.", - listOf( - Markup( - MarkupType.Link, - 66, - 70, - "https://github.com/googlesamples/android-architecture/pull/631" - ), - Markup( - MarkupType.Link, - 75, - 81, - "https://github.com/googlesamples/android-architecture/pull/635" - ), - Markup( - MarkupType.Link, - 128, - 151, - "https://github.com/googlesamples/android-architecture/pull/635/files#diff-7eb5d85ec3ea4e05ecddb7dc8ae20aa1R62" - ) - ) +val paragraphsPost3 = + listOf( + Paragraph( + ParagraphType.Text, + "Learn how to get started converting Java Programming Language code to Kotlin, making it more idiomatic and avoid common pitfalls, by following our new Refactoring to Kotlin codelab, available in English \uD83C\uDDEC\uD83C\uDDE7, Chinese \uD83C\uDDE8\uD83C\uDDF3 and Brazilian Portuguese \uD83C\uDDE7\uD83C\uDDF7.", + listOf( + Markup( + MarkupType.Link, + 151, + 172, + "https://codelabs.developers.google.com/codelabs/java-to-kotlin/#0", + ), + Markup( + MarkupType.Link, + 209, + 216, + "https://clmirror.storage.googleapis.com/codelabs/java-to-kotlin-zh/index.html#0", + ), + Markup( + MarkupType.Link, + 226, + 246, + "https://codelabs.developers.google.com/codelabs/java-to-kotlin-pt-br/#0", + ), + ), + ), + Paragraph( + ParagraphType.Text, + "When you first get started writing Kotlin code, you tend to follow Java Programming Language idioms. The automatic converter, part of both Android Studio and Intellij IDEA, can do a pretty good job of automatically refactoring your code, but sometimes, it needs a little help. This is where our new Refactoring to Kotlin codelab comes in.", + listOf( + Markup( + MarkupType.Link, + 105, + 124, + "https://www.jetbrains.com/help/idea/converting-a-java-file-to-kotlin-file.html", + ), + ), + ), + Paragraph( + ParagraphType.Text, + "We’ll take two classes (a User and a Repository) in Java Programming Language and convert them to Kotlin, check out what the automatic converter did and why. Then we go to the next level — make it idiomatic, teaching best practices and useful tips along the way.", + listOf( + Markup(MarkupType.Code, 26, 30), + Markup(MarkupType.Code, 37, 47), + ), + ), + Paragraph( + ParagraphType.Text, + "The Refactoring to Kotlin codelab starts with basic topics — understand how nullability is declared in Kotlin, what types of equality are defined or how to best handle classes whose role is just to hold data. We then continue with how to handle static fields and functions in Kotlin and how to apply the Singleton pattern, with the help of one handy keyword: object. We’ll see how Kotlin helps us model our classes better, how it differentiates between a property of a class and an action the class can do. Finally, we’ll learn how to execute code only in the context of a specific object with the scope functions.", + listOf( + Markup(MarkupType.Code, 245, 251), + Markup(MarkupType.Code, 359, 365), + Markup( + MarkupType.Link, + 4, + 25, + "https://codelabs.developers.google.com/codelabs/java-to-kotlin/#0", + ), + ), + ), + Paragraph( + ParagraphType.Text, + "Thanks to Walmyr Carvalho and Nelson Glauber for translating the codelab in Brazilian Portuguese!", + listOf( + Markup( + MarkupType.Link, + 21, + 42, + "https://codelabs.developers.google.com/codelabs/java-to-kotlin/#0", + ), + ), + ), + Paragraph( + ParagraphType.Text, + "", + listOf( + Markup( + MarkupType.Link, + 76, + 96, + "https://codelabs.developers.google.com/codelabs/java-to-kotlin-pt-br/#0", + ), + ), + ), ) -) -val paragraphsPost5 = listOf( - Paragraph( - ParagraphType.Text, - "Working with collections is a common task and the Kotlin Standard Library offers many great utility functions. It also offers two ways of working with collections based on how they’re evaluated: eagerly — with Collections, and lazily — with Sequences. Continue reading to find out what’s the difference between the two, which one you should use and when, and what the performance implications of each are.", - listOf( - Markup(MarkupType.Code, 210, 220), - Markup(MarkupType.Code, 241, 249), - Markup( - MarkupType.Link, - 210, - 221, - "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/index.html" - ), - Markup( - MarkupType.Link, - 241, - 250, - "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/index.html" - ), - Markup(MarkupType.Bold, 130, 134), - Markup(MarkupType.Bold, 195, 202), - Markup(MarkupType.Bold, 227, 233), - Markup(MarkupType.Italic, 130, 134) - ) - ), - Paragraph( - ParagraphType.Header, - "Collections vs sequences" - ), - Paragraph( - ParagraphType.Text, - "The difference between eager and lazy evaluation lies in when each transformation on the collection is performed.", - listOf( - Markup( - MarkupType.Italic, - 57, - 61 - ) - ) - ), - Paragraph( - ParagraphType.Text, - "Collections are eagerly evaluated — each operation is performed when it’s called and the result of the operation is stored in a new collection. The transformations on collections are inline functions. For example, looking at how map is implemented, we can see that it’s an inline function, that creates a new ArrayList:", - listOf( - Markup(MarkupType.Code, 229, 232), - Markup(MarkupType.Code, 273, 279), - Markup(MarkupType.Code, 309, 318), - Markup( - MarkupType.Link, - 183, - 199, - "https://kotlinlang.org/docs/reference/inline-functions.html" - ), - Markup( - MarkupType.Link, - 229, - 232, - "https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/common/src/generated/_Collections.kt#L1312" - ), - Markup(MarkupType.Bold, 0, 12), - Markup(MarkupType.Italic, 16, 23) - ) - ), - Paragraph( - ParagraphType.CodeBlock, - "public inline fun Iterable.map(transform: (T) -> R): List {\n" + - " return mapTo(ArrayList(collectionSizeOrDefault(10)), transform)\n" + - "}", - listOf( - Markup(MarkupType.Bold, 7, 13), - Markup(MarkupType.Bold, 88, 97) - ) - ), - Paragraph( - ParagraphType.Text, - "Sequences are lazily evaluated. They have two types of operations: intermediate and terminal. Intermediate operations are not performed on the spot; they’re just stored. Only when a terminal operation is called, the intermediate operations are triggered on each element in a row and finally, the terminal operation is applied. Intermediate operations (like map, distinct, groupBy etc) return another sequence whereas terminal operations (like first, toList, count etc) don’t.", - listOf( - Markup(MarkupType.Code, 357, 360), - Markup(MarkupType.Code, 362, 370), - Markup(MarkupType.Code, 372, 379), - Markup(MarkupType.Code, 443, 448), - Markup(MarkupType.Code, 450, 456), - Markup(MarkupType.Code, 458, 463), - Markup(MarkupType.Bold, 0, 9), - Markup(MarkupType.Bold, 67, 79), - Markup(MarkupType.Bold, 84, 92), - Markup(MarkupType.Bold, 254, 269), - Markup(MarkupType.Italic, 14, 20) - ) - ), - Paragraph( - ParagraphType.Text, - "Sequences don’t hold a reference to the items of the collection. They’re created based on the iterator of the original collection and keep a reference to all the intermediate operations that need to be performed." - ), - Paragraph( - ParagraphType.Text, - "Unlike transformations on collections, intermediate transformations on sequences are not inline functions — inline functions cannot be stored and sequences need to store them. Looking at how an intermediate operation like map is implemented, we can see that the transform function is kept in a new instance of a Sequence:", - listOf( - Markup(MarkupType.Code, 222, 225), - Markup(MarkupType.Code, 312, 320), - Markup( - MarkupType.Link, - 222, - 225, - "https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/common/src/generated/_Sequences.kt#L860" - ) - ) - ), - Paragraph( - ParagraphType.CodeBlock, - "public fun Sequence.map(transform: (T) -> R): Sequence{ \n" + - " return TransformingSequence(this, transform)\n" + - "}", - listOf(Markup(MarkupType.Bold, 85, 105)) - ), - Paragraph( - ParagraphType.Text, - "A terminal operation, like first, iterates through the elements of the sequence until the predicate condition is matched.", - listOf( - Markup(MarkupType.Code, 27, 32), - Markup( - MarkupType.Link, - 27, - 32, - "https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/common/src/generated/_Sequences.kt#L117" - ) - ) - ), - Paragraph( - ParagraphType.CodeBlock, - "public inline fun Sequence.first(predicate: (T) -> Boolean): T {\n" + - " for (element in this) if (predicate(element)) return element\n" + - " throw NoSuchElementException(“Sequence contains no element matching the predicate.”)\n" + - "}" - ), - Paragraph( - ParagraphType.Text, - "If we look at how a sequence like TransformingSequence (used in the map above) is implemented, we’ll see that when next is called on the sequence iterator, the transformation stored is also applied.", - listOf( - Markup(MarkupType.Code, 34, 54), - Markup(MarkupType.Code, 68, 71) - ) - ), - Paragraph( - ParagraphType.CodeBlock, - "internal class TransformingIndexedSequence \n" + - "constructor(private val sequence: Sequence, private val transformer: (Int, T) -> R) : Sequence {", - listOf( - Markup( - MarkupType.Bold, - 109, - 120 - ) - ) - ), - Paragraph( - ParagraphType.CodeBlock, - "override fun iterator(): Iterator = object : Iterator {\n" + - " …\n" + - " override fun next(): R {\n" + - " return transformer(checkIndexOverflow(index++), iterator.next())\n" + - " }\n" + - " …\n" + - "}", - listOf( - Markup(MarkupType.Bold, 83, 89), - Markup(MarkupType.Bold, 107, 118) - ) - ), - Paragraph( - ParagraphType.Text, - "Independent on whether you’re using collections or sequences, the Kotlin Standard Library offers quite a wide range of operations for both, like find, filter, groupBy and others. Make sure you check them out, before implementing your own version of these.", - listOf( - Markup(MarkupType.Code, 145, 149), - Markup(MarkupType.Code, 151, 157), - Markup(MarkupType.Code, 159, 166), - Markup( - MarkupType.Link, - 193, - 207, - "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/#functions" - ) - ) - ), - Paragraph( - ParagraphType.Header, - "Collections and sequences" - ), - Paragraph( - ParagraphType.Text, - "Let’s say that we have a list of objects of different shapes. We want to make the shapes yellow and then take the first square shape." - ), - Paragraph( - ParagraphType.Text, - "Let’s see how and when each operation is applied for collections and when for sequences" - ), - Paragraph( - ParagraphType.Subhead, - "Collections" - ), - Paragraph( - ParagraphType.Text, - "map is called — a new ArrayList is created. We iterate through all items of the initial collection, transform it by copying the original object and changing the color, then add it to the new list.", - listOf(Markup(MarkupType.Code, 0, 3)) - ), - Paragraph( - ParagraphType.Text, - "first is called — we iterate through each item until the first square is found", - listOf(Markup(MarkupType.Code, 0, 5)) - ), - Paragraph( - ParagraphType.Subhead, - "Sequences" - ), - Paragraph( - ParagraphType.Bullet, - "asSequence — a sequence is created based on the Iterator of the original collection", - listOf(Markup(MarkupType.Code, 0, 10)) - ), - Paragraph( - ParagraphType.Bullet, - "map is called — the transformation is added to the list of operations needed to be performed by the sequence but the operation is NOT performed", - listOf( - Markup(MarkupType.Code, 0, 3), - Markup(MarkupType.Bold, 130, 133) - ) - ), - Paragraph( - ParagraphType.Bullet, - "first is called — this is a terminal operation, so, all the intermediate operations are triggered, on each element of the collection. We iterate through the initial collection applying map and then first on each of them. Since the condition from first is satisfied by the 2nd element, then we no longer apply the map on the rest of the collection.", - listOf(Markup(MarkupType.Code, 0, 5)) - ), +val paragraphsPost4 = + listOf( + Paragraph( + ParagraphType.Text, + "TL;DR: Expose resource IDs from ViewModels to avoid showing obsolete data.", + ), + Paragraph( + ParagraphType.Text, + "In a ViewModel, if you’re exposing data coming from resources (strings, drawables, colors…), you have to take into account that ViewModel objects ignore configuration changes such as locale changes. When the user changes their locale, activities are recreated but the ViewModel objects are not.", + listOf( + Markup( + MarkupType.Bold, + 183, + 197, + ), + ), + ), + Paragraph( + ParagraphType.Text, + "AndroidViewModel is a subclass of ViewModel that is aware of the Application context. However, having access to a context can be dangerous if you’re not observing or reacting to the lifecycle of that context. The recommended practice is to avoid dealing with objects that have a lifecycle in ViewModels.", + listOf( + Markup(MarkupType.Code, 0, 16), + Markup(MarkupType.Code, 34, 43), + Markup(MarkupType.Bold, 209, 303), + ), + ), + Paragraph( + ParagraphType.Text, + "Let’s look at an example based on this issue in the tracker: Updating ViewModel on system locale change.", + listOf( + Markup( + MarkupType.Link, + 61, + 103, + "https://issuetracker.google.com/issues/111961971", + ), + Markup(MarkupType.Italic, 61, 104), + ), + ), + Paragraph( + ParagraphType.Text, + "The problem is that the string is resolved in the constructor only once. If there’s a locale change, the ViewModel won’t be recreated. This will result in our app showing obsolete data and therefore being only partially localized.", + listOf(Markup(MarkupType.Bold, 73, 133)), + ), + Paragraph( + ParagraphType.Text, + "As Sergey points out in the comments to the issue, the recommended approach is to expose the ID of the resource you want to load and do so in the view. As the view (activity, fragment, etc.) is lifecycle-aware it will be recreated after a configuration change so the resource will be reloaded correctly.", + listOf( + Markup( + MarkupType.Link, + 3, + 9, + "https://twitter.com/ZelenetS", + ), + Markup( + MarkupType.Link, + 28, + 36, + "https://issuetracker.google.com/issues/111961971#comment2", + ), + Markup(MarkupType.Bold, 82, 150), + ), + ), + Paragraph( + ParagraphType.Text, + "Even if you don’t plan to localize your app, it makes testing much easier and cleans up your ViewModel objects so there’s no reason not to future-proof.", + ), + Paragraph( + ParagraphType.Text, + "We fixed this issue in the android-architecture repository in the Java and Kotlin branches and we offloaded resource loading to the Data Binding layout.", + listOf( + Markup( + MarkupType.Link, + 66, + 70, + "https://github.com/googlesamples/android-architecture/pull/631", + ), + Markup( + MarkupType.Link, + 75, + 81, + "https://github.com/googlesamples/android-architecture/pull/635", + ), + Markup( + MarkupType.Link, + 128, + 151, + "https://github.com/googlesamples/android-architecture/pull/635/files#diff-7eb5d85ec3ea4e05ecddb7dc8ae20aa1R62", + ), + ), + ), + ) - Paragraph( - ParagraphType.Text, - "When working with sequences no intermediate collection is created and since items are evaluated one by one, map is only performed on some of the inputs." - ), - Paragraph( - ParagraphType.Header, - "Performance" - ), - Paragraph( - ParagraphType.Subhead, - "Order of transformations" - ), - Paragraph( - ParagraphType.Text, - "Independent of whether you’re using collections or sequences, the order of transformations matters. In the example above, first doesn’t need to happen after map since it’s not a consequence of the map transformation. If we reverse the order of our business logic and call first on the collection and then transform the result, then we only create one new object — the yellow square. When using sequences — we avoid creating 2 new objects, when using collections, we avoid creating an entire new list.", - listOf( - Markup(MarkupType.Code, 122, 127), - Markup(MarkupType.Code, 157, 160), - Markup(MarkupType.Code, 197, 200) - ) - ), - Paragraph( - ParagraphType.Text, - "Because terminal operations can finish processing early, and intermediate operations are evaluated lazily, sequences can, in some cases, help you avoid doing unnecessary work compared to collections. Make sure you always check the order of the transformations and the dependencies between them!" - ), - Paragraph( - ParagraphType.Subhead, - "Inlining and large data sets consequences" - ), - Paragraph( - ParagraphType.Text, - "Collection operations use inline functions, so the bytecode of the operation, together with the bytecode of the lambda passed to it will be inlined. Sequences don’t use inline functions, therefore, new Function objects are created for each operation.", - listOf( - Markup( - MarkupType.Code, - 202, - 210 - ) - ) - ), - Paragraph( - ParagraphType.Text, - "On the other hand, collections create a new list for every transformation while sequences just keep a reference to the transformation function." - ), - Paragraph( - ParagraphType.Text, - "When working with small collections, with 1–2 operators, these differences don’t have big implications so working with collections should be ok. But, when working with large lists the intermediate collection creation can become expensive; in such cases, use sequences.", - listOf( - Markup(MarkupType.Bold, 18, 35), - Markup(MarkupType.Bold, 119, 130), - Markup(MarkupType.Bold, 168, 179), - Markup(MarkupType.Bold, 258, 267) - ) - ), - Paragraph( - ParagraphType.Text, - "Unfortunately, I’m not aware of any benchmarking study done that would help us get a better understanding on how the performance of collections vs sequences is affected with different sizes of collections or operation chains." - ), - Paragraph( - ParagraphType.Text, - "Collections eagerly evaluate your data while sequences do so lazily. Depending on the size of your data, pick the one that fits best: collections — for small lists or sequences — for larger ones, and pay attention to the order of the transformations." +val paragraphsPost5 = + listOf( + Paragraph( + ParagraphType.Text, + "Working with collections is a common task and the Kotlin Standard Library offers many great utility functions. It also offers two ways of working with collections based on how they’re evaluated: eagerly — with Collections, and lazily — with Sequences. Continue reading to find out what’s the difference between the two, which one you should use and when, and what the performance implications of each are.", + listOf( + Markup(MarkupType.Code, 210, 220), + Markup(MarkupType.Code, 241, 249), + Markup( + MarkupType.Link, + 210, + 221, + "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/index.html", + ), + Markup( + MarkupType.Link, + 241, + 250, + "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/index.html", + ), + Markup(MarkupType.Bold, 130, 134), + Markup(MarkupType.Bold, 195, 202), + Markup(MarkupType.Bold, 227, 233), + Markup(MarkupType.Italic, 130, 134), + ), + ), + Paragraph( + ParagraphType.Header, + "Collections vs sequences", + ), + Paragraph( + ParagraphType.Text, + "The difference between eager and lazy evaluation lies in when each transformation on the collection is performed.", + listOf( + Markup( + MarkupType.Italic, + 57, + 61, + ), + ), + ), + Paragraph( + ParagraphType.Text, + "Collections are eagerly evaluated — each operation is performed when it’s called and the result of the operation is stored in a new collection. The transformations on collections are inline functions. For example, looking at how map is implemented, we can see that it’s an inline function, that creates a new ArrayList:", + listOf( + Markup(MarkupType.Code, 229, 232), + Markup(MarkupType.Code, 273, 279), + Markup(MarkupType.Code, 309, 318), + Markup( + MarkupType.Link, + 183, + 199, + "https://kotlinlang.org/docs/reference/inline-functions.html", + ), + Markup( + MarkupType.Link, + 229, + 232, + "https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/common/src/generated/_Collections.kt#L1312", + ), + Markup(MarkupType.Bold, 0, 12), + Markup(MarkupType.Italic, 16, 23), + ), + ), + Paragraph( + ParagraphType.CodeBlock, + "public inline fun Iterable.map(transform: (T) -> R): List {\n" + + " return mapTo(ArrayList(collectionSizeOrDefault(10)), transform)\n" + + "}", + listOf( + Markup(MarkupType.Bold, 7, 13), + Markup(MarkupType.Bold, 88, 97), + ), + ), + Paragraph( + ParagraphType.Text, + "Sequences are lazily evaluated. They have two types of operations: intermediate and terminal. Intermediate operations are not performed on the spot; they’re just stored. Only when a terminal operation is called, the intermediate operations are triggered on each element in a row and finally, the terminal operation is applied. Intermediate operations (like map, distinct, groupBy etc) return another sequence whereas terminal operations (like first, toList, count etc) don’t.", + listOf( + Markup(MarkupType.Code, 357, 360), + Markup(MarkupType.Code, 362, 370), + Markup(MarkupType.Code, 372, 379), + Markup(MarkupType.Code, 443, 448), + Markup(MarkupType.Code, 450, 456), + Markup(MarkupType.Code, 458, 463), + Markup(MarkupType.Bold, 0, 9), + Markup(MarkupType.Bold, 67, 79), + Markup(MarkupType.Bold, 84, 92), + Markup(MarkupType.Bold, 254, 269), + Markup(MarkupType.Italic, 14, 20), + ), + ), + Paragraph( + ParagraphType.Text, + "Sequences don’t hold a reference to the items of the collection. They’re created based on the iterator of the original collection and keep a reference to all the intermediate operations that need to be performed.", + ), + Paragraph( + ParagraphType.Text, + "Unlike transformations on collections, intermediate transformations on sequences are not inline functions — inline functions cannot be stored and sequences need to store them. Looking at how an intermediate operation like map is implemented, we can see that the transform function is kept in a new instance of a Sequence:", + listOf( + Markup(MarkupType.Code, 222, 225), + Markup(MarkupType.Code, 312, 320), + Markup( + MarkupType.Link, + 222, + 225, + "https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/common/src/generated/_Sequences.kt#L860", + ), + ), + ), + Paragraph( + ParagraphType.CodeBlock, + "public fun Sequence.map(transform: (T) -> R): Sequence{ \n" + + " return TransformingSequence(this, transform)\n" + + "}", + listOf(Markup(MarkupType.Bold, 85, 105)), + ), + Paragraph( + ParagraphType.Text, + "A terminal operation, like first, iterates through the elements of the sequence until the predicate condition is matched.", + listOf( + Markup(MarkupType.Code, 27, 32), + Markup( + MarkupType.Link, + 27, + 32, + "https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/common/src/generated/_Sequences.kt#L117", + ), + ), + ), + Paragraph( + ParagraphType.CodeBlock, + "public inline fun Sequence.first(predicate: (T) -> Boolean): T {\n" + + " for (element in this) if (predicate(element)) return element\n" + + " throw NoSuchElementException(“Sequence contains no element matching the predicate.”)\n" + + "}", + ), + Paragraph( + ParagraphType.Text, + "If we look at how a sequence like TransformingSequence (used in the map above) is implemented, we’ll see that when next is called on the sequence iterator, the transformation stored is also applied.", + listOf( + Markup(MarkupType.Code, 34, 54), + Markup(MarkupType.Code, 68, 71), + ), + ), + Paragraph( + ParagraphType.CodeBlock, + "internal class TransformingIndexedSequence \n" + + "constructor(private val sequence: Sequence, private val transformer: (Int, T) -> R) : Sequence {", + listOf( + Markup( + MarkupType.Bold, + 109, + 120, + ), + ), + ), + Paragraph( + ParagraphType.CodeBlock, + "override fun iterator(): Iterator = object : Iterator {\n" + + " …\n" + + " override fun next(): R {\n" + + " return transformer(checkIndexOverflow(index++), iterator.next())\n" + + " }\n" + + " …\n" + + "}", + listOf( + Markup(MarkupType.Bold, 83, 89), + Markup(MarkupType.Bold, 107, 118), + ), + ), + Paragraph( + ParagraphType.Text, + "Independent on whether you’re using collections or sequences, the Kotlin Standard Library offers quite a wide range of operations for both, like find, filter, groupBy and others. Make sure you check them out, before implementing your own version of these.", + listOf( + Markup(MarkupType.Code, 145, 149), + Markup(MarkupType.Code, 151, 157), + Markup(MarkupType.Code, 159, 166), + Markup( + MarkupType.Link, + 193, + 207, + "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/#functions", + ), + ), + ), + Paragraph( + ParagraphType.Header, + "Collections and sequences", + ), + Paragraph( + ParagraphType.Text, + "Let’s say that we have a list of objects of different shapes. We want to make the shapes yellow and then take the first square shape.", + ), + Paragraph( + ParagraphType.Text, + "Let’s see how and when each operation is applied for collections and when for sequences", + ), + Paragraph( + ParagraphType.Subhead, + "Collections", + ), + Paragraph( + ParagraphType.Text, + "map is called — a new ArrayList is created. We iterate through all items of the initial collection, transform it by copying the original object and changing the color, then add it to the new list.", + listOf(Markup(MarkupType.Code, 0, 3)), + ), + Paragraph( + ParagraphType.Text, + "first is called — we iterate through each item until the first square is found", + listOf(Markup(MarkupType.Code, 0, 5)), + ), + Paragraph( + ParagraphType.Subhead, + "Sequences", + ), + Paragraph( + ParagraphType.Bullet, + "asSequence — a sequence is created based on the Iterator of the original collection", + listOf(Markup(MarkupType.Code, 0, 10)), + ), + Paragraph( + ParagraphType.Bullet, + "map is called — the transformation is added to the list of operations needed to be performed by the sequence but the operation is NOT performed", + listOf( + Markup(MarkupType.Code, 0, 3), + Markup(MarkupType.Bold, 130, 133), + ), + ), + Paragraph( + ParagraphType.Bullet, + "first is called — this is a terminal operation, so, all the intermediate operations are triggered, on each element of the collection. We iterate through the initial collection applying map and then first on each of them. Since the condition from first is satisfied by the 2nd element, then we no longer apply the map on the rest of the collection.", + listOf(Markup(MarkupType.Code, 0, 5)), + ), + Paragraph( + ParagraphType.Text, + "When working with sequences no intermediate collection is created and since items are evaluated one by one, map is only performed on some of the inputs.", + ), + Paragraph( + ParagraphType.Header, + "Performance", + ), + Paragraph( + ParagraphType.Subhead, + "Order of transformations", + ), + Paragraph( + ParagraphType.Text, + "Independent of whether you’re using collections or sequences, the order of transformations matters. In the example above, first doesn’t need to happen after map since it’s not a consequence of the map transformation. If we reverse the order of our business logic and call first on the collection and then transform the result, then we only create one new object — the yellow square. When using sequences — we avoid creating 2 new objects, when using collections, we avoid creating an entire new list.", + listOf( + Markup(MarkupType.Code, 122, 127), + Markup(MarkupType.Code, 157, 160), + Markup(MarkupType.Code, 197, 200), + ), + ), + Paragraph( + ParagraphType.Text, + "Because terminal operations can finish processing early, and intermediate operations are evaluated lazily, sequences can, in some cases, help you avoid doing unnecessary work compared to collections. Make sure you always check the order of the transformations and the dependencies between them!", + ), + Paragraph( + ParagraphType.Subhead, + "Inlining and large data sets consequences", + ), + Paragraph( + ParagraphType.Text, + "Collection operations use inline functions, so the bytecode of the operation, together with the bytecode of the lambda passed to it will be inlined. Sequences don’t use inline functions, therefore, new Function objects are created for each operation.", + listOf( + Markup( + MarkupType.Code, + 202, + 210, + ), + ), + ), + Paragraph( + ParagraphType.Text, + "On the other hand, collections create a new list for every transformation while sequences just keep a reference to the transformation function.", + ), + Paragraph( + ParagraphType.Text, + "When working with small collections, with 1–2 operators, these differences don’t have big implications so working with collections should be ok. But, when working with large lists the intermediate collection creation can become expensive; in such cases, use sequences.", + listOf( + Markup(MarkupType.Bold, 18, 35), + Markup(MarkupType.Bold, 119, 130), + Markup(MarkupType.Bold, 168, 179), + Markup(MarkupType.Bold, 258, 267), + ), + ), + Paragraph( + ParagraphType.Text, + "Unfortunately, I’m not aware of any benchmarking study done that would help us get a better understanding on how the performance of collections vs sequences is affected with different sizes of collections or operation chains.", + ), + Paragraph( + ParagraphType.Text, + "Collections eagerly evaluate your data while sequences do so lazily. Depending on the size of your data, pick the one that fits best: collections — for small lists or sequences — for larger ones, and pay attention to the order of the transformations.", + ), ) -) -val paragraphsPost6 = listOf( - Paragraph( - ParagraphType.Text, - "The Android Studio logo redesign caught the attention of the developer community since its sneak peek at the Android Developer Summit. We are thrilled to release the new Android Studio logo with the stable release of Flamingo. Now that the new logo is available to most Android Studio users, we can examine the design changes in greater detail and decode their meaning." - ), - Paragraph( - ParagraphType.Text, - "This case study offers a comprehensive overview of the design journey, from identifying the initial problem to the final outcome. It explores the critical brand elements that the team needed to consider and the tools used throughout the redesign process. This case study also delves into the various stages of design exploration, highlighting the efforts to create a modern logo while honoring the Android Studio brand's legacy." - ), - Paragraph( - ParagraphType.Header, - "Identifying the problem" - ), - Paragraph( - ParagraphType.Text, - "You told us the Android Studio logo looked a little weird and complicated. It doesn't shrink down well and it's way too similar to the emulator. We heard you!" - ), - Paragraph( - ParagraphType.Text, - "The Android Studio logo used between 2020 and 2022 was well-suited for print, but it posed challenges when used as an application icon. Its readability suffered when reduced to smaller sizes, and its similarity to the emulator caused confusion." - ), - Paragraph( - ParagraphType.Text, - "Additionally, the use of color alone to differentiate between Canary and Stable versions made it difficult for users with color vision deficiencies." - ), - Paragraph( - ParagraphType.Text, - "The redesign aimed to resolve these concerns by creating a logo that was easy to read, visually distinctive, and followed the OS guidelines when necessary, ensuring accessibility. The new design also maintained a connection with the Android logo family while honoring its legacy." - ), - Paragraph( - ParagraphType.Text, - "In this case study, we will delve into the version history and evolution of the Android Studio logo and how it has changed over the years." - ), - Paragraph( - ParagraphType.Header, - "A brief history of the Android Studio logo" - ), - Paragraph( - ParagraphType.Bullet, - "2013: The original Android Studio logo was a 3D robot that highlighted the gears and interworking of the bugdroid. At this time, the Android Emulator was the bugdroid.", - listOf( - Markup(MarkupType.Bold, 0, 5) - ) - ), - Paragraph( - ParagraphType.Bullet, - "2014: The Android Emulator merged to a flat mark but remained otherwise unchanged.", - listOf( - Markup(MarkupType.Bold, 0, 5) - ) - ), - Paragraph( - ParagraphType.Bullet, - "2014-2019: An updated Android Studio logo was introduced featuring an \"A\" compass in front of a green circle.", - listOf( - Markup(MarkupType.Bold, 0, 10) - ) - ), - Paragraph( - ParagraphType.Bullet, - "2019: In Canary 3.6, the color palette was updated to match Android 10.", - listOf( - Markup(MarkupType.Bold, 0, 5) - ) - ), - Paragraph( - ParagraphType.Bullet, - "2020-2022: With the release of Android Studio 4.1 Canary, the \"A\" compass was reduced to an abstract form placed in front of a blueprint. The Android head was also added, peeking over the top.", - listOf( - Markup(MarkupType.Bold, 0, 10) - ) - ), - Paragraph( - ParagraphType.Header, - "Understanding the Android brand elements" - ), - Paragraph( - ParagraphType.Text, - "When redesigning a logo, it's important to consider brand elements that unify products within an ecosystem. For the Android Developer ecosystem, the \"robot head\" is a key brand element, alongside the primaryAndroid green color. The secondary colors blue and navy, and tertiary colors like orange, can also be utilized for support." - ), - Paragraph( - ParagraphType.Header, - "Key objectives" - ), - Paragraph( - ParagraphType.Bullet, - "Iconography: use recognizable and appropriate symbols, such as compass \"A\" for Android Studio or a device for Android Emulator, to convey the purpose and functionality clearly and quickly.", - listOf( - Markup(MarkupType.Bold, 0, 12) - ) - ), - Paragraph( - ParagraphType.Bullet, - "Enhance recognition and scalability: the Android Studio and Android Emulator should prioritize legibility and scalability, ensuring that they can be easily recognized and understood even at smaller sizes.", - listOf( - Markup(MarkupType.Bold, 0, 36) - ) - ), - Paragraph( - ParagraphType.Bullet, - "Establish distinction: the Android Studio and Android Emulator need to be easily distinguishable, to avoid confusion.", - listOf( - Markup(MarkupType.Bold, 0, 22) - ) - ), - Paragraph( - ParagraphType.Bullet, - "Maintain brand consistency: the Android Studio and Android Emulator designs should be consistent with the overall branding and visual identity of the Android family, while still being distinctive.", - listOf( - Markup(MarkupType.Bold, 0, 27) - ) - ), - Paragraph( - ParagraphType.Bullet, - "Ensure accessibility: the logo should be accessible to all users, including those with visual impairments. This means using clear shapes, colors, and contrast.", - listOf( - Markup(MarkupType.Bold, 0, 21) - ) - ), - Paragraph( - ParagraphType.Bullet, - "Follow OS guidelines: the updated application icon must align with the Android visual language and conform to the guidelines of macOS, Windows, and Linux operating systems, ensuring consistency and coherence across all platforms.", - listOf( - Markup(MarkupType.Bold, 0, 21) - ) - ), - Paragraph( - ParagraphType.Bullet, - "Ensure versatility: the Android Studio logo should be versatile enough to work in a variety of sizes and contexts, such as on different devices and platforms.", - listOf( - Markup(MarkupType.Bold, 0, 20) - ) - ), - Paragraph( - ParagraphType.Text, - "Read More", - listOf( - Markup( - MarkupType.Link, - 0, - 9, - "https://android-developers.googleblog.com/2023/05/redesigning-android-studio-logo.html" - ) - ) +val paragraphsPost6 = + listOf( + Paragraph( + ParagraphType.Text, + "The Android Studio logo redesign caught the attention of the developer community since its sneak peek at the Android Developer Summit. We are thrilled to release the new Android Studio logo with the stable release of Flamingo. Now that the new logo is available to most Android Studio users, we can examine the design changes in greater detail and decode their meaning.", + ), + Paragraph( + ParagraphType.Text, + "This case study offers a comprehensive overview of the design journey, from identifying the initial problem to the final outcome. It explores the critical brand elements that the team needed to consider and the tools used throughout the redesign process. This case study also delves into the various stages of design exploration, highlighting the efforts to create a modern logo while honoring the Android Studio brand's legacy.", + ), + Paragraph( + ParagraphType.Header, + "Identifying the problem", + ), + Paragraph( + ParagraphType.Text, + "You told us the Android Studio logo looked a little weird and complicated. It doesn't shrink down well and it's way too similar to the emulator. We heard you!", + ), + Paragraph( + ParagraphType.Text, + "The Android Studio logo used between 2020 and 2022 was well-suited for print, but it posed challenges when used as an application icon. Its readability suffered when reduced to smaller sizes, and its similarity to the emulator caused confusion.", + ), + Paragraph( + ParagraphType.Text, + "Additionally, the use of color alone to differentiate between Canary and Stable versions made it difficult for users with color vision deficiencies.", + ), + Paragraph( + ParagraphType.Text, + "The redesign aimed to resolve these concerns by creating a logo that was easy to read, visually distinctive, and followed the OS guidelines when necessary, ensuring accessibility. The new design also maintained a connection with the Android logo family while honoring its legacy.", + ), + Paragraph( + ParagraphType.Text, + "In this case study, we will delve into the version history and evolution of the Android Studio logo and how it has changed over the years.", + ), + Paragraph( + ParagraphType.Header, + "A brief history of the Android Studio logo", + ), + Paragraph( + ParagraphType.Bullet, + "2013: The original Android Studio logo was a 3D robot that highlighted the gears and interworking of the bugdroid. At this time, the Android Emulator was the bugdroid.", + listOf( + Markup(MarkupType.Bold, 0, 5), + ), + ), + Paragraph( + ParagraphType.Bullet, + "2014: The Android Emulator merged to a flat mark but remained otherwise unchanged.", + listOf( + Markup(MarkupType.Bold, 0, 5), + ), + ), + Paragraph( + ParagraphType.Bullet, + "2014-2019: An updated Android Studio logo was introduced featuring an \"A\" compass in front of a green circle.", + listOf( + Markup(MarkupType.Bold, 0, 10), + ), + ), + Paragraph( + ParagraphType.Bullet, + "2019: In Canary 3.6, the color palette was updated to match Android 10.", + listOf( + Markup(MarkupType.Bold, 0, 5), + ), + ), + Paragraph( + ParagraphType.Bullet, + "2020-2022: With the release of Android Studio 4.1 Canary, the \"A\" compass was reduced to an abstract form placed in front of a blueprint. The Android head was also added, peeking over the top.", + listOf( + Markup(MarkupType.Bold, 0, 10), + ), + ), + Paragraph( + ParagraphType.Header, + "Understanding the Android brand elements", + ), + Paragraph( + ParagraphType.Text, + "When redesigning a logo, it's important to consider brand elements that unify products within an ecosystem. For the Android Developer ecosystem, the \"robot head\" is a key brand element, alongside the primaryAndroid green color. The secondary colors blue and navy, and tertiary colors like orange, can also be utilized for support.", + ), + Paragraph( + ParagraphType.Header, + "Key objectives", + ), + Paragraph( + ParagraphType.Bullet, + "Iconography: use recognizable and appropriate symbols, such as compass \"A\" for Android Studio or a device for Android Emulator, to convey the purpose and functionality clearly and quickly.", + listOf( + Markup(MarkupType.Bold, 0, 12), + ), + ), + Paragraph( + ParagraphType.Bullet, + "Enhance recognition and scalability: the Android Studio and Android Emulator should prioritize legibility and scalability, ensuring that they can be easily recognized and understood even at smaller sizes.", + listOf( + Markup(MarkupType.Bold, 0, 36), + ), + ), + Paragraph( + ParagraphType.Bullet, + "Establish distinction: the Android Studio and Android Emulator need to be easily distinguishable, to avoid confusion.", + listOf( + Markup(MarkupType.Bold, 0, 22), + ), + ), + Paragraph( + ParagraphType.Bullet, + "Maintain brand consistency: the Android Studio and Android Emulator designs should be consistent with the overall branding and visual identity of the Android family, while still being distinctive.", + listOf( + Markup(MarkupType.Bold, 0, 27), + ), + ), + Paragraph( + ParagraphType.Bullet, + "Ensure accessibility: the logo should be accessible to all users, including those with visual impairments. This means using clear shapes, colors, and contrast.", + listOf( + Markup(MarkupType.Bold, 0, 21), + ), + ), + Paragraph( + ParagraphType.Bullet, + "Follow OS guidelines: the updated application icon must align with the Android visual language and conform to the guidelines of macOS, Windows, and Linux operating systems, ensuring consistency and coherence across all platforms.", + listOf( + Markup(MarkupType.Bold, 0, 21), + ), + ), + Paragraph( + ParagraphType.Bullet, + "Ensure versatility: the Android Studio logo should be versatile enough to work in a variety of sizes and contexts, such as on different devices and platforms.", + listOf( + Markup(MarkupType.Bold, 0, 20), + ), + ), + Paragraph( + ParagraphType.Text, + "Read More", + listOf( + Markup( + MarkupType.Link, + 0, + 9, + "https://android-developers.googleblog.com/2023/05/redesigning-android-studio-logo.html", + ), + ), + ), ) -) -val post1 = Post( - id = "dc523f0ed25c", - title = "A Little Thing about Android Module Paths", - subtitle = "How to configure your module paths, instead of using Gradle’s default.", - url = "https://medium.com/androiddevelopers/gradle-path-configuration-dc523f0ed25c", - publication = publication, - metadata = Metadata( - author = pietro, - date = "August 02", - readTimeMinutes = 1 - ), - paragraphs = paragraphsPost1, - imageId = R.drawable.post_1, - imageThumbId = R.drawable.post_1_thumb -) +val post1 = + Post( + id = "dc523f0ed25c", + title = "A Little Thing about Android Module Paths", + subtitle = "How to configure your module paths, instead of using Gradle’s default.", + url = "https://medium.com/androiddevelopers/gradle-path-configuration-dc523f0ed25c", + publication = publication, + metadata = + Metadata( + author = pietro, + date = "August 02", + readTimeMinutes = 1, + ), + paragraphs = paragraphsPost1, + imageId = R.drawable.post_1, + imageThumbId = R.drawable.post_1_thumb, + ) -val post2 = Post( - id = "7446d8dfd7dc", - title = "Dagger in Kotlin: Gotchas and Optimizations", - subtitle = "Use Dagger in Kotlin! This article includes best practices to optimize your build time and gotchas you might encounter.", - url = "https://medium.com/androiddevelopers/dagger-in-kotlin-gotchas-and-optimizations-7446d8dfd7dc", - publication = publication, - metadata = Metadata( - author = manuel, - date = "July 30", - readTimeMinutes = 3 - ), - paragraphs = paragraphsPost2, - imageId = R.drawable.post_2, - imageThumbId = R.drawable.post_2_thumb -) +val post2 = + Post( + id = "7446d8dfd7dc", + title = "Dagger in Kotlin: Gotchas and Optimizations", + subtitle = "Use Dagger in Kotlin! This article includes best practices to optimize your build time and gotchas you might encounter.", + url = "https://medium.com/androiddevelopers/dagger-in-kotlin-gotchas-and-optimizations-7446d8dfd7dc", + publication = publication, + metadata = + Metadata( + author = manuel, + date = "July 30", + readTimeMinutes = 3, + ), + paragraphs = paragraphsPost2, + imageId = R.drawable.post_2, + imageThumbId = R.drawable.post_2_thumb, + ) -val post3 = Post( - id = "ac552dcc1741", - title = "From Java Programming Language to Kotlin — the idiomatic way", - subtitle = "Learn how to get started converting Java Programming Language code to Kotlin, making it more idiomatic and avoid common pitfalls, by…", - url = "https://medium.com/androiddevelopers/from-java-programming-language-to-kotlin-the-idiomatic-way-ac552dcc1741", - publication = publication, - metadata = Metadata( - author = florina, - date = "July 09", - readTimeMinutes = 1 - ), - paragraphs = paragraphsPost3, - imageId = R.drawable.post_3, - imageThumbId = R.drawable.post_3_thumb -) +val post3 = + Post( + id = "ac552dcc1741", + title = "From Java Programming Language to Kotlin — the idiomatic way", + subtitle = "Learn how to get started converting Java Programming Language code to Kotlin, making it more idiomatic and avoid common pitfalls, by…", + url = "https://medium.com/androiddevelopers/from-java-programming-language-to-kotlin-the-idiomatic-way-ac552dcc1741", + publication = publication, + metadata = + Metadata( + author = florina, + date = "July 09", + readTimeMinutes = 1, + ), + paragraphs = paragraphsPost3, + imageId = R.drawable.post_3, + imageThumbId = R.drawable.post_3_thumb, + ) -val post4 = Post( - id = "84eb677660d9", - title = "Locale changes and the AndroidViewModel antipattern", - subtitle = "TL;DR: Expose resource IDs from ViewModels to avoid showing obsolete data.", - url = "https://medium.com/androiddevelopers/locale-changes-and-the-androidviewmodel-antipattern-84eb677660d9", - publication = publication, - metadata = Metadata( - author = jose, - date = "April 02", - readTimeMinutes = 1 - ), - paragraphs = paragraphsPost4, - imageId = R.drawable.post_4, - imageThumbId = R.drawable.post_4_thumb -) +val post4 = + Post( + id = "84eb677660d9", + title = "Locale changes and the AndroidViewModel antipattern", + subtitle = "TL;DR: Expose resource IDs from ViewModels to avoid showing obsolete data.", + url = "https://medium.com/androiddevelopers/locale-changes-and-the-androidviewmodel-antipattern-84eb677660d9", + publication = publication, + metadata = + Metadata( + author = jose, + date = "April 02", + readTimeMinutes = 1, + ), + paragraphs = paragraphsPost4, + imageId = R.drawable.post_4, + imageThumbId = R.drawable.post_4_thumb, + ) -val post5 = Post( - id = "55db18283aca", - title = "Collections and sequences in Kotlin", - subtitle = "Working with collections is a common task and the Kotlin Standard Library offers many great utility functions. It also offers two ways of…", - url = "https://medium.com/androiddevelopers/collections-and-sequences-in-kotlin-55db18283aca", - publication = publication, - metadata = Metadata( - author = florina, - date = "July 24", - readTimeMinutes = 4 - ), - paragraphs = paragraphsPost5, - imageId = R.drawable.post_5, - imageThumbId = R.drawable.post_5_thumb -) +val post5 = + Post( + id = "55db18283aca", + title = "Collections and sequences in Kotlin", + subtitle = "Working with collections is a common task and the Kotlin Standard Library offers many great utility functions. It also offers two ways of…", + url = "https://medium.com/androiddevelopers/collections-and-sequences-in-kotlin-55db18283aca", + publication = publication, + metadata = + Metadata( + author = florina, + date = "July 24", + readTimeMinutes = 4, + ), + paragraphs = paragraphsPost5, + imageId = R.drawable.post_5, + imageThumbId = R.drawable.post_5_thumb, + ) -val post6 = Post( - id = "55db18283ac0", - title = "Redesigning the Android Studio Logo", - subtitle = "A case study offering a comprehensive overview of the design journey of the Android Studio product logo.", - url = "https://android-developers.googleblog.com/2023/05/redesigning-android-studio-logo.html", - publication = publication, - metadata = Metadata( - author = androidstudioteam, - date = "May 10", - readTimeMinutes = 5 - ), - paragraphs = paragraphsPost6, - imageId = R.drawable.post_6, - imageThumbId = R.drawable.post_6_thumb -) +val post6 = + Post( + id = "55db18283ac0", + title = "Redesigning the Android Studio Logo", + subtitle = "A case study offering a comprehensive overview of the design journey of the Android Studio product logo.", + url = "https://android-developers.googleblog.com/2023/05/redesigning-android-studio-logo.html", + publication = publication, + metadata = + Metadata( + author = androidstudioteam, + date = "May 10", + readTimeMinutes = 5, + ), + paragraphs = paragraphsPost6, + imageId = R.drawable.post_6, + imageThumbId = R.drawable.post_6_thumb, + ) val posts: PostsFeed = PostsFeed( highlightedPost = post6, recommendedPosts = listOf(post1, post2, post3), - popularPosts = listOf( - post5, - post1.copy(id = "post6"), - post2.copy(id = "post7") - ), - recentPosts = listOf( - post6, - post3.copy(id = "post8"), - post4.copy(id = "post9"), - post5.copy(id = "post10") - ) + popularPosts = + listOf( + post5, + post1.copy(id = "post6"), + post2.copy(id = "post7"), + ), + recentPosts = + listOf( + post6, + post3.copy(id = "post8"), + post4.copy(id = "post9"), + post5.copy(id = "post10"), + ), ) diff --git a/JetNews/app/src/main/java/com/example/jetnews/glance/ui/Divider.kt b/JetNews/app/src/main/java/com/example/jetnews/glance/ui/Divider.kt index 557f8a419a..b2e66ab49a 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/glance/ui/Divider.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/glance/ui/Divider.kt @@ -36,13 +36,14 @@ import com.example.jetnews.glance.ui.theme.JetnewsGlanceColorScheme @Composable fun Divider( thickness: Dp = DividerDefaults.Thickness, - color: ColorProvider = DividerDefaults.color + color: ColorProvider = DividerDefaults.color, ) { Spacer( - modifier = GlanceModifier - .fillMaxWidth() - .height(thickness) - .background(color) + modifier = + GlanceModifier + .fillMaxWidth() + .height(thickness) + .background(color), ) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/glance/ui/JetnewsGlanceAppWidget.kt b/JetNews/app/src/main/java/com/example/jetnews/glance/ui/JetnewsGlanceAppWidget.kt index 063576e2e5..b11ef40475 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/glance/ui/JetnewsGlanceAppWidget.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/glance/ui/JetnewsGlanceAppWidget.kt @@ -60,7 +60,10 @@ import kotlinx.coroutines.withContext class JetnewsGlanceAppWidget : GlanceAppWidget() { override val sizeMode: SizeMode = SizeMode.Exact - override suspend fun provideGlance(context: Context, id: GlanceId) { + override suspend fun provideGlance( + context: Context, + id: GlanceId, + ) { val application = context.applicationContext as JetnewsApplication val postsRepository = application.container.postsRepository @@ -68,12 +71,14 @@ class JetnewsGlanceAppWidget : GlanceAppWidget() { // The widget is configured to refresh periodically using the "android:updatePeriodMillis" // configuration, and during each refresh, the data is loaded here. // The repository can internally return cached results here if it already has fresh data. - val initialPostsFeed = withContext(Dispatchers.IO) { - postsRepository.getPostsFeed().successOr(null) - } - val initialBookmarks: Set = withContext(Dispatchers.IO) { - postsRepository.observeFavorites().first() - } + val initialPostsFeed = + withContext(Dispatchers.IO) { + postsRepository.getPostsFeed().successOr(null) + } + val initialBookmarks: Set = + withContext(Dispatchers.IO) { + postsRepository.observeFavorites().first() + } provideContent { val scope = rememberCoroutineScope() @@ -84,16 +89,17 @@ class JetnewsGlanceAppWidget : GlanceAppWidget() { // Provide a custom color scheme if the SDK version doesn't support dynamic colors. GlanceTheme( - colors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - GlanceTheme.colors - } else { - JetnewsGlanceColorScheme.colors - } + colors = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + GlanceTheme.colors + } else { + JetnewsGlanceColorScheme.colors + }, ) { JetnewsContent( posts = recommendedTopPosts, bookmarks = bookmarks, - onToggleBookmark = { scope.launch { postsRepository.toggleFavorite(it) } } + onToggleBookmark = { scope.launch { postsRepository.toggleFavorite(it) } }, ) } } @@ -103,12 +109,13 @@ class JetnewsGlanceAppWidget : GlanceAppWidget() { private fun JetnewsContent( posts: List, bookmarks: Set?, - onToggleBookmark: (String) -> Unit + onToggleBookmark: (String) -> Unit, ) { Column( - modifier = GlanceModifier - .background(GlanceTheme.colors.surface) - .cornerRadius(24.dp) + modifier = + GlanceModifier + .background(GlanceTheme.colors.surface) + .cornerRadius(24.dp), ) { Header(modifier = GlanceModifier.fillMaxWidth()) // Set key for each size so that the onToggleBookmark lambda is called only once for the @@ -118,7 +125,7 @@ class JetnewsGlanceAppWidget : GlanceAppWidget() { modifier = GlanceModifier.fillMaxWidth(), posts = posts, bookmarks = bookmarks ?: setOf(), - onToggleBookmark = onToggleBookmark + onToggleBookmark = onToggleBookmark, ) } } @@ -129,20 +136,20 @@ class JetnewsGlanceAppWidget : GlanceAppWidget() { Row( verticalAlignment = Alignment.CenterVertically, horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.padding(horizontal = 10.dp, vertical = 20.dp) + modifier = modifier.padding(horizontal = 10.dp, vertical = 20.dp), ) { val context = LocalContext.current Image( provider = ImageProvider(R.drawable.ic_jetnews_logo), colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), contentDescription = null, - modifier = GlanceModifier.size(24.dp) + modifier = GlanceModifier.size(24.dp), ) Spacer(modifier = GlanceModifier.width(8.dp)) Image( contentDescription = context.getString(R.string.app_name), colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurfaceVariant), - provider = ImageProvider(R.drawable.ic_jetnews_wordmark) + provider = ImageProvider(R.drawable.ic_jetnews_wordmark), ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/glance/ui/Post.kt b/JetNews/app/src/main/java/com/example/jetnews/glance/ui/Post.kt index 3c8fea4f15..2088f551c2 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/glance/ui/Post.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/glance/ui/Post.kt @@ -51,27 +51,31 @@ import com.example.jetnews.ui.MainActivity enum class PostLayout { HORIZONTAL_SMALL, HORIZONTAL_LARGE, VERTICAL } -fun DpSize.toPostLayout(): PostLayout { - return when { +fun DpSize.toPostLayout(): PostLayout = + when { (this.width <= 300.dp) -> PostLayout.VERTICAL (this.width <= 700.dp) -> PostLayout.HORIZONTAL_SMALL else -> PostLayout.HORIZONTAL_LARGE } -} -private fun Context.authorReadTimeString(author: String, readTimeMinutes: Int) = - getString(R.string.home_post_min_read) - .format(author, readTimeMinutes) +private fun Context.authorReadTimeString( + author: String, + readTimeMinutes: Int, +) = getString(R.string.home_post_min_read) + .format(author, readTimeMinutes) -private fun openPostDetails(context: Context, post: Post): Action { +private fun openPostDetails( + context: Context, + post: Post, +): Action { // actionStartActivity is the preferred way to start activities. return actionStartActivity( Intent( Intent.ACTION_VIEW, "$JETNEWS_APP_URI/home?postId=${post.id}".toUri(), context, - MainActivity::class.java - ) + MainActivity::class.java, + ), ) } @@ -84,27 +88,30 @@ fun Post( postLayout: PostLayout, ) { when (postLayout) { - PostLayout.HORIZONTAL_SMALL -> HorizontalPost( - post = post, - bookmarks = bookmarks, - onToggleBookmark = onToggleBookmark, - modifier = modifier, - ) + PostLayout.HORIZONTAL_SMALL -> + HorizontalPost( + post = post, + bookmarks = bookmarks, + onToggleBookmark = onToggleBookmark, + modifier = modifier, + ) - PostLayout.HORIZONTAL_LARGE -> HorizontalPost( - post = post, - bookmarks = bookmarks, - onToggleBookmark = onToggleBookmark, - modifier = modifier, - showImageThumbnail = false - ) + PostLayout.HORIZONTAL_LARGE -> + HorizontalPost( + post = post, + bookmarks = bookmarks, + onToggleBookmark = onToggleBookmark, + modifier = modifier, + showImageThumbnail = false, + ) - PostLayout.VERTICAL -> VerticalPost( - post = post, - bookmarks = bookmarks, - onToggleBookmark = onToggleBookmark, - modifier = modifier, - ) + PostLayout.VERTICAL -> + VerticalPost( + post = post, + bookmarks = bookmarks, + onToggleBookmark = onToggleBookmark, + modifier = modifier, + ) } } @@ -114,38 +121,39 @@ fun HorizontalPost( bookmarks: Set, onToggleBookmark: (String) -> Unit, modifier: GlanceModifier, - showImageThumbnail: Boolean = true + showImageThumbnail: Boolean = true, ) { val context = LocalContext.current Row( verticalAlignment = Alignment.Vertical.CenterVertically, - modifier = modifier.clickable(onClick = openPostDetails(context, post)) + modifier = modifier.clickable(onClick = openPostDetails(context, post)), ) { if (showImageThumbnail) { PostImage( imageId = post.imageThumbId, contentScale = ContentScale.Fit, - modifier = GlanceModifier.size(80.dp) + modifier = GlanceModifier.size(80.dp), ) } else { PostImage( imageId = post.imageId, contentScale = ContentScale.Crop, - modifier = GlanceModifier.width(250.dp) + modifier = GlanceModifier.width(250.dp), ) } PostDescription( title = post.title, - metadata = context.authorReadTimeString( - author = post.metadata.author.name, - readTimeMinutes = post.metadata.readTimeMinutes - ), - modifier = GlanceModifier.defaultWeight().padding(horizontal = 20.dp) + metadata = + context.authorReadTimeString( + author = post.metadata.author.name, + readTimeMinutes = post.metadata.readTimeMinutes, + ), + modifier = GlanceModifier.defaultWeight().padding(horizontal = 20.dp), ) BookmarkButton( id = post.id, isBookmarked = bookmarks.contains(post.id), - onToggleBookmark = onToggleBookmark + onToggleBookmark = onToggleBookmark, ) } } @@ -160,42 +168,48 @@ fun VerticalPost( val context = LocalContext.current Column( verticalAlignment = Alignment.Vertical.CenterVertically, - modifier = modifier.clickable(onClick = openPostDetails(context, post)) + modifier = modifier.clickable(onClick = openPostDetails(context, post)), ) { PostImage(imageId = post.imageId, modifier = GlanceModifier.fillMaxWidth()) Spacer(modifier = GlanceModifier.height(4.dp)) Row(verticalAlignment = Alignment.CenterVertically) { PostDescription( title = post.title, - metadata = context.authorReadTimeString( - author = post.metadata.author.name, - readTimeMinutes = post.metadata.readTimeMinutes - ), - modifier = GlanceModifier.defaultWeight() + metadata = + context.authorReadTimeString( + author = post.metadata.author.name, + readTimeMinutes = post.metadata.readTimeMinutes, + ), + modifier = GlanceModifier.defaultWeight(), ) Spacer(modifier = GlanceModifier.width(10.dp)) BookmarkButton( id = post.id, isBookmarked = bookmarks.contains(post.id), - onToggleBookmark = onToggleBookmark + onToggleBookmark = onToggleBookmark, ) } } } @Composable -fun BookmarkButton(id: String, isBookmarked: Boolean, onToggleBookmark: (String) -> Unit) { +fun BookmarkButton( + id: String, + isBookmarked: Boolean, + onToggleBookmark: (String) -> Unit, +) { Image( - provider = ImageProvider( - if (isBookmarked) { - R.drawable.ic_jetnews_bookmark_filled - } else { - R.drawable.ic_jetnews_bookmark - } - ), + provider = + ImageProvider( + if (isBookmarked) { + R.drawable.ic_jetnews_bookmark_filled + } else { + R.drawable.ic_jetnews_bookmark + }, + ), colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), contentDescription = "${if (isBookmarked) R.string.unbookmark else R.string.bookmark}", - modifier = GlanceModifier.clickable { onToggleBookmark(id) } + modifier = GlanceModifier.clickable { onToggleBookmark(id) }, ) } @@ -203,30 +217,36 @@ fun BookmarkButton(id: String, isBookmarked: Boolean, onToggleBookmark: (String) fun PostImage( imageId: Int, contentScale: ContentScale = ContentScale.Crop, - modifier: GlanceModifier = GlanceModifier + modifier: GlanceModifier = GlanceModifier, ) { Image( provider = ImageProvider(imageId), contentScale = contentScale, contentDescription = null, - modifier = modifier.cornerRadius(5.dp) + modifier = modifier.cornerRadius(5.dp), ) } @Composable -fun PostDescription(title: String, metadata: String, modifier: GlanceModifier) { +fun PostDescription( + title: String, + metadata: String, + modifier: GlanceModifier, +) { Column(modifier = modifier) { Text( text = title, maxLines = 3, - style = JetnewsGlanceTextStyles.bodyLarge - .copy(color = GlanceTheme.colors.onBackground) + style = + JetnewsGlanceTextStyles.bodyLarge + .copy(color = GlanceTheme.colors.onBackground), ) Spacer(modifier = GlanceModifier.height(4.dp)) Text( text = metadata, - style = JetnewsGlanceTextStyles.bodySmall - .copy(color = GlanceTheme.colors.onBackground) + style = + JetnewsGlanceTextStyles.bodySmall + .copy(color = GlanceTheme.colors.onBackground), ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/Theme.kt b/JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/JetnewsGlanceColorScheme.kt similarity index 75% rename from JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/Theme.kt rename to JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/JetnewsGlanceColorScheme.kt index 80ca05b38e..f554b6e9de 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/Theme.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/JetnewsGlanceColorScheme.kt @@ -22,13 +22,15 @@ import com.example.jetnews.ui.theme.DarkColors import com.example.jetnews.ui.theme.LightColors object JetnewsGlanceColorScheme { - val colors = ColorProviders( - light = LightColors, - dark = DarkColors - ) + val colors = + ColorProviders( + light = LightColors, + dark = DarkColors, + ) - val outlineVariant = ColorProvider( - day = LightColors.onSurface.copy(alpha = 0.1f), - night = DarkColors.onSurface.copy(alpha = 0.1f) - ) + val outlineVariant = + ColorProvider( + day = LightColors.onSurface.copy(alpha = 0.1f), + night = DarkColors.onSurface.copy(alpha = 0.1f), + ) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/Type.kt b/JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/JetnewsGlanceTextStyles.kt similarity index 100% rename from JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/Type.kt rename to JetNews/app/src/main/java/com/example/jetnews/glance/ui/theme/JetnewsGlanceTextStyles.kt diff --git a/JetNews/app/src/main/java/com/example/jetnews/model/Post.kt b/JetNews/app/src/main/java/com/example/jetnews/model/Post.kt index 9bd77c1364..557cf7fcd4 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/model/Post.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/model/Post.kt @@ -27,36 +27,36 @@ data class Post( val metadata: Metadata, val paragraphs: List = emptyList(), @DrawableRes val imageId: Int, - @DrawableRes val imageThumbId: Int + @DrawableRes val imageThumbId: Int, ) data class Metadata( val author: PostAuthor, val date: String, - val readTimeMinutes: Int + val readTimeMinutes: Int, ) data class PostAuthor( val name: String, - val url: String? = null + val url: String? = null, ) data class Publication( val name: String, - val logoUrl: String + val logoUrl: String, ) data class Paragraph( val type: ParagraphType, val text: String, - val markups: List = emptyList() + val markups: List = emptyList(), ) data class Markup( val type: MarkupType, val start: Int, val end: Int, - val href: String? = null + val href: String? = null, ) enum class MarkupType { diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt index 7278bc820c..5a690d8281 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt @@ -47,25 +47,31 @@ fun AppDrawer( navigateToHome: () -> Unit, navigateToInterests: () -> Unit, closeDrawer: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { ModalDrawerSheet(modifier) { JetNewsLogo( - modifier = Modifier.padding(horizontal = 28.dp, vertical = 24.dp) + modifier = Modifier.padding(horizontal = 28.dp, vertical = 24.dp), ) NavigationDrawerItem( label = { Text(stringResource(id = R.string.home_title)) }, icon = { Icon(Icons.Filled.Home, null) }, selected = currentRoute == JetnewsDestinations.HOME_ROUTE, - onClick = { navigateToHome(); closeDrawer() }, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + onClick = { + navigateToHome() + closeDrawer() + }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), ) NavigationDrawerItem( label = { Text(stringResource(id = R.string.interests_title)) }, icon = { Icon(Icons.Filled.ListAlt, null) }, selected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE, - onClick = { navigateToInterests(); closeDrawer() }, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + onClick = { + navigateToInterests() + closeDrawer() + }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), ) } } @@ -76,13 +82,13 @@ private fun JetNewsLogo(modifier: Modifier = Modifier) { Icon( painterResource(R.drawable.ic_jetnews_logo), contentDescription = null, - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) Spacer(Modifier.width(8.dp)) Icon( painter = painterResource(R.drawable.ic_jetnews_wordmark), contentDescription = stringResource(R.string.app_name), - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -96,7 +102,7 @@ fun PreviewAppDrawer() { currentRoute = JetnewsDestinations.HOME_ROUTE, navigateToHome = {}, navigateToInterests = {}, - closeDrawer = { } + closeDrawer = { }, ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt index 3cc1c6d3d1..5e79acb17c 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt @@ -42,9 +42,10 @@ fun JetnewsApp( ) { JetnewsTheme { val navController = rememberNavController() - val navigationActions = remember(navController) { - JetnewsNavigationActions(navController) - } + val navigationActions = + remember(navController) { + JetnewsNavigationActions(navController) + } val coroutineScope = rememberCoroutineScope() @@ -61,12 +62,12 @@ fun JetnewsApp( currentRoute = currentRoute, navigateToHome = navigationActions.navigateToHome, navigateToInterests = navigationActions.navigateToInterests, - closeDrawer = { coroutineScope.launch { sizeAwareDrawerState.close() } } + closeDrawer = { coroutineScope.launch { sizeAwareDrawerState.close() } }, ) }, drawerState = sizeAwareDrawerState, // Only enable opening the drawer via gestures if the screen is not expanded - gesturesEnabled = !isExpandedScreen + gesturesEnabled = !isExpandedScreen, ) { Row { if (isExpandedScreen) { diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt index 67fccd2dd1..44cddef275 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt @@ -45,23 +45,26 @@ fun JetnewsNavGraph( NavHost( navController = navController, startDestination = startDestination, - modifier = modifier + modifier = modifier, ) { composable( route = JetnewsDestinations.HOME_ROUTE, - deepLinks = listOf( - navDeepLink { - uriPattern = - "$JETNEWS_APP_URI/${JetnewsDestinations.HOME_ROUTE}?$POST_ID={$POST_ID}" - } - ) + deepLinks = + listOf( + navDeepLink { + uriPattern = + "$JETNEWS_APP_URI/${JetnewsDestinations.HOME_ROUTE}?$POST_ID={$POST_ID}" + }, + ), ) { navBackStackEntry -> - val homeViewModel: HomeViewModel = viewModel( - factory = HomeViewModel.provideFactory( - postsRepository = appContainer.postsRepository, - preSelectedPostId = navBackStackEntry.arguments?.getString(POST_ID) + val homeViewModel: HomeViewModel = + viewModel( + factory = + HomeViewModel.provideFactory( + postsRepository = appContainer.postsRepository, + preSelectedPostId = navBackStackEntry.arguments?.getString(POST_ID), + ), ) - ) HomeRoute( homeViewModel = homeViewModel, isExpandedScreen = isExpandedScreen, @@ -69,13 +72,14 @@ fun JetnewsNavGraph( ) } composable(JetnewsDestinations.INTERESTS_ROUTE) { - val interestsViewModel: InterestsViewModel = viewModel( - factory = InterestsViewModel.provideFactory(appContainer.interestsRepository) - ) + val interestsViewModel: InterestsViewModel = + viewModel( + factory = InterestsViewModel.provideFactory(appContainer.interestsRepository), + ) InterestsRoute( interestsViewModel = interestsViewModel, isExpandedScreen = isExpandedScreen, - openDrawer = openDrawer + openDrawer = openDrawer, ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavigation.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavigation.kt index 8dd1ee01d6..d95e93ed76 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavigation.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavigation.kt @@ -30,7 +30,9 @@ object JetnewsDestinations { /** * Models the navigation actions in the app. */ -class JetnewsNavigationActions(navController: NavHostController) { +class JetnewsNavigationActions( + navController: NavHostController, +) { val navigateToHome: () -> Unit = { navController.navigate(JetnewsDestinations.HOME_ROUTE) { // Pop up to the start destination of the graph to diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/MainActivity.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/MainActivity.kt index bd6d2ac0bd..fdbd8043ed 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/MainActivity.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/MainActivity.kt @@ -25,7 +25,6 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import com.example.jetnews.JetnewsApplication class MainActivity : ComponentActivity() { - @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt index 5891166cbb..1c7b5f77a9 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt @@ -87,7 +87,7 @@ fun ArticleScreen( isFavorite: Boolean, onToggleFavorite: () -> Unit, modifier: Modifier = Modifier, - lazyListState: LazyListState = rememberLazyListState() + lazyListState: LazyListState = rememberLazyListState(), ) { var showUnimplementedActionDialog by rememberSaveable { mutableStateOf(false) } if (showUnimplementedActionDialog) { @@ -105,7 +105,7 @@ fun ArticleScreen( Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.cd_navigate_up), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) } } @@ -119,11 +119,11 @@ fun ArticleScreen( BookmarkButton(isBookmarked = isFavorite, onClick = onToggleFavorite) ShareButton(onClick = { sharePost(post, context) }) TextSettingsButton(onClick = { showUnimplementedActionDialog = true }) - } + }, ) } }, - lazyListState = lazyListState + lazyListState = lazyListState, ) } } @@ -141,7 +141,7 @@ private fun ArticleScreenContent( post: Post, navigationIconContent: @Composable () -> Unit = { }, bottomBarContent: @Composable () -> Unit = { }, - lazyListState: LazyListState = rememberLazyListState() + lazyListState: LazyListState = rememberLazyListState(), ) { val topAppBarState = rememberTopAppBarState() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState) @@ -150,16 +150,17 @@ private fun ArticleScreenContent( TopAppBar( title = post.publication?.name.orEmpty(), navigationIconContent = navigationIconContent, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, ) }, - bottomBar = bottomBarContent + bottomBar = bottomBarContent, ) { innerPadding -> PostContent( post = post, contentPadding = innerPadding, - modifier = Modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = + Modifier + .nestedScroll(scrollBehavior.nestedScrollConnection), state = lazyListState, ) } @@ -171,7 +172,7 @@ private fun TopAppBar( title: String, navigationIconContent: @Composable () -> Unit, scrollBehavior: TopAppBarScrollBehavior?, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { CenterAlignedTopAppBar( title = { @@ -179,20 +180,21 @@ private fun TopAppBar( Image( painter = painterResource(id = R.drawable.icon_article_background), contentDescription = null, - modifier = Modifier - .clip(CircleShape) - .size(36.dp) + modifier = + Modifier + .clip(CircleShape) + .size(36.dp), ) Text( text = stringResource(R.string.published_in, title), style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(start = 8.dp) + modifier = Modifier.padding(start = 8.dp), ) } }, navigationIcon = navigationIconContent, scrollBehavior = scrollBehavior, - modifier = modifier + modifier = modifier, ) } @@ -208,14 +210,14 @@ private fun FunctionalityNotAvailablePopup(onDismiss: () -> Unit) { text = { Text( text = stringResource(id = R.string.article_functionality_not_available), - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyLarge, ) }, confirmButton = { TextButton(onClick = onDismiss) { Text(text = stringResource(id = R.string.close)) } - } + }, ) } @@ -225,17 +227,21 @@ private fun FunctionalityNotAvailablePopup(onDismiss: () -> Unit) { * @param post to share * @param context Android context to show the share sheet in */ -fun sharePost(post: Post, context: Context) { - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TITLE, post.title) - putExtra(Intent.EXTRA_TEXT, post.url) - } +fun sharePost( + post: Post, + context: Context, +) { + val intent = + Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TITLE, post.title) + putExtra(Intent.EXTRA_TEXT, post.url) + } context.startActivity( Intent.createChooser( intent, - context.getString(R.string.article_share_post) - ) + context.getString(R.string.article_share_post), + ), ) } @@ -245,9 +251,10 @@ fun sharePost(post: Post, context: Context) { @Composable fun PreviewArticleDrawer() { JetnewsTheme { - val post = runBlocking { - (BlockingFakePostsRepository().getPost(post3.id) as Result.Success).data - } + val post = + runBlocking { + (BlockingFakePostsRepository().getPost(post3.id) as Result.Success).data + } ArticleScreen(post, false, {}, false, {}) } } @@ -256,15 +263,16 @@ fun PreviewArticleDrawer() { @Preview( "Article screen navrail (dark)", uiMode = UI_MODE_NIGHT_YES, - device = Devices.PIXEL_C + device = Devices.PIXEL_C, ) @Preview("Article screen navrail (big font)", fontScale = 1.5f, device = Devices.PIXEL_C) @Composable fun PreviewArticleNavRail() { JetnewsTheme { - val post = runBlocking { - (BlockingFakePostsRepository().getPost(post3.id) as Result.Success).data - } + val post = + runBlocking { + (BlockingFakePostsRepository().getPost(post3.id) as Result.Success).data + } ArticleScreen(post, true, {}, false, {}) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt index 82346a135d..cc3f695d01 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt @@ -85,7 +85,7 @@ fun PostContent( post: Post, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), - state: LazyListState = rememberLazyListState() + state: LazyListState = rememberLazyListState(), ) { LazyColumn( contentPadding = contentPadding, @@ -113,51 +113,54 @@ fun LazyListScope.postContentItems(post: Post) { @Composable private fun PostHeaderImage(post: Post) { - val imageModifier = Modifier - .heightIn(min = 180.dp) - .fillMaxWidth() - .clip(shape = MaterialTheme.shapes.large) + val imageModifier = + Modifier + .heightIn(min = 180.dp) + .fillMaxWidth() + .clip(shape = MaterialTheme.shapes.large) Image( painter = painterResource(post.imageId), contentDescription = null, // decorative modifier = imageModifier, - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, ) } @Composable private fun PostMetadata( metadata: Metadata, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row( // Merge semantics so accessibility services consider this row a single element - modifier = modifier.semantics(mergeDescendants = true) {} + modifier = modifier.semantics(mergeDescendants = true) {}, ) { Image( imageVector = Icons.Filled.AccountCircle, contentDescription = null, // decorative modifier = Modifier.size(40.dp), colorFilter = ColorFilter.tint(LocalContentColor.current), - contentScale = ContentScale.Fit + contentScale = ContentScale.Fit, ) Spacer(Modifier.width(8.dp)) Column { Text( text = metadata.author.name, style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(top = 4.dp) + modifier = Modifier.padding(top = 4.dp), ) Text( - text = stringResource( - id = R.string.article_post_min_read, - formatArgs = arrayOf( - metadata.date, - metadata.readTimeMinutes - ) - ), - style = MaterialTheme.typography.bodySmall + text = + stringResource( + id = R.string.article_post_min_read, + formatArgs = + arrayOf( + metadata.date, + metadata.readTimeMinutes, + ), + ), + style = MaterialTheme.typography.bodySmall, ) } } @@ -167,35 +170,39 @@ private fun PostMetadata( private fun Paragraph(paragraph: Paragraph) { val (textStyle, paragraphStyle, trailingPadding) = paragraph.type.getTextAndParagraphStyle() - val annotatedString = paragraphToAnnotatedString( - paragraph, - MaterialTheme.typography, - MaterialTheme.colorScheme.codeBlockBackground - ) + val annotatedString = + paragraphToAnnotatedString( + paragraph, + MaterialTheme.typography, + MaterialTheme.colorScheme.codeBlockBackground, + ) Box(modifier = Modifier.padding(bottom = trailingPadding)) { when (paragraph.type) { - ParagraphType.Bullet -> BulletParagraph( - text = annotatedString, - textStyle = textStyle, - paragraphStyle = paragraphStyle - ) - ParagraphType.CodeBlock -> CodeBlockParagraph( - text = annotatedString, - textStyle = textStyle, - paragraphStyle = paragraphStyle - ) + ParagraphType.Bullet -> + BulletParagraph( + text = annotatedString, + textStyle = textStyle, + paragraphStyle = paragraphStyle, + ) + ParagraphType.CodeBlock -> + CodeBlockParagraph( + text = annotatedString, + textStyle = textStyle, + paragraphStyle = paragraphStyle, + ) ParagraphType.Header -> { Text( modifier = Modifier.padding(4.dp), text = annotatedString, - style = textStyle.merge(paragraphStyle) + style = textStyle.merge(paragraphStyle), ) } - else -> Text( - modifier = Modifier.padding(4.dp), - text = annotatedString, - style = textStyle - ) + else -> + Text( + modifier = Modifier.padding(4.dp), + text = annotatedString, + style = textStyle, + ) } } } @@ -204,17 +211,17 @@ private fun Paragraph(paragraph: Paragraph) { private fun CodeBlockParagraph( text: AnnotatedString, textStyle: TextStyle, - paragraphStyle: ParagraphStyle + paragraphStyle: ParagraphStyle, ) { Surface( color = MaterialTheme.colorScheme.codeBlockBackground, shape = MaterialTheme.shapes.small, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) { Text( modifier = Modifier.padding(16.dp), text = text, - style = textStyle.merge(paragraphStyle) + style = textStyle.merge(paragraphStyle), ) } } @@ -223,27 +230,28 @@ private fun CodeBlockParagraph( private fun BulletParagraph( text: AnnotatedString, textStyle: TextStyle, - paragraphStyle: ParagraphStyle + paragraphStyle: ParagraphStyle, ) { Row { with(LocalDensity.current) { // this box is acting as a character, so it's sized with font scaling (sp) Box( - modifier = Modifier - .size(8.sp.toDp(), 8.sp.toDp()) - .alignBy { - // Add an alignment "baseline" 1sp below the bottom of the circle - 9.sp.roundToPx() - } - .background(LocalContentColor.current, CircleShape), + modifier = + Modifier + .size(8.sp.toDp(), 8.sp.toDp()) + .alignBy { + // Add an alignment "baseline" 1sp below the bottom of the circle + 9.sp.roundToPx() + }.background(LocalContentColor.current, CircleShape), ) { /* no content */ } } Text( - modifier = Modifier - .weight(1f) - .alignBy(FirstBaseline), + modifier = + Modifier + .weight(1f) + .alignBy(FirstBaseline), text = text, - style = textStyle.merge(paragraphStyle) + style = textStyle.merge(paragraphStyle), ) } } @@ -251,7 +259,7 @@ private fun BulletParagraph( private data class ParagraphStyling( val textStyle: TextStyle, val paragraphStyle: ParagraphStyle, - val trailingPadding: Dp + val trailingPadding: Dp, ) @Composable @@ -275,9 +283,11 @@ private fun ParagraphType.getTextAndParagraphStyle(): ParagraphStyling { textStyle = typography.headlineMedium trailingPadding = 16.dp } - ParagraphType.CodeBlock -> textStyle = typography.bodyLarge.copy( - fontFamily = FontFamily.Monospace - ) + ParagraphType.CodeBlock -> + textStyle = + typography.bodyLarge.copy( + fontFamily = FontFamily.Monospace, + ) ParagraphType.Quote -> textStyle = typography.bodyLarge ParagraphType.Bullet -> { paragraphStyle = ParagraphStyle(textIndent = TextIndent(firstLine = 8.sp)) @@ -286,44 +296,45 @@ private fun ParagraphType.getTextAndParagraphStyle(): ParagraphStyling { return ParagraphStyling( textStyle, paragraphStyle, - trailingPadding + trailingPadding, ) } private fun paragraphToAnnotatedString( paragraph: Paragraph, typography: Typography, - codeBlockBackground: Color + codeBlockBackground: Color, ): AnnotatedString { - val styles: List> = paragraph.markups - .map { it.toAnnotatedStringItem(typography, codeBlockBackground) } + val styles: List> = + paragraph.markups + .map { it.toAnnotatedStringItem(typography, codeBlockBackground) } return AnnotatedString(text = paragraph.text, spanStyles = styles) } fun Markup.toAnnotatedStringItem( typography: Typography, - codeBlockBackground: Color -): AnnotatedString.Range { - return when (this.type) { + codeBlockBackground: Color, +): AnnotatedString.Range = + when (this.type) { MarkupType.Italic -> { AnnotatedString.Range( typography.bodyLarge.copy(fontStyle = FontStyle.Italic).toSpanStyle(), start, - end + end, ) } MarkupType.Link -> { AnnotatedString.Range( typography.bodyLarge.copy(textDecoration = TextDecoration.Underline).toSpanStyle(), start, - end + end, ) } MarkupType.Bold -> { AnnotatedString.Range( typography.bodyLarge.copy(fontWeight = FontWeight.Bold).toSpanStyle(), start, - end + end, ) } MarkupType.Code -> { @@ -331,14 +342,13 @@ fun Markup.toAnnotatedStringItem( typography.bodyLarge .copy( background = codeBlockBackground, - fontFamily = FontFamily.Monospace + fontFamily = FontFamily.Monospace, ).toSpanStyle(), start, - end + end, ) } } -} private val ColorScheme.codeBlockBackground: Color get() = onSurface.copy(alpha = .15f) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt index 43bf5640d4..aa66d2547e 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt @@ -42,7 +42,7 @@ fun AppNavRail( currentRoute: String, navigateToHome: () -> Unit, navigateToInterests: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { NavigationRail( header = { @@ -50,10 +50,10 @@ fun AppNavRail( painterResource(R.drawable.ic_jetnews_logo), null, Modifier.padding(vertical = 12.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) }, - modifier = modifier + modifier = modifier, ) { Spacer(Modifier.weight(1f)) NavigationRailItem( @@ -61,14 +61,14 @@ fun AppNavRail( onClick = navigateToHome, icon = { Icon(Icons.Filled.Home, stringResource(R.string.home_title)) }, label = { Text(stringResource(R.string.home_title)) }, - alwaysShowLabel = false + alwaysShowLabel = false, ) NavigationRailItem( selected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE, onClick = navigateToInterests, icon = { Icon(Icons.Filled.ListAlt, stringResource(R.string.interests_title)) }, label = { Text(stringResource(R.string.interests_title)) }, - alwaysShowLabel = false + alwaysShowLabel = false, ) Spacer(Modifier.weight(1f)) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/components/JetnewsSnackbarHost.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/components/JetnewsSnackbarHost.kt index 1f46ec4510..374c825ba8 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/components/JetnewsSnackbarHost.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/components/JetnewsSnackbarHost.kt @@ -35,15 +35,16 @@ import androidx.compose.ui.unit.dp fun JetnewsSnackbarHost( hostState: SnackbarHostState, modifier: Modifier = Modifier, - snackbar: @Composable (SnackbarData) -> Unit = { Snackbar(it) } + snackbar: @Composable (SnackbarData) -> Unit = { Snackbar(it) }, ) { SnackbarHost( hostState = hostState, - modifier = modifier - .systemBarsPadding() - // Limit the Snackbar width for large screens - .wrapContentWidth(align = Alignment.Start) - .widthIn(max = 550.dp), - snackbar = snackbar + modifier = + modifier + .systemBarsPadding() + // Limit the Snackbar width for large screens + .wrapContentWidth(align = Alignment.Start) + .widthIn(max = 550.dp), + snackbar = snackbar, ) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeRoute.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeRoute.kt index 0835169687..f555b0aab8 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeRoute.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeRoute.kt @@ -45,7 +45,7 @@ fun HomeRoute( homeViewModel: HomeViewModel, isExpandedScreen: Boolean, openDrawer: () -> Unit, - snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, ) { // UiState of the HomeScreen val uiState by homeViewModel.uiState.collectAsStateWithLifecycle() @@ -94,20 +94,21 @@ fun HomeRoute( onInteractWithArticleDetails: (String) -> Unit, onSearchInputChanged: (String) -> Unit, openDrawer: () -> Unit, - snackbarHostState: SnackbarHostState + snackbarHostState: SnackbarHostState, ) { // Construct the lazy list states for the list and the details outside of deciding which one to // show. This allows the associated state to survive beyond that decision, and therefore // we get to preserve the scroll throughout any changes to the content. val homeListLazyListState = rememberLazyListState() - val articleDetailLazyListStates = when (uiState) { - is HomeUiState.HasPosts -> uiState.postsFeed.allPosts - is HomeUiState.NoPosts -> emptyList() - }.associate { post -> - key(post.id) { - post.id to rememberLazyListState() + val articleDetailLazyListStates = + when (uiState) { + is HomeUiState.HasPosts -> uiState.postsFeed.allPosts + is HomeUiState.NoPosts -> emptyList() + }.associate { post -> + key(post.id) { + post.id to rememberLazyListState() + } } - } val homeScreenType = getHomeScreenType(isExpandedScreen, uiState) when (homeScreenType) { @@ -154,9 +155,10 @@ fun HomeRoute( onToggleFavorite = { onToggleFavorite(uiState.selectedPost.id) }, - lazyListState = articleDetailLazyListStates.getValue( - uiState.selectedPost.id - ) + lazyListState = + articleDetailLazyListStates.getValue( + uiState.selectedPost.id, + ), ) // If we are just showing the detail, have a back press switch to the list. @@ -180,7 +182,7 @@ fun HomeRoute( private enum class HomeScreenType { FeedWithArticleDetails, Feed, - ArticleDetails + ArticleDetails, } /** @@ -190,19 +192,20 @@ private enum class HomeScreenType { @Composable private fun getHomeScreenType( isExpandedScreen: Boolean, - uiState: HomeUiState -): HomeScreenType = when (isExpandedScreen) { - false -> { - when (uiState) { - is HomeUiState.HasPosts -> { - if (uiState.isArticleOpen) { - HomeScreenType.ArticleDetails - } else { - HomeScreenType.Feed + uiState: HomeUiState, +): HomeScreenType = + when (isExpandedScreen) { + false -> { + when (uiState) { + is HomeUiState.HasPosts -> { + if (uiState.isArticleOpen) { + HomeScreenType.ArticleDetails + } else { + HomeScreenType.Feed + } } + is HomeUiState.NoPosts -> HomeScreenType.Feed } - is HomeUiState.NoPosts -> HomeScreenType.Feed } + true -> HomeScreenType.FeedWithArticleDetails } - true -> HomeScreenType.FeedWithArticleDetails -} diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreens.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreens.kt index 7f490dc551..9be76d3cc5 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreens.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreens.kt @@ -152,9 +152,10 @@ fun HomeFeedWithArticleDetailsScreen( onArticleTapped = onSelectPost, onToggleFavorite = onToggleFavorite, contentPadding = contentPadding, - modifier = Modifier - .width(334.dp) - .notifyInput(onInteractWithList), + modifier = + Modifier + .width(334.dp) + .notifyInput(onInteractWithList), state = homeListLazyListState, searchInput = hasPostsUiState.searchInput, onSearchInputChanged = onSearchInputChanged, @@ -173,12 +174,13 @@ fun HomeFeedWithArticleDetailsScreen( LazyColumn( state = detailLazyListState, contentPadding = contentPadding, - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxSize() - .notifyInput { - onInteractWithDetail(detailPost.id) - } + modifier = + Modifier + .padding(horizontal = 16.dp) + .fillMaxSize() + .notifyInput { + onInteractWithDetail(detailPost.id) + }, ) { stickyHeader { val context = LocalContext.current @@ -186,9 +188,10 @@ fun HomeFeedWithArticleDetailsScreen( isFavorite = hasPostsUiState.favorites.contains(detailPost.id), onToggleFavorite = { onToggleFavorite(detailPost.id) }, onSharePost = { sharePost(detailPost, context) }, - modifier = Modifier - .fillMaxWidth() - .wrapContentWidth(Alignment.End) + modifier = + Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.End), ) } postContentItems(detailPost) @@ -240,7 +243,7 @@ fun HomeFeedScreen( onErrorDismiss = onErrorDismiss, openDrawer = openDrawer, snackbarHostState = snackbarHostState, - modifier = modifier + modifier = modifier, ) { hasPostsUiState, contentPadding, contentModifier -> PostList( postsFeed = hasPostsUiState.postsFeed, @@ -252,7 +255,7 @@ fun HomeFeedScreen( modifier = contentModifier, state = homeListLazyListState, searchInput = searchInput, - onSearchInputChanged = onSearchInputChanged + onSearchInputChanged = onSearchInputChanged, ) } } @@ -279,8 +282,8 @@ private fun HomeScreenWithList( hasPostsContent: @Composable ( uiState: HomeUiState.HasPosts, contentPadding: PaddingValues, - modifier: Modifier - ) -> Unit + modifier: Modifier, + ) -> Unit, ) { val topAppBarState = rememberTopAppBarState() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarState) @@ -290,19 +293,20 @@ private fun HomeScreenWithList( if (showTopAppBar) { HomeTopAppBar( openDrawer = openDrawer, - topAppBarState = topAppBarState + topAppBarState = topAppBarState, ) } }, - modifier = modifier + modifier = modifier, ) { innerPadding -> val contentModifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) LoadingContent( - empty = when (uiState) { - is HomeUiState.HasPosts -> false - is HomeUiState.NoPosts -> uiState.isLoading - }, + empty = + when (uiState) { + is HomeUiState.HasPosts -> false + is HomeUiState.NoPosts -> uiState.isLoading + }, emptyContent = { FullScreenLoading() }, loading = uiState.isLoading, onRefresh = onRefreshPosts, @@ -315,11 +319,11 @@ private fun HomeScreenWithList( // if there are no posts, and no error, let the user refresh manually TextButton( onClick = onRefreshPosts, - modifier.padding(innerPadding).fillMaxSize() + modifier.padding(innerPadding).fillMaxSize(), ) { Text( stringResource(id = R.string.home_tap_to_load_content), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } else { @@ -327,12 +331,12 @@ private fun HomeScreenWithList( Box( contentModifier .padding(innerPadding) - .fillMaxSize() + .fillMaxSize(), ) { /* empty screen */ } } } } - } + }, ) } @@ -354,10 +358,11 @@ private fun HomeScreenWithList( // If there's a change to errorMessageText, retryMessageText or snackbarHostState, // the previous effect will be cancelled and a new one will start with the new values LaunchedEffect(errorMessageText, retryMessageText, snackbarHostState) { - val snackbarResult = snackbarHostState.showSnackbar( - message = errorMessageText, - actionLabel = retryMessageText - ) + val snackbarResult = + snackbarHostState.showSnackbar( + message = errorMessageText, + actionLabel = retryMessageText, + ) if (snackbarResult == SnackbarResult.ActionPerformed) { onRefreshPostsState() } @@ -382,7 +387,7 @@ private fun LoadingContent( emptyContent: @Composable () -> Unit, loading: Boolean, onRefresh: () -> Unit, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { if (empty) { emptyContent() @@ -420,7 +425,7 @@ private fun PostList( LazyColumn( modifier = modifier, contentPadding = contentPadding, - state = state + state = state, ) { if (showExpandedSearch) { item { @@ -438,14 +443,15 @@ private fun PostList( postsFeed.recommendedPosts, onArticleTapped, favorites, - onToggleFavorite + onToggleFavorite, ) } } if (postsFeed.popularPosts.isNotEmpty() && !showExpandedSearch) { item { PostListPopularSection( - postsFeed.popularPosts, onArticleTapped + postsFeed.popularPosts, + onArticleTapped, ) } } @@ -461,9 +467,10 @@ private fun PostList( @Composable private fun FullScreenLoading() { Box( - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center) + modifier = + Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center), ) { CircularProgressIndicator() } @@ -476,15 +483,18 @@ private fun FullScreenLoading() { * @param navigateToArticle (event) request navigation to Article screen */ @Composable -private fun PostListTopSection(post: Post, navigateToArticle: (String) -> Unit) { +private fun PostListTopSection( + post: Post, + navigateToArticle: (String) -> Unit, +) { Text( modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp), text = stringResource(id = R.string.home_top_section_title), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) PostCardTop( post = post, - modifier = Modifier.clickable(onClick = { navigateToArticle(post.id) }) + modifier = Modifier.clickable(onClick = { navigateToArticle(post.id) }), ) PostListDivider() } @@ -500,7 +510,7 @@ private fun PostListSimpleSection( posts: List, navigateToArticle: (String) -> Unit, favorites: Set, - onToggleFavorite: (String) -> Unit + onToggleFavorite: (String) -> Unit, ) { Column { posts.forEach { post -> @@ -508,7 +518,7 @@ private fun PostListSimpleSection( post = post, navigateToArticle = navigateToArticle, isFavorite = favorites.contains(post.id), - onToggleFavorite = { onToggleFavorite(post.id) } + onToggleFavorite = { onToggleFavorite(post.id) }, ) PostListDivider() } @@ -524,25 +534,26 @@ private fun PostListSimpleSection( @Composable private fun PostListPopularSection( posts: List, - navigateToArticle: (String) -> Unit + navigateToArticle: (String) -> Unit, ) { Column { Text( modifier = Modifier.padding(16.dp), text = stringResource(id = R.string.home_popular_section_title), - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleLarge, ) Row( - modifier = Modifier - .horizontalScroll(rememberScrollState()) - .height(IntrinsicSize.Max) - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) + modifier = + Modifier + .horizontalScroll(rememberScrollState()) + .height(IntrinsicSize.Max) + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { for (post in posts) { PostCardPopular( post, - navigateToArticle + navigateToArticle, ) } } @@ -560,7 +571,7 @@ private fun PostListPopularSection( @Composable private fun PostListHistorySection( posts: List, - navigateToArticle: (String) -> Unit + navigateToArticle: (String) -> Unit, ) { Column { posts.forEach { post -> @@ -577,7 +588,7 @@ private fun PostListHistorySection( private fun PostListDivider() { Divider( modifier = Modifier.padding(horizontal = 14.dp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f) + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f), ) } @@ -599,24 +610,26 @@ private fun HomeSearch( onValueChange = onSearchInputChanged, placeholder = { Text(stringResource(R.string.home_search)) }, leadingIcon = { Icon(Icons.Filled.Search, null) }, - modifier = modifier - .fillMaxWidth() - .interceptKey(Key.Enter) { - // submit a search query when Enter is pressed - submitSearch(onSearchInputChanged, context) - keyboardController?.hide() - focusManager.clearFocus(force = true) - }, + modifier = + modifier + .fillMaxWidth() + .interceptKey(Key.Enter) { + // submit a search query when Enter is pressed + submitSearch(onSearchInputChanged, context) + keyboardController?.hide() + focusManager.clearFocus(force = true) + }, singleLine = true, // keyboardOptions change the newline key to a search key on the soft keyboard keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), // keyboardActions submits the search query when the search key is pressed - keyboardActions = KeyboardActions( - onSearch = { - submitSearch(onSearchInputChanged, context) - keyboardController?.hide() - } - ) + keyboardActions = + KeyboardActions( + onSearch = { + submitSearch(onSearchInputChanged, context) + keyboardController?.hide() + }, + ), ) } @@ -625,14 +638,15 @@ private fun HomeSearch( */ private fun submitSearch( onSearchInputChanged: (String) -> Unit, - context: Context + context: Context, ) { onSearchInputChanged("") - Toast.makeText( - context, - "Search is not yet implemented", - Toast.LENGTH_SHORT - ).show() + Toast + .makeText( + context, + "Search is not yet implemented", + Toast.LENGTH_SHORT, + ).show() } /** @@ -643,12 +657,12 @@ private fun PostTopBar( isFavorite: Boolean, onToggleFavorite: () -> Unit, onSharePost: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Surface( shape = RoundedCornerShape(8.dp), border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.onSurface.copy(alpha = .6f)), - modifier = modifier.padding(end = 16.dp) + modifier = modifier.padding(end = 16.dp), ) { Row(Modifier.padding(horizontal = 8.dp)) { FavoriteButton(onClick = { /* Functionality not available */ }) @@ -669,7 +683,7 @@ private fun HomeTopAppBar( modifier: Modifier = Modifier, topAppBarState: TopAppBarState = rememberTopAppBarState(), scrollBehavior: TopAppBarScrollBehavior? = - TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState) + TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState), ) { val context = LocalContext.current val title = stringResource(id = R.string.app_name) @@ -680,33 +694,34 @@ private fun HomeTopAppBar( contentDescription = title, contentScale = ContentScale.Inside, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) }, navigationIcon = { IconButton(onClick = openDrawer) { Icon( painter = painterResource(R.drawable.ic_jetnews_logo), - contentDescription = stringResource(R.string.cd_open_navigation_drawer) + contentDescription = stringResource(R.string.cd_open_navigation_drawer), ) } }, actions = { IconButton(onClick = { - Toast.makeText( - context, - "Search is not yet implemented in this configuration", - Toast.LENGTH_LONG - ).show() + Toast + .makeText( + context, + "Search is not yet implemented in this configuration", + Toast.LENGTH_LONG, + ).show() }) { Icon( imageVector = Icons.Filled.Search, - contentDescription = stringResource(R.string.cd_search) + contentDescription = stringResource(R.string.cd_search), ) } }, scrollBehavior = scrollBehavior, - modifier = modifier + modifier = modifier, ) } @@ -715,20 +730,22 @@ private fun HomeTopAppBar( @Preview("Home list drawer screen (big font)", fontScale = 1.5f) @Composable fun PreviewHomeListDrawerScreen() { - val postsFeed = runBlocking { - (BlockingFakePostsRepository().getPostsFeed() as Result.Success).data - } + val postsFeed = + runBlocking { + (BlockingFakePostsRepository().getPostsFeed() as Result.Success).data + } JetnewsTheme { HomeFeedScreen( - uiState = HomeUiState.HasPosts( - postsFeed = postsFeed, - selectedPost = postsFeed.highlightedPost, - isArticleOpen = false, - favorites = emptySet(), - isLoading = false, - errorMessages = emptyList(), - searchInput = "" - ), + uiState = + HomeUiState.HasPosts( + postsFeed = postsFeed, + selectedPost = postsFeed.highlightedPost, + isArticleOpen = false, + favorites = emptySet(), + isLoading = false, + errorMessages = emptyList(), + searchInput = "", + ), showTopAppBar = false, onToggleFavorite = {}, onSelectPost = {}, @@ -737,7 +754,7 @@ fun PreviewHomeListDrawerScreen() { openDrawer = {}, homeListLazyListState = rememberLazyListState(), snackbarHostState = SnackbarHostState(), - onSearchInputChanged = {} + onSearchInputChanged = {}, ) } } @@ -746,25 +763,27 @@ fun PreviewHomeListDrawerScreen() { @Preview( "Home list navrail screen (dark)", uiMode = UI_MODE_NIGHT_YES, - device = Devices.NEXUS_7_2013 + device = Devices.NEXUS_7_2013, ) @Preview("Home list navrail screen (big font)", fontScale = 1.5f, device = Devices.NEXUS_7_2013) @Composable fun PreviewHomeListNavRailScreen() { - val postsFeed = runBlocking { - (BlockingFakePostsRepository().getPostsFeed() as Result.Success).data - } + val postsFeed = + runBlocking { + (BlockingFakePostsRepository().getPostsFeed() as Result.Success).data + } JetnewsTheme { HomeFeedScreen( - uiState = HomeUiState.HasPosts( - postsFeed = postsFeed, - selectedPost = postsFeed.highlightedPost, - isArticleOpen = false, - favorites = emptySet(), - isLoading = false, - errorMessages = emptyList(), - searchInput = "" - ), + uiState = + HomeUiState.HasPosts( + postsFeed = postsFeed, + selectedPost = postsFeed.highlightedPost, + isArticleOpen = false, + favorites = emptySet(), + isLoading = false, + errorMessages = emptyList(), + searchInput = "", + ), showTopAppBar = true, onToggleFavorite = {}, onSelectPost = {}, @@ -773,7 +792,7 @@ fun PreviewHomeListNavRailScreen() { openDrawer = {}, homeListLazyListState = rememberLazyListState(), snackbarHostState = SnackbarHostState(), - onSearchInputChanged = {} + onSearchInputChanged = {}, ) } } @@ -783,20 +802,22 @@ fun PreviewHomeListNavRailScreen() { @Preview("Home list detail screen (big font)", fontScale = 1.5f, device = Devices.PIXEL_C) @Composable fun PreviewHomeListDetailScreen() { - val postsFeed = runBlocking { - (BlockingFakePostsRepository().getPostsFeed() as Result.Success).data - } + val postsFeed = + runBlocking { + (BlockingFakePostsRepository().getPostsFeed() as Result.Success).data + } JetnewsTheme { HomeFeedWithArticleDetailsScreen( - uiState = HomeUiState.HasPosts( - postsFeed = postsFeed, - selectedPost = postsFeed.highlightedPost, - isArticleOpen = false, - favorites = emptySet(), - isLoading = false, - errorMessages = emptyList(), - searchInput = "" - ), + uiState = + HomeUiState.HasPosts( + postsFeed = postsFeed, + selectedPost = postsFeed.highlightedPost, + isArticleOpen = false, + favorites = emptySet(), + isLoading = false, + errorMessages = emptyList(), + searchInput = "", + ), showTopAppBar = true, onToggleFavorite = {}, onSelectPost = {}, @@ -806,13 +827,14 @@ fun PreviewHomeListDetailScreen() { onInteractWithDetail = {}, openDrawer = {}, homeListLazyListState = rememberLazyListState(), - articleDetailLazyListStates = postsFeed.allPosts.associate { post -> - key(post.id) { - post.id to rememberLazyListState() - } - }, + articleDetailLazyListStates = + postsFeed.allPosts.associate { post -> + key(post.id) { + post.id to rememberLazyListState() + } + }, snackbarHostState = SnackbarHostState(), - onSearchInputChanged = {} + onSearchInputChanged = {}, ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeViewModel.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeViewModel.kt index c112b7f1a8..cff1196227 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeViewModel.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeViewModel.kt @@ -25,13 +25,13 @@ import com.example.jetnews.data.posts.PostsRepository import com.example.jetnews.model.Post import com.example.jetnews.model.PostsFeed import com.example.jetnews.utils.ErrorMessage -import java.util.UUID import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.util.UUID /** * UI state for the Home route. @@ -40,7 +40,6 @@ import kotlinx.coroutines.launch * precisely represent the state available to render the UI. */ sealed interface HomeUiState { - val isLoading: Boolean val errorMessages: List val searchInput: String @@ -54,7 +53,7 @@ sealed interface HomeUiState { data class NoPosts( override val isLoading: Boolean, override val errorMessages: List, - override val searchInput: String + override val searchInput: String, ) : HomeUiState /** @@ -69,7 +68,7 @@ sealed interface HomeUiState { val favorites: Set, override val isLoading: Boolean, override val errorMessages: List, - override val searchInput: String + override val searchInput: String, ) : HomeUiState } @@ -85,7 +84,6 @@ private data class HomeViewModelState( val errorMessages: List = emptyList(), val searchInput: String = "", ) { - /** * Converts this [HomeViewModelState] into a more strongly typed [HomeUiState] for driving * the ui. @@ -95,7 +93,7 @@ private data class HomeViewModelState( HomeUiState.NoPosts( isLoading = isLoading, errorMessages = errorMessages, - searchInput = searchInput + searchInput = searchInput, ) } else { HomeUiState.HasPosts( @@ -103,14 +101,15 @@ private data class HomeViewModelState( // Determine the selected post. This will be the post the user last selected. // If there is none (or that post isn't in the current feed), default to the // highlighted post - selectedPost = postsFeed.allPosts.find { - it.id == selectedPostId - } ?: postsFeed.highlightedPost, + selectedPost = + postsFeed.allPosts.find { + it.id == selectedPostId + } ?: postsFeed.highlightedPost, isArticleOpen = isArticleOpen, favorites = favorites, isLoading = isLoading, errorMessages = errorMessages, - searchInput = searchInput + searchInput = searchInput, ) } } @@ -120,25 +119,26 @@ private data class HomeViewModelState( */ class HomeViewModel( private val postsRepository: PostsRepository, - preSelectedPostId: String? + preSelectedPostId: String?, ) : ViewModel() { - - private val viewModelState = MutableStateFlow( - HomeViewModelState( - isLoading = true, - selectedPostId = preSelectedPostId, - isArticleOpen = preSelectedPostId != null + private val viewModelState = + MutableStateFlow( + HomeViewModelState( + isLoading = true, + selectedPostId = preSelectedPostId, + isArticleOpen = preSelectedPostId != null, + ), ) - ) // UI state exposed to the UI - val uiState = viewModelState - .map(HomeViewModelState::toUiState) - .stateIn( - viewModelScope, - SharingStarted.Eagerly, - viewModelState.value.toUiState() - ) + val uiState = + viewModelState + .map(HomeViewModelState::toUiState) + .stateIn( + viewModelScope, + SharingStarted.Eagerly, + viewModelState.value.toUiState(), + ) init { refreshPosts() @@ -164,10 +164,12 @@ class HomeViewModel( when (result) { is Result.Success -> it.copy(postsFeed = result.data, isLoading = false) is Result.Error -> { - val errorMessages = it.errorMessages + ErrorMessage( - id = UUID.randomUUID().mostSignificantBits, - messageId = R.string.load_error - ) + val errorMessages = + it.errorMessages + + ErrorMessage( + id = UUID.randomUUID().mostSignificantBits, + messageId = R.string.load_error, + ) it.copy(errorMessages = errorMessages, isLoading = false) } } @@ -218,7 +220,7 @@ class HomeViewModel( viewModelState.update { it.copy( selectedPostId = postId, - isArticleOpen = true + isArticleOpen = true, ) } } @@ -238,12 +240,11 @@ class HomeViewModel( companion object { fun provideFactory( postsRepository: PostsRepository, - preSelectedPostId: String? = null - ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return HomeViewModel(postsRepository, preSelectedPostId) as T + preSelectedPostId: String? = null, + ): ViewModelProvider.Factory = + object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = HomeViewModel(postsRepository, preSelectedPostId) as T } - } } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardTop.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardTop.kt index 9267c0f3d2..a6ad0219f3 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardTop.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardTop.kt @@ -41,45 +41,52 @@ import com.example.jetnews.ui.theme.JetnewsTheme import com.example.jetnews.utils.CompletePreviews @Composable -fun PostCardTop(post: Post, modifier: Modifier = Modifier) { +fun PostCardTop( + post: Post, + modifier: Modifier = Modifier, +) { // TUTORIAL CONTENT STARTS HERE val typography = MaterialTheme.typography Column( - modifier = modifier - .fillMaxWidth() - .padding(16.dp) + modifier = + modifier + .fillMaxWidth() + .padding(16.dp), ) { - val imageModifier = Modifier - .heightIn(min = 180.dp) - .fillMaxWidth() - .clip(shape = MaterialTheme.shapes.large) + val imageModifier = + Modifier + .heightIn(min = 180.dp) + .fillMaxWidth() + .clip(shape = MaterialTheme.shapes.large) Image( painter = painterResource(post.imageId), contentDescription = null, // decorative modifier = imageModifier, - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, ) Spacer(Modifier.height(16.dp)) Text( text = post.title, style = typography.titleLarge, - modifier = Modifier.padding(bottom = 8.dp) + modifier = Modifier.padding(bottom = 8.dp), ) Text( text = post.metadata.author.name, style = typography.labelLarge, - modifier = Modifier.padding(bottom = 4.dp) + modifier = Modifier.padding(bottom = 4.dp), ) Text( - text = stringResource( - id = R.string.home_post_min_read, - formatArgs = arrayOf( - post.metadata.date, - post.metadata.readTimeMinutes - ) - ), - style = typography.bodySmall + text = + stringResource( + id = R.string.home_post_min_read, + formatArgs = + arrayOf( + post.metadata.date, + post.metadata.readTimeMinutes, + ), + ), + style = typography.bodySmall, ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardYourNetwork.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardYourNetwork.kt index aa8569cec6..af597c865d 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardYourNetwork.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardYourNetwork.kt @@ -54,22 +54,24 @@ import com.example.jetnews.ui.theme.JetnewsTheme fun PostCardPopular( post: Post, navigateToArticle: (String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Card( onClick = { navigateToArticle(post.id) }, shape = MaterialTheme.shapes.medium, - modifier = modifier - .width(280.dp) + modifier = + modifier + .width(280.dp), ) { Column { Image( painter = painterResource(post.imageId), contentDescription = null, // decorative contentScale = ContentScale.Crop, - modifier = Modifier - .height(100.dp) - .fillMaxWidth() + modifier = + Modifier + .height(100.dp) + .fillMaxWidth(), ) Column(modifier = Modifier.padding(16.dp)) { @@ -77,25 +79,27 @@ fun PostCardPopular( text = post.title, style = MaterialTheme.typography.headlineSmall, maxLines = 2, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) Spacer(modifier = Modifier.weight(1f)) Text( text = post.metadata.author.name, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) Text( - text = stringResource( - id = R.string.home_post_min_read, - formatArgs = arrayOf( - post.metadata.date, - post.metadata.readTimeMinutes - ) - ), - style = MaterialTheme.typography.bodySmall + text = + stringResource( + id = R.string.home_post_min_read, + formatArgs = + arrayOf( + post.metadata.date, + post.metadata.readTimeMinutes, + ), + ), + style = MaterialTheme.typography.bodySmall, ) } } @@ -106,7 +110,7 @@ fun PostCardPopular( @Preview("Dark colors", uiMode = UI_MODE_NIGHT_YES) @Composable fun PreviewPostCardPopular( - @PreviewParameter(PostPreviewParameterProvider::class, limit = 1) post: Post + @PreviewParameter(PostPreviewParameterProvider::class, limit = 1) post: Post, ) { JetnewsTheme { Surface { @@ -118,7 +122,7 @@ fun PreviewPostCardPopular( @Preview("Regular colors, long text") @Composable fun PreviewPostCardPopularLongText( - @PreviewParameter(PostPreviewParameterProvider::class, limit = 1) post: Post + @PreviewParameter(PostPreviewParameterProvider::class, limit = 1) post: Post, ) { val loremIpsum = """ @@ -134,12 +138,13 @@ fun PreviewPostCardPopularLongText( PostCardPopular( post.copy( title = "Title$loremIpsum", - metadata = post.metadata.copy( - author = PostAuthor("Author: $loremIpsum"), - readTimeMinutes = Int.MAX_VALUE - ) + metadata = + post.metadata.copy( + author = PostAuthor("Author: $loremIpsum"), + readTimeMinutes = Int.MAX_VALUE, + ), ), - {} + {}, ) } } @@ -164,7 +169,12 @@ fun PreviewPostCardPopularLongText( * be the right place to instantiate dummy instances. */ class PostPreviewParameterProvider : PreviewParameterProvider { - override val values = sequenceOf( - post1, post2, post3, post4, post5 - ) + override val values = + sequenceOf( + post1, + post2, + post3, + post4, + post5, + ) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt index 6956bc6fcb..3f94194b27 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt @@ -56,30 +56,36 @@ import com.example.jetnews.ui.utils.BookmarkButton @Composable fun AuthorAndReadTime( post: Post, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row(modifier) { Text( - text = stringResource( - id = R.string.home_post_min_read, - formatArgs = arrayOf( - post.metadata.author.name, - post.metadata.readTimeMinutes - ) - ), - style = MaterialTheme.typography.bodyMedium + text = + stringResource( + id = R.string.home_post_min_read, + formatArgs = + arrayOf( + post.metadata.author.name, + post.metadata.readTimeMinutes, + ), + ), + style = MaterialTheme.typography.bodyMedium, ) } } @Composable -fun PostImage(post: Post, modifier: Modifier = Modifier) { +fun PostImage( + post: Post, + modifier: Modifier = Modifier, +) { Image( painter = painterResource(post.imageThumbId), contentDescription = null, // decorative - modifier = modifier - .size(40.dp, 40.dp) - .clip(MaterialTheme.shapes.small) + modifier = + modifier + .size(40.dp, 40.dp) + .clip(MaterialTheme.shapes.small), ) } @@ -98,29 +104,35 @@ fun PostCardSimple( post: Post, navigateToArticle: (String) -> Unit, isFavorite: Boolean, - onToggleFavorite: () -> Unit + onToggleFavorite: () -> Unit, ) { val bookmarkAction = stringResource(if (isFavorite) R.string.unbookmark else R.string.bookmark) Row( - modifier = Modifier - .clickable(onClick = { navigateToArticle(post.id) }) - .semantics { - // By defining a custom action, we tell accessibility services that this whole - // composable has an action attached to it. The accessibility service can choose - // how to best communicate this action to the user. - customActions = listOf( - CustomAccessibilityAction( - label = bookmarkAction, - action = { onToggleFavorite(); true } - ) - ) - } + modifier = + Modifier + .clickable(onClick = { navigateToArticle(post.id) }) + .semantics { + // By defining a custom action, we tell accessibility services that this whole + // composable has an action attached to it. The accessibility service can choose + // how to best communicate this action to the user. + customActions = + listOf( + CustomAccessibilityAction( + label = bookmarkAction, + action = { + onToggleFavorite() + true + }, + ), + ) + }, ) { PostImage(post, Modifier.padding(16.dp)) Column( - modifier = Modifier - .weight(1f) - .padding(vertical = 10.dp) + modifier = + Modifier + .weight(1f) + .padding(vertical = 10.dp), ) { PostTitle(post) AuthorAndReadTime(post) @@ -129,44 +141,48 @@ fun PostCardSimple( isBookmarked = isFavorite, onClick = onToggleFavorite, // Remove button semantics so action can be handled at row level - modifier = Modifier - .clearAndSetSemantics {} - .padding(vertical = 2.dp, horizontal = 6.dp) + modifier = + Modifier + .clearAndSetSemantics {} + .padding(vertical = 2.dp, horizontal = 6.dp), ) } } @Composable -fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) { +fun PostCardHistory( + post: Post, + navigateToArticle: (String) -> Unit, +) { var openDialog by remember { mutableStateOf(false) } Row( Modifier - .clickable(onClick = { navigateToArticle(post.id) }) + .clickable(onClick = { navigateToArticle(post.id) }), ) { PostImage( post = post, - modifier = Modifier.padding(16.dp) + modifier = Modifier.padding(16.dp), ) Column( Modifier .weight(1f) - .padding(vertical = 12.dp) + .padding(vertical = 12.dp), ) { Text( text = stringResource(id = R.string.home_post_based_on_history), - style = MaterialTheme.typography.labelMedium + style = MaterialTheme.typography.labelMedium, ) PostTitle(post = post) AuthorAndReadTime( post = post, - modifier = Modifier.padding(top = 4.dp) + modifier = Modifier.padding(top = 4.dp), ) } IconButton(onClick = { openDialog = true }) { Icon( imageVector = Icons.Filled.MoreVert, - contentDescription = stringResource(R.string.cd_more_actions) + contentDescription = stringResource(R.string.cd_more_actions), ) } } @@ -177,13 +193,13 @@ fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) { title = { Text( text = stringResource(id = R.string.fewer_stories), - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleLarge, ) }, text = { Text( text = stringResource(id = R.string.fewer_stories_content), - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyLarge, ) }, confirmButton = { @@ -191,11 +207,12 @@ fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) { text = stringResource(id = R.string.agree), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(15.dp) - .clickable { openDialog = false } + modifier = + Modifier + .padding(15.dp) + .clickable { openDialog = false }, ) - } + }, ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsRoute.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsRoute.kt index 6766bd2ef4..db4ed05cea 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsRoute.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsRoute.kt @@ -35,12 +35,13 @@ fun InterestsRoute( interestsViewModel: InterestsViewModel, isExpandedScreen: Boolean, openDrawer: () -> Unit, - snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, ) { val tabContent = rememberTabContent(interestsViewModel) - val (currentSection, updateSection) = rememberSaveable { - mutableStateOf(tabContent.first().section) - } + val (currentSection, updateSection) = + rememberSaveable { + mutableStateOf(tabContent.first().section) + } InterestsScreen( tabContent = tabContent, @@ -48,6 +49,6 @@ fun InterestsRoute( isExpandedScreen = isExpandedScreen, onTabChange = updateSection, openDrawer = openDrawer, - snackbarHostState = snackbarHostState + snackbarHostState = snackbarHostState, ) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt index 521034b9f8..cab9f6cb8c 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt @@ -77,13 +77,15 @@ import com.example.jetnews.data.interests.InterestSection import com.example.jetnews.data.interests.TopicSelection import com.example.jetnews.data.interests.impl.FakeInterestsRepository import com.example.jetnews.ui.theme.JetnewsTheme -import kotlin.math.max import kotlinx.coroutines.runBlocking +import kotlin.math.max -enum class Sections(@StringRes val titleResId: Int) { +enum class Sections( + @StringRes val titleResId: Int, +) { Topics(R.string.interests_section_topics), People(R.string.interests_section_people), - Publications(R.string.interests_section_publications) + Publications(R.string.interests_section_publications), } /** @@ -96,7 +98,10 @@ enum class Sections(@StringRes val titleResId: Int) { * @param section the tab that this content is for * @param section content of the tab, a composable that describes the content */ -class TabContent(val section: Sections, val content: @Composable () -> Unit) +class TabContent( + val section: Sections, + val content: @Composable () -> Unit, +) /** * Stateless interest screen displays the tabs specified in [tabContent] adapting the UI to @@ -118,7 +123,7 @@ fun InterestsScreen( isExpandedScreen: Boolean, onTabChange: (Sections) -> Unit, openDrawer: () -> Unit, - snackbarHostState: SnackbarHostState + snackbarHostState: SnackbarHostState, ) { val context = LocalContext.current Scaffold( @@ -129,7 +134,7 @@ fun InterestsScreen( Text( text = stringResource(R.string.cd_interests), style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, ) }, navigationIcon = { @@ -137,9 +142,10 @@ fun InterestsScreen( IconButton(onClick = openDrawer) { Icon( painter = painterResource(R.drawable.ic_jetnews_logo), - contentDescription = stringResource( - R.string.cd_open_navigation_drawer - ), + contentDescription = + stringResource( + R.string.cd_open_navigation_drawer, + ), ) } } @@ -147,26 +153,30 @@ fun InterestsScreen( actions = { IconButton( onClick = { - Toast.makeText( - context, - "Search is not yet implemented in this configuration", - Toast.LENGTH_LONG - ).show() - } + Toast + .makeText( + context, + "Search is not yet implemented in this configuration", + Toast.LENGTH_LONG, + ).show() + }, ) { Icon( imageVector = Icons.Filled.Search, - contentDescription = stringResource(R.string.cd_search) + contentDescription = stringResource(R.string.cd_search), ) } - } + }, ) - } + }, ) { innerPadding -> val screenModifier = Modifier.padding(innerPadding) InterestScreenContent( - currentSection, isExpandedScreen, - onTabChange, tabContent, screenModifier + currentSection, + isExpandedScreen, + onTabChange, + tabContent, + screenModifier, ) } } @@ -182,33 +192,36 @@ fun rememberTabContent(interestsViewModel: InterestsViewModel): List // Describe the screen sections here since each section needs 2 states and 1 event. // Pass them to the stateless InterestsScreen using a tabContent. - val topicsSection = TabContent(Sections.Topics) { - val selectedTopics by interestsViewModel.selectedTopics.collectAsStateWithLifecycle() - TabWithSections( - sections = uiState.topics, - selectedTopics = selectedTopics, - onTopicSelect = { interestsViewModel.toggleTopicSelection(it) } - ) - } + val topicsSection = + TabContent(Sections.Topics) { + val selectedTopics by interestsViewModel.selectedTopics.collectAsStateWithLifecycle() + TabWithSections( + sections = uiState.topics, + selectedTopics = selectedTopics, + onTopicSelect = { interestsViewModel.toggleTopicSelection(it) }, + ) + } - val peopleSection = TabContent(Sections.People) { - val selectedPeople by interestsViewModel.selectedPeople.collectAsStateWithLifecycle() - TabWithTopics( - topics = uiState.people, - selectedTopics = selectedPeople, - onTopicSelect = { interestsViewModel.togglePersonSelected(it) } - ) - } + val peopleSection = + TabContent(Sections.People) { + val selectedPeople by interestsViewModel.selectedPeople.collectAsStateWithLifecycle() + TabWithTopics( + topics = uiState.people, + selectedTopics = selectedPeople, + onTopicSelect = { interestsViewModel.togglePersonSelected(it) }, + ) + } - val publicationSection = TabContent(Sections.Publications) { - val selectedPublications by interestsViewModel.selectedPublications - .collectAsStateWithLifecycle() - TabWithTopics( - topics = uiState.publications, - selectedTopics = selectedPublications, - onTopicSelect = { interestsViewModel.togglePublicationSelected(it) } - ) - } + val publicationSection = + TabContent(Sections.Publications) { + val selectedPublications by interestsViewModel.selectedPublications + .collectAsStateWithLifecycle() + TabWithTopics( + topics = uiState.publications, + selectedTopics = selectedPublications, + onTopicSelect = { interestsViewModel.togglePublicationSelected(it) }, + ) + } return listOf(topicsSection, peopleSection, publicationSection) } @@ -228,13 +241,13 @@ private fun InterestScreenContent( isExpandedScreen: Boolean, updateSection: (Sections) -> Unit, tabContent: List, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val selectedTabIndex = tabContent.indexOfFirst { it.section == currentSection } Column(modifier) { InterestsTabRow(selectedTabIndex, updateSection, tabContent, isExpandedScreen) HorizontalDivider( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f), ) Box(modifier = Modifier.weight(1f)) { // display the current tab content which is a @Composable () -> Unit @@ -246,9 +259,10 @@ private fun InterestScreenContent( /** * Modifier for UI containers that show interests items */ -private val tabContainerModifier = Modifier - .fillMaxWidth() - .wrapContentWidth(Alignment.CenterHorizontally) +private val tabContainerModifier = + Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally) /** * Display a simple list of topics @@ -261,11 +275,11 @@ private val tabContainerModifier = Modifier private fun TabWithTopics( topics: List, selectedTopics: Set, - onTopicSelect: (String) -> Unit + onTopicSelect: (String) -> Unit, ) { InterestsAdaptiveContentLayout( topPadding = 16.dp, - modifier = tabContainerModifier.verticalScroll(rememberScrollState()) + modifier = tabContainerModifier.verticalScroll(rememberScrollState()), ) { topics.forEach { topic -> TopicItem( @@ -288,16 +302,17 @@ private fun TabWithTopics( private fun TabWithSections( sections: List, selectedTopics: Set, - onTopicSelect: (TopicSelection) -> Unit + onTopicSelect: (TopicSelection) -> Unit, ) { Column(tabContainerModifier.verticalScroll(rememberScrollState())) { sections.forEach { (section, topics) -> Text( text = section, - modifier = Modifier - .padding(16.dp) - .semantics { heading() }, - style = MaterialTheme.typography.titleMedium + modifier = + Modifier + .padding(16.dp) + .semantics { heading() }, + style = MaterialTheme.typography.titleMedium, ) InterestsAdaptiveContentLayout { topics.forEach { topic -> @@ -324,37 +339,41 @@ private fun TopicItem( itemTitle: String, selected: Boolean, onToggle: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Column(Modifier.padding(horizontal = 16.dp)) { Row( - modifier = modifier.toggleable( - value = selected, - onValueChange = { onToggle() } - ), - verticalAlignment = Alignment.CenterVertically + modifier = + modifier.toggleable( + value = selected, + onValueChange = { onToggle() }, + ), + verticalAlignment = Alignment.CenterVertically, ) { val image = painterResource(R.drawable.placeholder_1_1) Image( painter = image, contentDescription = null, // decorative - modifier = Modifier - .size(56.dp) - .clip(RoundedCornerShape(4.dp)) + modifier = + Modifier + .size(56.dp) + .clip(RoundedCornerShape(4.dp)), ) Text( text = itemTitle, - modifier = Modifier - .padding(16.dp) - .weight(1f), // Break line if the title is too long - style = MaterialTheme.typography.titleMedium + modifier = + Modifier + .padding(16.dp) + .weight(1f), + // Break line if the title is too long + style = MaterialTheme.typography.titleMedium, ) Spacer(Modifier.width(16.dp)) SelectTopicButton(selected = selected) } HorizontalDivider( modifier = modifier.padding(start = 72.dp, top = 8.dp, bottom = 8.dp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f), ) } } @@ -367,13 +386,13 @@ private fun InterestsTabRow( selectedTabIndex: Int, updateSection: (Sections) -> Unit, tabContent: List, - isExpandedScreen: Boolean + isExpandedScreen: Boolean, ) { when (isExpandedScreen) { false -> { TabRow( selectedTabIndex = selectedTabIndex, - contentColor = MaterialTheme.colorScheme.primary + contentColor = MaterialTheme.colorScheme.primary, ) { InterestsTabRowContent(selectedTabIndex, updateSection, tabContent) } @@ -382,13 +401,13 @@ private fun InterestsTabRow( ScrollableTabRow( selectedTabIndex = selectedTabIndex, contentColor = MaterialTheme.colorScheme.primary, - edgePadding = 0.dp + edgePadding = 0.dp, ) { InterestsTabRowContent( selectedTabIndex = selectedTabIndex, updateSection = updateSection, tabContent = tabContent, - modifier = Modifier.padding(horizontal = 8.dp) + modifier = Modifier.padding(horizontal = 8.dp), ) } } @@ -400,24 +419,25 @@ private fun InterestsTabRowContent( selectedTabIndex: Int, updateSection: (Sections) -> Unit, tabContent: List, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { tabContent.forEachIndexed { index, content -> - val colorText = if (selectedTabIndex == index) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) - } + val colorText = + if (selectedTabIndex == index) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + } Tab( selected = selectedTabIndex == index, onClick = { updateSection(content.section) }, - modifier = Modifier.heightIn(min = 48.dp) + modifier = Modifier.heightIn(min = 48.dp), ) { Text( text = stringResource(id = content.section.titleResId), color = colorText, style = MaterialTheme.typography.titleMedium, - modifier = modifier.paddingFromBaseline(top = 20.dp) + modifier = modifier.paddingFromBaseline(top = 20.dp), ) } } @@ -452,24 +472,26 @@ private fun InterestsAdaptiveContentLayout( // the design mocks, but this logic could change in the future. val columns = if (outerConstraints.maxWidth < multipleColumnsBreakPointPx) 1 else 2 // Max width for each item taking into account available space, spacing and `itemMaxWidth` - val itemWidth = if (columns == 1) { - outerConstraints.maxWidth - } else { - val maxWidthWithSpaces = outerConstraints.maxWidth - (columns - 1) * itemSpacingPx - (maxWidthWithSpaces / columns).coerceIn(0, itemMaxWidthPx) - } + val itemWidth = + if (columns == 1) { + outerConstraints.maxWidth + } else { + val maxWidthWithSpaces = outerConstraints.maxWidth - (columns - 1) * itemSpacingPx + (maxWidthWithSpaces / columns).coerceIn(0, itemMaxWidthPx) + } val itemConstraints = outerConstraints.copy(maxWidth = itemWidth) // Keep track of the height of each row to calculate the layout's final size val rowHeights = IntArray(measurables.size / columns + 1) // Measure elements with their maximum width and keep track of the height - val placeables = measurables.mapIndexed { index, measureable -> - val placeable = measureable.measure(itemConstraints) - // Update the height for each row - val row = index.floorDiv(columns) - rowHeights[row] = max(rowHeights[row], placeable.height) - placeable - } + val placeables = + measurables.mapIndexed { index, measureable -> + val placeable = measureable.measure(itemConstraints) + // Update the height for each row + val row = index.floorDiv(columns) + rowHeights[row] = max(rowHeights[row], placeable.height) + placeable + } // Calculate maxHeight of the Interests layout. Heights of the row + top padding val layoutHeight = topPaddingPx + rowHeights.sum() @@ -479,7 +501,7 @@ private fun InterestsAdaptiveContentLayout( // Lay out given the max width and height layout( width = outerConstraints.constrainWidth(layoutWidth), - height = outerConstraints.constrainHeight(layoutHeight) + height = outerConstraints.constrainHeight(layoutHeight), ) { // Track the y co-ord we have placed children up to var yPosition = topPaddingPx @@ -504,9 +526,10 @@ private fun InterestsAdaptiveContentLayout( fun PreviewInterestsScreenDrawer() { JetnewsTheme { val tabContent = getFakeTabsContent() - val (currentSection, updateSection) = rememberSaveable { - mutableStateOf(tabContent.first().section) - } + val (currentSection, updateSection) = + rememberSaveable { + mutableStateOf(tabContent.first().section) + } InterestsScreen( tabContent = tabContent, @@ -514,27 +537,32 @@ fun PreviewInterestsScreenDrawer() { isExpandedScreen = false, onTabChange = updateSection, openDrawer = { }, - snackbarHostState = SnackbarHostState() + snackbarHostState = SnackbarHostState(), ) } } @Preview("Interests screen navrail", "Interests", device = Devices.PIXEL_C) @Preview( - "Interests screen navrail (dark)", "Interests", - uiMode = UI_MODE_NIGHT_YES, device = Devices.PIXEL_C + "Interests screen navrail (dark)", + "Interests", + uiMode = UI_MODE_NIGHT_YES, + device = Devices.PIXEL_C, ) @Preview( - "Interests screen navrail (big font)", "Interests", - fontScale = 1.5f, device = Devices.PIXEL_C + "Interests screen navrail (big font)", + "Interests", + fontScale = 1.5f, + device = Devices.PIXEL_C, ) @Composable fun PreviewInterestsScreenNavRail() { JetnewsTheme { val tabContent = getFakeTabsContent() - val (currentSection, updateSection) = rememberSaveable { - mutableStateOf(tabContent.first().section) - } + val (currentSection, updateSection) = + rememberSaveable { + mutableStateOf(tabContent.first().section) + } InterestsScreen( tabContent = tabContent, @@ -542,7 +570,7 @@ fun PreviewInterestsScreenNavRail() { isExpandedScreen = true, onTabChange = updateSection, openDrawer = { }, - snackbarHostState = SnackbarHostState() + snackbarHostState = SnackbarHostState(), ) } } @@ -551,9 +579,10 @@ fun PreviewInterestsScreenNavRail() { @Preview("Interests screen topics tab (dark)", "Topics", uiMode = UI_MODE_NIGHT_YES) @Composable fun PreviewTopicsTab() { - val topics = runBlocking { - (FakeInterestsRepository().getTopics() as Result.Success).data - } + val topics = + runBlocking { + (FakeInterestsRepository().getTopics() as Result.Success).data + } JetnewsTheme { Surface { TabWithSections(topics, setOf()) { } @@ -565,9 +594,10 @@ fun PreviewTopicsTab() { @Preview("Interests screen people tab (dark)", "People", uiMode = UI_MODE_NIGHT_YES) @Composable fun PreviewPeopleTab() { - val people = runBlocking { - (FakeInterestsRepository().getPeople() as Result.Success).data - } + val people = + runBlocking { + (FakeInterestsRepository().getPeople() as Result.Success).data + } JetnewsTheme { Surface { TabWithTopics(people, setOf()) { } @@ -579,9 +609,10 @@ fun PreviewPeopleTab() { @Preview("Interests screen publications tab (dark)", "Publications", uiMode = UI_MODE_NIGHT_YES) @Composable fun PreviewPublicationsTab() { - val publications = runBlocking { - (FakeInterestsRepository().getPublications() as Result.Success).data - } + val publications = + runBlocking { + (FakeInterestsRepository().getPublications() as Result.Success).data + } JetnewsTheme { Surface { TabWithTopics(publications, setOf()) { } @@ -591,24 +622,27 @@ fun PreviewPublicationsTab() { private fun getFakeTabsContent(): List { val interestsRepository = FakeInterestsRepository() - val topicsSection = TabContent(Sections.Topics) { - TabWithSections( - runBlocking { (interestsRepository.getTopics() as Result.Success).data }, - emptySet() - ) { } - } - val peopleSection = TabContent(Sections.People) { - TabWithTopics( - runBlocking { (interestsRepository.getPeople() as Result.Success).data }, - emptySet() - ) { } - } - val publicationSection = TabContent(Sections.Publications) { - TabWithTopics( - runBlocking { (interestsRepository.getPublications() as Result.Success).data }, - emptySet() - ) { } - } + val topicsSection = + TabContent(Sections.Topics) { + TabWithSections( + runBlocking { (interestsRepository.getTopics() as Result.Success).data }, + emptySet(), + ) { } + } + val peopleSection = + TabContent(Sections.People) { + TabWithTopics( + runBlocking { (interestsRepository.getPeople() as Result.Success).data }, + emptySet(), + ) { } + } + val publicationSection = + TabContent(Sections.Publications) { + TabWithTopics( + runBlocking { (interestsRepository.getPublications() as Result.Success).data }, + emptySet(), + ) { } + } return listOf(topicsSection, peopleSection, publicationSection) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsViewModel.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsViewModel.kt index 57af7de1ed..e5d3f7b6f9 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsViewModel.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsViewModel.kt @@ -43,9 +43,8 @@ data class InterestsUiState( ) class InterestsViewModel( - private val interestsRepository: InterestsRepository + private val interestsRepository: InterestsRepository, ) : ViewModel() { - // UI state exposed to the UI private val _uiState = MutableStateFlow(InterestsUiState(loading = true)) val uiState: StateFlow = _uiState.asStateFlow() @@ -54,21 +53,21 @@ class InterestsViewModel( interestsRepository.observeTopicsSelected().stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), - emptySet() + emptySet(), ) val selectedPeople = interestsRepository.observePeopleSelected().stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), - emptySet() + emptySet(), ) val selectedPublications = interestsRepository.observePublicationSelected().stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), - emptySet() + emptySet(), ) init { @@ -115,7 +114,7 @@ class InterestsViewModel( loading = false, topics = topics, people = people, - publications = publications + publications = publications, ) } } @@ -125,13 +124,10 @@ class InterestsViewModel( * Factory for InterestsViewModel that takes PostsRepository as a dependency */ companion object { - fun provideFactory( - interestsRepository: InterestsRepository, - ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return InterestsViewModel(interestsRepository) as T + fun provideFactory(interestsRepository: InterestsRepository): ViewModelProvider.Factory = + object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = InterestsViewModel(interestsRepository) as T } - } } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/SelectTopicButton.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/SelectTopicButton.kt index 154f895adb..538e955ae6 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/SelectTopicButton.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/SelectTopicButton.kt @@ -37,35 +37,38 @@ import com.example.jetnews.ui.theme.JetnewsTheme @Composable fun SelectTopicButton( modifier: Modifier = Modifier, - selected: Boolean = false + selected: Boolean = false, ) { val icon = if (selected) Icons.Filled.Done else Icons.Filled.Add - val iconColor = if (selected) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.primary - } - val borderColor = if (selected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) - } - val backgroundColor = if (selected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onPrimary - } + val iconColor = + if (selected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.primary + } + val borderColor = + if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) + } + val backgroundColor = + if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onPrimary + } Surface( color = backgroundColor, shape = CircleShape, border = BorderStroke(1.dp, borderColor), - modifier = modifier.size(36.dp, 36.dp) + modifier = modifier.size(36.dp, 36.dp), ) { Image( imageVector = icon, colorFilter = ColorFilter.tint(iconColor), modifier = Modifier.padding(8.dp), - contentDescription = null // toggleable at higher level + contentDescription = null, // toggleable at higher level ) } } @@ -75,7 +78,7 @@ fun SelectTopicButton( @Composable fun SelectTopicButtonPreviewOff() { SelectTopicButtonPreviewTemplate( - selected = false + selected = false, ) } @@ -84,19 +87,17 @@ fun SelectTopicButtonPreviewOff() { @Composable fun SelectTopicButtonPreviewOn() { SelectTopicButtonPreviewTemplate( - selected = true + selected = true, ) } @Composable -private fun SelectTopicButtonPreviewTemplate( - selected: Boolean -) { +private fun SelectTopicButtonPreviewTemplate(selected: Boolean) { JetnewsTheme { Surface { SelectTopicButton( modifier = Modifier.padding(32.dp), - selected = selected + selected = selected, ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/modifiers/KeyEvents.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/modifiers/KeyEvents.kt index 11f4ec573d..35a4a04814 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/modifiers/KeyEvents.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/modifiers/KeyEvents.kt @@ -28,11 +28,15 @@ import androidx.compose.ui.input.key.type * Intercepts a key event rather than passing it on to children */ @OptIn(ExperimentalComposeUiApi::class) -fun Modifier.interceptKey(key: Key, onKeyEvent: () -> Unit): Modifier { - return this.onPreviewKeyEvent { +fun Modifier.interceptKey( + key: Key, + onKeyEvent: () -> Unit, +): Modifier = + this.onPreviewKeyEvent { if (it.key == key && it.type == KeyUp) { // fire onKeyEvent on KeyUp to prevent duplicates onKeyEvent() true - } else it.key == key // only pass the key event to children if it's not the chosen key + } else { + it.key == key // only pass the key event to children if it's not the chosen key + } } -} diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Shape.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Shape.kt index db5e33b888..a0463688d3 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Shape.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Shape.kt @@ -20,8 +20,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp -val JetnewsShapes = Shapes( - small = RoundedCornerShape(4.dp), - medium = RoundedCornerShape(4.dp), - large = RoundedCornerShape(8.dp) -) +val JetnewsShapes = + Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(8.dp), + ) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Theme.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Theme.kt index 20b4c960e1..eb55a05960 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Theme.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Theme.kt @@ -26,70 +26,72 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -val LightColors = lightColorScheme( - primary = md_theme_light_primary, - onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - error = md_theme_light_error, - errorContainer = md_theme_light_errorContainer, - onError = md_theme_light_onError, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - inversePrimary = md_theme_light_inversePrimary, - surfaceTint = md_theme_light_surfaceTint, -) +val LightColors = + lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + ) -val DarkColors = darkColorScheme( - primary = md_theme_dark_primary, - onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - error = md_theme_dark_error, - errorContainer = md_theme_dark_errorContainer, - onError = md_theme_dark_onError, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - inversePrimary = md_theme_dark_inversePrimary, - surfaceTint = md_theme_dark_surfaceTint, -) +val DarkColors = + darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + ) @Composable fun JetnewsTheme( darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -103,6 +105,6 @@ fun JetnewsTheme( colorScheme = colorScheme, shapes = JetnewsShapes, typography = JetnewsTypography, - content = content + content = content, ) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Type.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Type.kt index 71bb56ba6c..c268edd979 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Type.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Type.kt @@ -27,84 +27,131 @@ import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.unit.sp import com.example.jetnews.R -private val Montserrat = FontFamily( - Font(R.font.montserrat_regular), - Font(R.font.montserrat_medium, FontWeight.W500) -) +private val Montserrat = + FontFamily( + Font(R.font.montserrat_regular), + Font(R.font.montserrat_medium, FontWeight.W500), + ) @Suppress("DEPRECATION") -val defaultTextStyle = TextStyle( - fontFamily = Montserrat, - platformStyle = PlatformTextStyle( - includeFontPadding = false - ), - lineHeightStyle = LineHeightStyle( - alignment = LineHeightStyle.Alignment.Center, - trim = LineHeightStyle.Trim.None +val defaultTextStyle = + TextStyle( + fontFamily = Montserrat, + platformStyle = + PlatformTextStyle( + includeFontPadding = false, + ), + lineHeightStyle = + LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ) -) -val JetnewsTypography = Typography( - displayLarge = defaultTextStyle.copy( - fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp - ), - displayMedium = defaultTextStyle.copy( - fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp - ), - displaySmall = defaultTextStyle.copy( - fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp - ), - headlineLarge = defaultTextStyle.copy( - fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading - ), - headlineMedium = defaultTextStyle.copy( - fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading - ), - headlineSmall = defaultTextStyle.copy( - fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading - ), - titleLarge = defaultTextStyle.copy( - fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading - ), - titleMedium = defaultTextStyle.copy( - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp, - fontWeight = FontWeight.Medium, - lineBreak = LineBreak.Heading - ), - titleSmall = defaultTextStyle.copy( - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp, - fontWeight = FontWeight.Medium, - lineBreak = LineBreak.Heading - ), - labelLarge = defaultTextStyle.copy( - fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp, fontWeight = FontWeight.Medium - ), - labelMedium = defaultTextStyle.copy( - fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium - ), - labelSmall = defaultTextStyle.copy( - fontSize = 11.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium - ), - bodyLarge = defaultTextStyle.copy( - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp, - lineBreak = LineBreak.Paragraph - ), - bodyMedium = defaultTextStyle.copy( - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp, - lineBreak = LineBreak.Paragraph - ), - bodySmall = defaultTextStyle.copy( - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp, - lineBreak = LineBreak.Paragraph - ), -) +val JetnewsTypography = + Typography( + displayLarge = + defaultTextStyle.copy( + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp, + ), + displayMedium = + defaultTextStyle.copy( + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp, + ), + displaySmall = + defaultTextStyle.copy( + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp, + ), + headlineLarge = + defaultTextStyle.copy( + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + lineBreak = LineBreak.Heading, + ), + headlineMedium = + defaultTextStyle.copy( + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp, + lineBreak = LineBreak.Heading, + ), + headlineSmall = + defaultTextStyle.copy( + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp, + lineBreak = LineBreak.Heading, + ), + titleLarge = + defaultTextStyle.copy( + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp, + lineBreak = LineBreak.Heading, + ), + titleMedium = + defaultTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading, + ), + titleSmall = + defaultTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading, + ), + labelLarge = + defaultTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + fontWeight = FontWeight.Medium, + ), + labelMedium = + defaultTextStyle.copy( + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + fontWeight = FontWeight.Medium, + ), + labelSmall = + defaultTextStyle.copy( + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + fontWeight = FontWeight.Medium, + ), + bodyLarge = + defaultTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + lineBreak = LineBreak.Paragraph, + ), + bodyMedium = + defaultTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + lineBreak = LineBreak.Paragraph, + ), + bodySmall = + defaultTextStyle.copy( + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + lineBreak = LineBreak.Paragraph, + ), + ) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/utils/JetnewsIcons.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/utils/JetnewsIcons.kt index ce0c69b0ae..475a6fd7f9 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/utils/JetnewsIcons.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/utils/JetnewsIcons.kt @@ -37,7 +37,7 @@ fun FavoriteButton(onClick: () -> Unit) { IconButton(onClick) { Icon( imageVector = Icons.Filled.ThumbUpOffAlt, - contentDescription = stringResource(R.string.cd_add_to_favorites) + contentDescription = stringResource(R.string.cd_add_to_favorites), ) } } @@ -46,23 +46,25 @@ fun FavoriteButton(onClick: () -> Unit) { fun BookmarkButton( isBookmarked: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val clickLabel = stringResource( - if (isBookmarked) R.string.unbookmark else R.string.bookmark - ) + val clickLabel = + stringResource( + if (isBookmarked) R.string.unbookmark else R.string.bookmark, + ) IconToggleButton( checked = isBookmarked, onCheckedChange = { onClick() }, - modifier = modifier.semantics { - // Use a custom click label that accessibility services can communicate to the user. - // We only want to override the label, not the actual action, so for the action we pass null. - this.onClick(label = clickLabel, action = null) - } + modifier = + modifier.semantics { + // Use a custom click label that accessibility services can communicate to the user. + // We only want to override the label, not the actual action, so for the action we pass null. + this.onClick(label = clickLabel, action = null) + }, ) { Icon( imageVector = if (isBookmarked) Icons.Filled.Bookmark else Icons.Filled.BookmarkBorder, - contentDescription = null // handled by click label of parent + contentDescription = null, // handled by click label of parent ) } } @@ -72,7 +74,7 @@ fun ShareButton(onClick: () -> Unit) { IconButton(onClick) { Icon( imageVector = Icons.Filled.Share, - contentDescription = stringResource(R.string.cd_share) + contentDescription = stringResource(R.string.cd_share), ) } } @@ -82,7 +84,7 @@ fun TextSettingsButton(onClick: () -> Unit) { IconButton(onClick) { Icon( painter = painterResource(R.drawable.ic_text_settings), - contentDescription = stringResource(R.string.cd_text_settings) + contentDescription = stringResource(R.string.cd_text_settings), ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/utils/ErrorMessage.kt b/JetNews/app/src/main/java/com/example/jetnews/utils/ErrorMessage.kt index d05a2fdaaa..3317549055 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/utils/ErrorMessage.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/utils/ErrorMessage.kt @@ -18,4 +18,7 @@ package com.example.jetnews.utils import androidx.annotation.StringRes -data class ErrorMessage(val id: Long, @StringRes val messageId: Int) +data class ErrorMessage( + val id: Long, + @StringRes val messageId: Int, +) diff --git a/JetNews/app/src/main/java/com/example/jetnews/utils/MapExtensions.kt b/JetNews/app/src/main/java/com/example/jetnews/utils/MapExtensions.kt index 2b05e67539..408c6641d3 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/utils/MapExtensions.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/utils/MapExtensions.kt @@ -16,10 +16,11 @@ package com.example.jetnews.utils -internal fun Set.addOrRemove(element: E): Set { - return this.toMutableSet().apply { - if (!add(element)) { - remove(element) - } - }.toSet() -} +internal fun Set.addOrRemove(element: E): Set = + this + .toMutableSet() + .apply { + if (!add(element)) { + remove(element) + } + }.toSet() diff --git a/JetNews/app/src/main/java/com/example/jetnews/utils/MultipreviewAnnotations.kt b/JetNews/app/src/main/java/com/example/jetnews/utils/MultipreviewAnnotations.kt index bb2d75912d..9a0853fe3e 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/utils/MultipreviewAnnotations.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/utils/MultipreviewAnnotations.kt @@ -28,12 +28,12 @@ import androidx.compose.ui.tooling.preview.Preview @Preview( name = "small font", group = "font scales", - fontScale = 0.5f + fontScale = 0.5f, ) @Preview( name = "large font", group = "font scales", - fontScale = 1.5f + fontScale = 1.5f, ) annotation class FontScalePreviews @@ -46,17 +46,17 @@ annotation class FontScalePreviews @Preview( name = "phone", group = "devices", - device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480" + device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480", ) @Preview( name = "foldable", group = "devices", - device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480" + device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480", ) @Preview( name = "tablet", group = "devices", - device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480" + device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480", ) annotation class DevicePreviews @@ -75,7 +75,7 @@ annotation class DevicePreviews @Preview( name = "dark theme", group = "themes", - uiMode = UI_MODE_NIGHT_YES + uiMode = UI_MODE_NIGHT_YES, ) @FontScalePreviews @DevicePreviews diff --git a/JetNews/app/src/sharedTest/java/com/example/jetnews/HomeScreenTests.kt b/JetNews/app/src/sharedTest/java/com/example/jetnews/HomeScreenTests.kt index 5d1432ad0d..8a9a8a5e0e 100644 --- a/JetNews/app/src/sharedTest/java/com/example/jetnews/HomeScreenTests.kt +++ b/JetNews/app/src/sharedTest/java/com/example/jetnews/HomeScreenTests.kt @@ -36,7 +36,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class HomeScreenTests { - @get:Rule val composeTestRule = createComposeRule() @@ -48,14 +47,14 @@ class HomeScreenTests { val snackbarHostState = SnackbarHostState() composeTestRule.setContent { JetnewsTheme { - // When the Home screen receives data with an error HomeFeedScreen( - uiState = HomeUiState.NoPosts( - isLoading = false, - errorMessages = listOf(ErrorMessage(0L, R.string.load_error)), - searchInput = "" - ), + uiState = + HomeUiState.NoPosts( + isLoading = false, + errorMessages = listOf(ErrorMessage(0L, R.string.load_error)), + searchInput = "", + ), showTopAppBar = false, onToggleFavorite = {}, onSelectPost = {}, @@ -64,7 +63,7 @@ class HomeScreenTests { openDrawer = {}, homeListLazyListState = rememberLazyListState(), snackbarHostState = snackbarHostState, - onSearchInputChanged = {} + onSearchInputChanged = {}, ) } } @@ -73,10 +72,16 @@ class HomeScreenTests { runBlocking { // snapshotFlow converts a State to a Kotlin Flow so we can observe it // wait for the first a non-null `currentSnackbarData` - val actualSnackbarText = snapshotFlow { snackbarHostState.currentSnackbarData } - .filterNotNull().first().visuals.message - val expectedSnackbarText = InstrumentationRegistry.getInstrumentation() - .targetContext.resources.getString(R.string.load_error) + val actualSnackbarText = + snapshotFlow { snackbarHostState.currentSnackbarData } + .filterNotNull() + .first() + .visuals.message + val expectedSnackbarText = + InstrumentationRegistry + .getInstrumentation() + .targetContext.resources + .getString(R.string.load_error) assertEquals(expectedSnackbarText, actualSnackbarText) } } diff --git a/JetNews/app/src/sharedTest/java/com/example/jetnews/JetnewsTests.kt b/JetNews/app/src/sharedTest/java/com/example/jetnews/JetnewsTests.kt index ed48a5ecc3..ba7b44adca 100644 --- a/JetNews/app/src/sharedTest/java/com/example/jetnews/JetnewsTests.kt +++ b/JetNews/app/src/sharedTest/java/com/example/jetnews/JetnewsTests.kt @@ -32,7 +32,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class JetnewsTests { - @get:Rule val composeTestRule = createComposeRule() @@ -49,7 +48,6 @@ class JetnewsTests { @Test fun app_opensArticle() { - println(composeTestRule.onRoot().printToString()) composeTestRule.onAllNodes(hasText("Manuel Vivo", substring = true))[0].performClick() @@ -64,10 +62,11 @@ class JetnewsTests { @Test fun app_opensInterests() { - composeTestRule.onNodeWithContentDescription( - label = "Open navigation drawer", - useUnmergedTree = true - ).performClick() + composeTestRule + .onNodeWithContentDescription( + label = "Open navigation drawer", + useUnmergedTree = true, + ).performClick() composeTestRule.onNodeWithText("Interests").performClick() // TODO - this fails on CI but not locally. (https://github.com/android/compose-samples/issues/1442) // composeTestRule.waitUntilAtLeastOneExists(hasText("Topics"), 5000L) diff --git a/JetNews/app/src/sharedTest/java/com/example/jetnews/TestAppContainer.kt b/JetNews/app/src/sharedTest/java/com/example/jetnews/TestAppContainer.kt index 2b8ed36c6c..e1abd49414 100644 --- a/JetNews/app/src/sharedTest/java/com/example/jetnews/TestAppContainer.kt +++ b/JetNews/app/src/sharedTest/java/com/example/jetnews/TestAppContainer.kt @@ -23,8 +23,9 @@ import com.example.jetnews.data.interests.impl.FakeInterestsRepository import com.example.jetnews.data.posts.PostsRepository import com.example.jetnews.data.posts.impl.BlockingFakePostsRepository -class TestAppContainer(private val context: Context) : AppContainer { - +class TestAppContainer( + private val context: Context, +) : AppContainer { override val postsRepository: PostsRepository by lazy { BlockingFakePostsRepository() } diff --git a/JetNews/app/src/sharedTest/java/com/example/jetnews/TestHelper.kt b/JetNews/app/src/sharedTest/java/com/example/jetnews/TestHelper.kt index 1e7e115d3b..c2efae1037 100644 --- a/JetNews/app/src/sharedTest/java/com/example/jetnews/TestHelper.kt +++ b/JetNews/app/src/sharedTest/java/com/example/jetnews/TestHelper.kt @@ -28,7 +28,7 @@ fun ComposeContentTestRule.launchJetNewsApp(context: Context) { setContent { JetnewsApp( appContainer = TestAppContainer(context), - widthSizeClass = WindowWidthSizeClass.Compact + widthSizeClass = WindowWidthSizeClass.Compact, ) } } diff --git a/JetNews/build.gradle.kts b/JetNews/build.gradle.kts index 08ccea3e70..a776efc594 100644 --- a/JetNews/build.gradle.kts +++ b/JetNews/build.gradle.kts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import com.diffplug.gradle.spotless.SpotlessExtension plugins { alias(libs.plugins.gradle.versions) @@ -21,6 +22,30 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.compose) apply false + alias(libs.plugins.spotless) } apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") + +subprojects { + apply { + plugin(rootProject.libs.plugins.spotless.get().pluginId) + } + configure { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt") + ktlint(libs.versions.ktlint.get()) + .setEditorConfigPath("${project.rootDir}/.editorconfig") + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } + // Additional configuration for Kotlin Gradle scripts + kotlinGradle { + target("**/*.kts") + targetExclude("**/build/**/*.kts") + ktlint(libs.versions.ktlint.get()) // Apply ktlint to Gradle Kotlin scripts + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") + } + } +} diff --git a/JetNews/buildscripts/init.gradle.kts b/JetNews/buildscripts/init.gradle.kts deleted file mode 100644 index 1b7a54264c..0000000000 --- a/JetNews/buildscripts/init.gradle.kts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -val ktlintVersion = "0.46.1" - -initscript { - val spotlessVersion = "6.10.0" - - repositories { - mavenCentral() - } - - dependencies { - classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion") - } -} - -allprojects { - if (this == rootProject) { - return@allprojects - } - apply() - extensions.configure { - kotlin { - target("**/*.kt") - targetExclude("**/build/**/*.kt") - ktlint(ktlintVersion).editorConfigOverride( - mapOf( - "ktlint_code_style" to "android", - "ij_kotlin_allow_trailing_comma" to true, - // These rules were introduced in ktlint 0.46.0 and should not be - // enabled without further discussion. They are disabled for now. - // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 - "disabled_rules" to - "filename," + - "annotation,annotation-spacing," + - "argument-list-wrapping," + - "double-colon-spacing," + - "enum-entry-name-case," + - "multiline-if-else," + - "no-empty-first-line-in-method-block," + - "package-name," + - "trailing-comma," + - "spacing-around-angle-brackets," + - "spacing-between-declarations-with-annotations," + - "spacing-between-declarations-with-comments," + - "unary-op-spacing" - ) - ) - licenseHeaderFile(rootProject.file("spotless/copyright.kt")) - } - format("kts") { - target("**/*.kts") - targetExclude("**/build/**/*.kts") - // Look for the first line that doesn't have a block comment (assumed to be the license) - licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") - } - } -} \ No newline at end of file diff --git a/JetNews/gradle/libs.versions.toml b/JetNews/gradle/libs.versions.toml index 2c34e54e54..016f154a0d 100644 --- a/JetNews/gradle/libs.versions.toml +++ b/JetNews/gradle/libs.versions.toml @@ -47,6 +47,7 @@ junit = "4.13.2" kotlin = "2.0.0" kotlinx_immutable = "0.3.7" ksp = "2.0.0-1.0.21" +ktlint = "1.3.1" maps-compose = "3.1.1" # @keep minSdk = "21" @@ -57,6 +58,7 @@ roborazzi = "1.12.0" rome = "1.18.0" room = "2.6.0" secrets = "2.0.1" +spotless = "6.25.0" # @keep targetSdk = "33" version-catalog-update = "0.8.4" @@ -179,4 +181,5 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } diff --git a/Jetcaster/.editorconfig b/Jetcaster/.editorconfig new file mode 100644 index 0000000000..3e9786e913 --- /dev/null +++ b/Jetcaster/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*.{kt,kts}] +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL + +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ktlint_standard_property-naming = disabled +ktlint_standard_backing-property-naming = disabled +ktlint_function_naming_ignore_when_annotated_with = Composable diff --git a/Jetcaster/build.gradle.kts b/Jetcaster/build.gradle.kts index 8488c7159d..3b96484ca4 100644 --- a/Jetcaster/build.gradle.kts +++ b/Jetcaster/build.gradle.kts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import com.diffplug.gradle.spotless.SpotlessExtension plugins { alias(libs.plugins.gradle.versions) @@ -24,6 +25,31 @@ plugins { alias(libs.plugins.hilt) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.compose) apply false + alias(libs.plugins.spotless) } apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") + + +subprojects { + apply { + plugin(rootProject.libs.plugins.spotless.get().pluginId) + } + configure { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt") + ktlint(libs.versions.ktlint.get()) + .setEditorConfigPath("${project.rootDir}/.editorconfig") + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } + // Additional configuration for Kotlin Gradle scripts + kotlinGradle { + target("**/*.kts") + targetExclude("**/build/**/*.kts") + ktlint(libs.versions.ktlint.get()) // Apply ktlint to Gradle Kotlin scripts + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") + } + } +} diff --git a/Jetcaster/buildscripts/init.gradle.kts b/Jetcaster/buildscripts/init.gradle.kts deleted file mode 100644 index 1b7a54264c..0000000000 --- a/Jetcaster/buildscripts/init.gradle.kts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -val ktlintVersion = "0.46.1" - -initscript { - val spotlessVersion = "6.10.0" - - repositories { - mavenCentral() - } - - dependencies { - classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion") - } -} - -allprojects { - if (this == rootProject) { - return@allprojects - } - apply() - extensions.configure { - kotlin { - target("**/*.kt") - targetExclude("**/build/**/*.kt") - ktlint(ktlintVersion).editorConfigOverride( - mapOf( - "ktlint_code_style" to "android", - "ij_kotlin_allow_trailing_comma" to true, - // These rules were introduced in ktlint 0.46.0 and should not be - // enabled without further discussion. They are disabled for now. - // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 - "disabled_rules" to - "filename," + - "annotation,annotation-spacing," + - "argument-list-wrapping," + - "double-colon-spacing," + - "enum-entry-name-case," + - "multiline-if-else," + - "no-empty-first-line-in-method-block," + - "package-name," + - "trailing-comma," + - "spacing-around-angle-brackets," + - "spacing-between-declarations-with-annotations," + - "spacing-between-declarations-with-comments," + - "unary-op-spacing" - ) - ) - licenseHeaderFile(rootProject.file("spotless/copyright.kt")) - } - format("kts") { - target("**/*.kts") - targetExclude("**/build/**/*.kts") - // Look for the first line that doesn't have a block comment (assumed to be the license) - licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") - } - } -} \ No newline at end of file diff --git a/Jetcaster/core/data-testing/build.gradle.kts b/Jetcaster/core/data-testing/build.gradle.kts index a8644e1c1b..29d8735a5e 100644 --- a/Jetcaster/core/data-testing/build.gradle.kts +++ b/Jetcaster/core/data-testing/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -5,10 +22,16 @@ plugins { android { namespace = "com.example.jetcaster.core.data.testing" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -19,7 +42,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } diff --git a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestCategoryStore.kt b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestCategoryStore.kt index 60de97944d..a7380ab914 100644 --- a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestCategoryStore.kt +++ b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestCategoryStore.kt @@ -32,33 +32,36 @@ import kotlinx.coroutines.flow.update * // TODO: Move to :testing module upon merging PR #1379 */ class TestCategoryStore : CategoryStore { - private val categoryFlow = MutableStateFlow>(emptyList()) private val podcastsInCategoryFlow = MutableStateFlow>>(emptyMap()) private val episodesFromPodcasts = MutableStateFlow>>(emptyMap()) - override fun categoriesSortedByPodcastCount(limit: Int): Flow> = - categoryFlow + override fun categoriesSortedByPodcastCount(limit: Int): Flow> = categoryFlow override fun podcastsInCategorySortedByPodcastCount( categoryId: Long, - limit: Int - ): Flow> = podcastsInCategoryFlow.map { - it[categoryId]?.take(limit) ?: emptyList() - } + limit: Int, + ): Flow> = + podcastsInCategoryFlow.map { + it[categoryId]?.take(limit) ?: emptyList() + } override fun episodesFromPodcastsInCategory( categoryId: Long, - limit: Int - ): Flow> = episodesFromPodcasts.map { - it[categoryId]?.take(limit) ?: emptyList() - } + limit: Int, + ): Flow> = + episodesFromPodcasts.map { + it[categoryId]?.take(limit) ?: emptyList() + } override suspend fun addCategory(category: Category): Long = -1 - override suspend fun addPodcastToCategory(podcastUri: String, categoryId: Long) {} + override suspend fun addPodcastToCategory( + podcastUri: String, + categoryId: Long, + ) {} override fun getCategory(name: String): Flow = flowOf() @@ -73,7 +76,10 @@ class TestCategoryStore : CategoryStore { * Test-only API for setting the list of podcasts in a category backed by this * [TestCategoryStore]. */ - fun setPodcastsInCategory(categoryId: Long, podcastsInCategory: List) { + fun setPodcastsInCategory( + categoryId: Long, + podcastsInCategory: List, + ) { podcastsInCategoryFlow.update { it + Pair(categoryId, podcastsInCategory) } @@ -83,7 +89,10 @@ class TestCategoryStore : CategoryStore { * Test-only API for setting the list of podcasts in a category backed by this * [TestCategoryStore]. */ - fun setEpisodesFromPodcast(categoryId: Long, podcastsInCategory: List) { + fun setEpisodesFromPodcast( + categoryId: Long, + podcastsInCategory: List, + ) { episodesFromPodcasts.update { it + Pair(categoryId, podcastsInCategory) } diff --git a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestEpisodeStore.kt b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestEpisodeStore.kt index 9dd7c526ea..988e459db9 100644 --- a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestEpisodeStore.kt +++ b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestEpisodeStore.kt @@ -27,8 +27,8 @@ import kotlinx.coroutines.flow.update // TODO: Move to :testing module upon merging PR #1379 class TestEpisodeStore : EpisodeStore { - private val episodesFlow = MutableStateFlow>(listOf()) + override fun episodeWithUri(episodeUri: String): Flow = episodesFlow.map { episodes -> episodes.first { it.uri == episodeUri } @@ -36,38 +36,44 @@ class TestEpisodeStore : EpisodeStore { override fun episodeAndPodcastWithUri(episodeUri: String): Flow = episodesFlow.map { episodes -> - val e = episodes.first { - it.uri == episodeUri - } + val e = + episodes.first { + it.uri == episodeUri + } EpisodeToPodcast().apply { episode = e - _podcasts = emptyList() + podcasts = emptyList() } } - override fun episodesInPodcast(podcastUri: String, limit: Int): Flow> = + override fun episodesInPodcast( + podcastUri: String, + limit: Int, + ): Flow> = episodesFlow.map { episodes -> - episodes.filter { - it.podcastUri == podcastUri - }.map { e -> - EpisodeToPodcast().apply { - episode = e + episodes + .filter { + it.podcastUri == podcastUri + }.map { e -> + EpisodeToPodcast().apply { + episode = e + } } - } } override fun episodesInPodcasts( podcastUris: List, - limit: Int + limit: Int, ): Flow> = episodesFlow.map { episodes -> - episodes.filter { - podcastUris.contains(it.podcastUri) - }.map { ep -> - EpisodeToPodcast().apply { - episode = ep + episodes + .filter { + podcastUris.contains(it.podcastUri) + }.map { ep -> + EpisodeToPodcast().apply { + episode = ep + } } - } } override suspend fun addEpisodes(episodes: Collection) = @@ -75,6 +81,5 @@ class TestEpisodeStore : EpisodeStore { it + episodes } - override suspend fun isEmpty(): Boolean = - episodesFlow.first().isEmpty() + override suspend fun isEmpty(): Boolean = episodesFlow.first().isEmpty() } diff --git a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestPodcastStore.kt b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestPodcastStore.kt index 8a4808ecfd..7df757a582 100644 --- a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestPodcastStore.kt +++ b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestPodcastStore.kt @@ -28,9 +28,9 @@ import kotlinx.coroutines.flow.update // TODO: Move to :testing module upon merging PR #1379 class TestPodcastStore : PodcastStore { - private val podcastFlow = MutableStateFlow>(listOf()) private val followedPodcasts = mutableSetOf() + override fun podcastWithUri(uri: String): Flow = podcastFlow.map { podcasts -> podcasts.first { it.uri == uri } @@ -56,45 +56,48 @@ class TestPodcastStore : PodcastStore { override fun followedPodcastsSortedByLastEpisode(limit: Int): Flow> = podcastFlow.map { podcasts -> - podcasts.filter { - followedPodcasts.contains(it.uri) - }.map { p -> - PodcastWithExtraInfo().apply { - podcast = p - isFollowed = true + podcasts + .filter { + followedPodcasts.contains(it.uri) + }.map { p -> + PodcastWithExtraInfo().apply { + podcast = p + isFollowed = true + } } - } } override fun searchPodcastByTitle( keyword: String, - limit: Int + limit: Int, ): Flow> = podcastFlow.map { podcastList -> - podcastList.filter { - it.title.contains(keyword) - }.map { p -> - PodcastWithExtraInfo().apply { - podcast = p - isFollowed = true + podcastList + .filter { + it.title.contains(keyword) + }.map { p -> + PodcastWithExtraInfo().apply { + podcast = p + isFollowed = true + } } - } } override fun searchPodcastByTitleAndCategories( keyword: String, categories: List, - limit: Int + limit: Int, ): Flow> = podcastFlow.map { podcastList -> - podcastList.filter { - it.title.contains(keyword) - }.map { p -> - PodcastWithExtraInfo().apply { - podcast = p - isFollowed = true + podcastList + .filter { + it.title.contains(keyword) + }.map { p -> + PodcastWithExtraInfo().apply { + podcast = p + isFollowed = true + } } - } } override suspend fun togglePodcastFollowed(podcastUri: String) { @@ -113,9 +116,7 @@ class TestPodcastStore : PodcastStore { followedPodcasts.remove(podcastUri) } - override suspend fun addPodcast(podcast: Podcast) = - podcastFlow.update { it + podcast } + override suspend fun addPodcast(podcast: Podcast) = podcastFlow.update { it + podcast } - override suspend fun isEmpty(): Boolean = - podcastFlow.first().isEmpty() + override suspend fun isEmpty(): Boolean = podcastFlow.first().isEmpty() } diff --git a/Jetcaster/core/data/build.gradle.kts b/Jetcaster/core/data/build.gradle.kts index bd81f036e7..1f891a2109 100644 --- a/Jetcaster/core/data/build.gradle.kts +++ b/Jetcaster/core/data/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -7,10 +24,16 @@ plugins { android { namespace = "com.example.jetcaster.core.data" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/Dispatcher.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/Dispatcher.kt index a57199979c..c1744abf48 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/Dispatcher.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/Dispatcher.kt @@ -20,7 +20,9 @@ import javax.inject.Qualifier @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class Dispatcher(val jetcasterDispatcher: JetcasterDispatchers) +annotation class Dispatcher( + val jetcasterDispatcher: JetcasterDispatchers, +) enum class JetcasterDispatchers { Main, diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/DateTimeTypeConverters.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/DateTimeTypeConverters.kt index 0199678c4c..812fa22a54 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/DateTimeTypeConverters.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/DateTimeTypeConverters.kt @@ -28,37 +28,25 @@ import java.time.format.DateTimeFormatter object DateTimeTypeConverters { @TypeConverter @JvmStatic - fun toOffsetDateTime(value: String?): OffsetDateTime? { - return value?.let { OffsetDateTime.parse(it) } - } + fun toOffsetDateTime(value: String?): OffsetDateTime? = value?.let { OffsetDateTime.parse(it) } @TypeConverter @JvmStatic - fun fromOffsetDateTime(date: OffsetDateTime?): String? { - return date?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) - } + fun fromOffsetDateTime(date: OffsetDateTime?): String? = date?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) @TypeConverter @JvmStatic - fun toLocalDateTime(value: String?): LocalDateTime? { - return value?.let { LocalDateTime.parse(value) } - } + fun toLocalDateTime(value: String?): LocalDateTime? = value?.let { LocalDateTime.parse(value) } @TypeConverter @JvmStatic - fun fromLocalDateTime(value: LocalDateTime?): String? { - return value?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - } + fun fromLocalDateTime(value: LocalDateTime?): String? = value?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) @TypeConverter @JvmStatic - fun toDuration(value: Long?): Duration? { - return value?.let { Duration.ofMillis(it) } - } + fun toDuration(value: Long?): Duration? = value?.let { Duration.ofMillis(it) } @TypeConverter @JvmStatic - fun fromDuration(value: Duration?): Long? { - return value?.toMillis() - } + fun fromDuration(value: Duration?): Long? = value?.toMillis() } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/JetcasterDatabase.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/JetcasterDatabase.kt index ced5d408b0..ac95e282bf 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/JetcasterDatabase.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/JetcasterDatabase.kt @@ -40,17 +40,22 @@ import com.example.jetcaster.core.data.database.model.PodcastFollowedEntry Episode::class, PodcastCategoryEntry::class, Category::class, - PodcastFollowedEntry::class + PodcastFollowedEntry::class, ], version = 1, - exportSchema = false + exportSchema = false, ) @TypeConverters(DateTimeTypeConverters::class) abstract class JetcasterDatabase : RoomDatabase() { abstract fun podcastsDao(): PodcastsDao + abstract fun episodesDao(): EpisodesDao + abstract fun categoriesDao(): CategoriesDao + abstract fun podcastCategoryEntryDao(): PodcastCategoryEntryDao + abstract fun transactionRunnerDao(): TransactionRunnerDao + abstract fun podcastFollowedEntryDao(): PodcastFollowedEntryDao } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/CategoriesDao.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/CategoriesDao.kt index baf958f139..a3b0f7eb3d 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/CategoriesDao.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/CategoriesDao.kt @@ -35,11 +35,9 @@ abstract class CategoriesDao : BaseDao { ) ON category_id = categories.id ORDER BY podcast_count DESC LIMIT :limit - """ + """, ) - abstract fun categoriesSortedByPodcastCount( - limit: Int - ): Flow> + abstract fun categoriesSortedByPodcastCount(limit: Int): Flow> @Query("SELECT * FROM categories WHERE name = :name") abstract suspend fun getCategoryWithName(name: String): Category? diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/EpisodesDao.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/EpisodesDao.kt index e1d60d5f07..0dc495ce0a 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/EpisodesDao.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/EpisodesDao.kt @@ -28,11 +28,10 @@ import kotlinx.coroutines.flow.Flow */ @Dao abstract class EpisodesDao : BaseDao { - @Query( """ SELECT * FROM episodes WHERE uri = :uri - """ + """, ) abstract fun episode(uri: String): Flow @@ -42,7 +41,7 @@ abstract class EpisodesDao : BaseDao { SELECT episodes.* FROM episodes INNER JOIN podcasts ON episodes.podcast_uri = podcasts.uri WHERE episodes.uri = :episodeUri - """ + """, ) abstract fun episodeAndPodcast(episodeUri: String): Flow @@ -52,11 +51,11 @@ abstract class EpisodesDao : BaseDao { SELECT * FROM episodes WHERE podcast_uri = :podcastUri ORDER BY datetime(published) DESC LIMIT :limit - """ + """, ) abstract fun episodesForPodcastUri( podcastUri: String, - limit: Int + limit: Int, ): Flow> @Transaction @@ -67,11 +66,11 @@ abstract class EpisodesDao : BaseDao { WHERE category_id = :categoryId ORDER BY datetime(published) DESC LIMIT :limit - """ + """, ) abstract fun episodesFromPodcastsInCategory( categoryId: Long, - limit: Int + limit: Int, ): Flow> @Query("SELECT COUNT(*) FROM episodes") @@ -83,10 +82,10 @@ abstract class EpisodesDao : BaseDao { SELECT * FROM episodes WHERE podcast_uri IN (:podcastUris) ORDER BY datetime(published) DESC LIMIT :limit - """ + """, ) abstract fun episodesForPodcasts( podcastUris: List, - limit: Int + limit: Int, ): Flow> } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastFollowedEntryDao.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastFollowedEntryDao.kt index 0816cc05e7..c1baff8e42 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastFollowedEntryDao.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastFollowedEntryDao.kt @@ -28,7 +28,5 @@ abstract class PodcastFollowedEntryDao : BaseDao { @Query("SELECT COUNT(*) FROM podcast_followed_entries WHERE podcast_uri = :podcastUri") protected abstract suspend fun podcastFollowRowCount(podcastUri: String): Int - suspend fun isPodcastFollowed(podcastUri: String): Boolean { - return podcastFollowRowCount(podcastUri) > 0 - } + suspend fun isPodcastFollowed(podcastUri: String): Boolean = podcastFollowRowCount(podcastUri) > 0 } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastsDao.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastsDao.kt index 4d5ce71755..416a2febed 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastsDao.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastsDao.kt @@ -44,7 +44,7 @@ abstract class PodcastsDao : BaseDao { LEFT JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = podcasts.uri WHERE podcasts.uri = :podcastUri ORDER BY datetime(last_episode_date) DESC - """ + """, ) abstract fun podcastWithExtraInfo(podcastUri: String): Flow @@ -61,11 +61,9 @@ abstract class PodcastsDao : BaseDao { LEFT JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = episodes.podcast_uri ORDER BY datetime(last_episode_date) DESC LIMIT :limit - """ + """, ) - abstract fun podcastsSortedByLastEpisode( - limit: Int - ): Flow> + abstract fun podcastsSortedByLastEpisode(limit: Int): Flow> @Transaction @Query( @@ -82,11 +80,11 @@ abstract class PodcastsDao : BaseDao { LEFT JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = inner_query.podcast_uri ORDER BY datetime(last_episode_date) DESC LIMIT :limit - """ + """, ) abstract fun podcastsInCategorySortedByLastEpisode( categoryId: Long, - limit: Int + limit: Int, ): Flow> @Transaction @@ -100,11 +98,9 @@ abstract class PodcastsDao : BaseDao { INNER JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = episodes.podcast_uri ORDER BY datetime(last_episode_date) DESC LIMIT :limit - """ + """, ) - abstract fun followedPodcastsSortedByLastEpisode( - limit: Int - ): Flow> + abstract fun followedPodcastsSortedByLastEpisode(limit: Int): Flow> @Transaction @Query( @@ -118,9 +114,12 @@ abstract class PodcastsDao : BaseDao { WHERE podcasts.title LIKE '%' || :keyword || '%' ORDER BY datetime(last_episode_date) DESC LIMIT :limit - """ + """, ) - abstract fun searchPodcastByTitle(keyword: String, limit: Int): Flow> + abstract fun searchPodcastByTitle( + keyword: String, + limit: Int, + ): Flow> @Transaction @Query( @@ -138,12 +137,12 @@ abstract class PodcastsDao : BaseDao { WHERE podcasts.title LIKE '%' || :keyword || '%' ORDER BY datetime(last_episode_date) DESC LIMIT :limit - """ + """, ) abstract fun searchPodcastByTitleAndCategory( keyword: String, categoryIdList: List, - limit: Int + limit: Int, ): Flow> @Query("SELECT COUNT(*) FROM podcasts") diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Category.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Category.kt index 4dff2871ef..9391e859f8 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Category.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Category.kt @@ -25,11 +25,11 @@ import androidx.room.PrimaryKey @Entity( tableName = "categories", indices = [ - Index("name", unique = true) - ] + Index("name", unique = true), + ], ) @Immutable data class Category( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, - @ColumnInfo(name = "name") val name: String + @ColumnInfo(name = "name") val name: String, ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Episode.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Episode.kt index 6a035d9646..884564e97f 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Episode.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Episode.kt @@ -29,7 +29,7 @@ import java.time.OffsetDateTime tableName = "episodes", indices = [ Index("uri", unique = true), - Index("podcast_uri") + Index("podcast_uri"), ], foreignKeys = [ ForeignKey( @@ -37,9 +37,9 @@ import java.time.OffsetDateTime parentColumns = ["uri"], childColumns = ["podcast_uri"], onUpdate = ForeignKey.CASCADE, - onDelete = ForeignKey.CASCADE - ) - ] + onDelete = ForeignKey.CASCADE, + ), + ], ) @Immutable data class Episode( @@ -50,5 +50,5 @@ data class Episode( @ColumnInfo(name = "summary") val summary: String? = null, @ColumnInfo(name = "author") val author: String? = null, @ColumnInfo(name = "published") val published: OffsetDateTime, - @ColumnInfo(name = "duration") val duration: Duration? = null + @ColumnInfo(name = "duration") val duration: Duration? = null, ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/EpisodeToPodcast.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/EpisodeToPodcast.kt index 7945f20316..555e09dd8c 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/EpisodeToPodcast.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/EpisodeToPodcast.kt @@ -26,23 +26,25 @@ class EpisodeToPodcast { lateinit var episode: Episode @Relation(parentColumn = "podcast_uri", entityColumn = "uri") - lateinit var _podcasts: List + lateinit var podcasts: List @get:Ignore val podcast: Podcast - get() = _podcasts[0] + get() = podcasts[0] /** * Allow consumers to destructure this class */ operator fun component1() = episode + operator fun component2() = podcast - override fun equals(other: Any?): Boolean = when { - other === this -> true - other is EpisodeToPodcast -> episode == other.episode && _podcasts == other._podcasts - else -> false - } + override fun equals(other: Any?): Boolean = + when { + other === this -> true + other is EpisodeToPodcast -> episode == other.episode && podcasts == other.podcasts + else -> false + } - override fun hashCode(): Int = Objects.hash(episode, _podcasts) + override fun hashCode(): Int = Objects.hash(episode, podcasts) } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Podcast.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Podcast.kt index 1d86f31f91..8e8d28f1b5 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Podcast.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Podcast.kt @@ -25,8 +25,8 @@ import androidx.room.PrimaryKey @Entity( tableName = "podcasts", indices = [ - Index("uri", unique = true) - ] + Index("uri", unique = true), + ], ) @Immutable data class Podcast( @@ -35,5 +35,5 @@ data class Podcast( @ColumnInfo(name = "description") val description: String? = null, @ColumnInfo(name = "author") val author: String? = null, @ColumnInfo(name = "image_url") val imageUrl: String? = null, - @ColumnInfo(name = "copyright") val copyright: String? = null + @ColumnInfo(name = "copyright") val copyright: String? = null, ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastCategoryEntry.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastCategoryEntry.kt index 3c2c67878d..1a99493f05 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastCategoryEntry.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastCategoryEntry.kt @@ -31,25 +31,25 @@ import androidx.room.PrimaryKey parentColumns = ["id"], childColumns = ["category_id"], onUpdate = ForeignKey.CASCADE, - onDelete = ForeignKey.CASCADE + onDelete = ForeignKey.CASCADE, ), ForeignKey( entity = Podcast::class, parentColumns = ["uri"], childColumns = ["podcast_uri"], onUpdate = ForeignKey.CASCADE, - onDelete = ForeignKey.CASCADE - ) + onDelete = ForeignKey.CASCADE, + ), ], indices = [ Index("podcast_uri", "category_id", unique = true), Index("category_id"), - Index("podcast_uri") - ] + Index("podcast_uri"), + ], ) @Immutable data class PodcastCategoryEntry( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, @ColumnInfo(name = "podcast_uri") val podcastUri: String, - @ColumnInfo(name = "category_id") val categoryId: Long + @ColumnInfo(name = "category_id") val categoryId: Long, ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastFollowedEntry.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastFollowedEntry.kt index 420e68f38f..7452f09a6a 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastFollowedEntry.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastFollowedEntry.kt @@ -31,15 +31,15 @@ import androidx.room.PrimaryKey parentColumns = ["uri"], childColumns = ["podcast_uri"], onUpdate = ForeignKey.CASCADE, - onDelete = ForeignKey.CASCADE - ) + onDelete = ForeignKey.CASCADE, + ), ], indices = [ - Index("podcast_uri", unique = true) - ] + Index("podcast_uri", unique = true), + ], ) @Immutable data class PodcastFollowedEntry( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, - @ColumnInfo(name = "podcast_uri") val podcastUri: String + @ColumnInfo(name = "podcast_uri") val podcastUri: String, ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastWithExtraInfo.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastWithExtraInfo.kt index 8794a46e47..316b34a210 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastWithExtraInfo.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastWithExtraInfo.kt @@ -35,18 +35,21 @@ class PodcastWithExtraInfo { * Allow consumers to destructure this class */ operator fun component1() = podcast + operator fun component2() = lastEpisodeDate + operator fun component3() = isFollowed - override fun equals(other: Any?): Boolean = when { - other === this -> true - other is PodcastWithExtraInfo -> { - podcast == other.podcast && - lastEpisodeDate == other.lastEpisodeDate && - isFollowed == other.isFollowed + override fun equals(other: Any?): Boolean = + when { + other === this -> true + other is PodcastWithExtraInfo -> { + podcast == other.podcast && + lastEpisodeDate == other.lastEpisodeDate && + isFollowed == other.isFollowed + } + else -> false } - else -> false - } override fun hashCode(): Int = Objects.hash(podcast, lastEpisodeDate, isFollowed) } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/di/DataDiModule.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/di/DataDiModule.kt index 68d4b1920f..6f1007ba20 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/di/DataDiModule.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/di/DataDiModule.kt @@ -41,35 +41,36 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import java.io.File -import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.logging.LoggingEventListener +import java.io.File +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object DataDiModule { - @Provides @Singleton fun provideOkHttpClient( - @ApplicationContext context: Context - ): OkHttpClient = OkHttpClient.Builder() - .cache(Cache(File(context.cacheDir, "http_cache"), (20 * 1024 * 1024).toLong())) - .apply { - if (BuildConfig.DEBUG) eventListenerFactory(LoggingEventListener.Factory()) - } - .build() + @ApplicationContext context: Context, + ): OkHttpClient = + OkHttpClient + .Builder() + .cache(Cache(File(context.cacheDir, "http_cache"), (20 * 1024 * 1024).toLong())) + .apply { + if (BuildConfig.DEBUG) eventListenerFactory(LoggingEventListener.Factory()) + }.build() @Provides @Singleton fun provideDatabase( - @ApplicationContext context: Context + @ApplicationContext context: Context, ): JetcasterDatabase = - Room.databaseBuilder(context, JetcasterDatabase::class.java, "data.db") + Room + .databaseBuilder(context, JetcasterDatabase::class.java, "data.db") // This is not recommended for normal apps, but the goal of this sample isn't to // showcase all of Room. .fallbackToDestructiveMigration() @@ -78,47 +79,37 @@ object DataDiModule { @Provides @Singleton fun provideImageLoader( - @ApplicationContext context: Context - ): ImageLoader = ImageLoader.Builder(context) - // Disable `Cache-Control` header support as some podcast images disable disk caching. - .respectCacheHeaders(false) - .build() + @ApplicationContext context: Context, + ): ImageLoader = + ImageLoader + .Builder(context) + // Disable `Cache-Control` header support as some podcast images disable disk caching. + .respectCacheHeaders(false) + .build() @Provides @Singleton - fun provideCategoriesDao( - database: JetcasterDatabase - ): CategoriesDao = database.categoriesDao() + fun provideCategoriesDao(database: JetcasterDatabase): CategoriesDao = database.categoriesDao() @Provides @Singleton - fun providePodcastCategoryEntryDao( - database: JetcasterDatabase - ): PodcastCategoryEntryDao = database.podcastCategoryEntryDao() + fun providePodcastCategoryEntryDao(database: JetcasterDatabase): PodcastCategoryEntryDao = database.podcastCategoryEntryDao() @Provides @Singleton - fun providePodcastsDao( - database: JetcasterDatabase - ): PodcastsDao = database.podcastsDao() + fun providePodcastsDao(database: JetcasterDatabase): PodcastsDao = database.podcastsDao() @Provides @Singleton - fun provideEpisodesDao( - database: JetcasterDatabase - ): EpisodesDao = database.episodesDao() + fun provideEpisodesDao(database: JetcasterDatabase): EpisodesDao = database.episodesDao() @Provides @Singleton - fun providePodcastFollowedEntryDao( - database: JetcasterDatabase - ): PodcastFollowedEntryDao = database.podcastFollowedEntryDao() + fun providePodcastFollowedEntryDao(database: JetcasterDatabase): PodcastFollowedEntryDao = database.podcastFollowedEntryDao() @Provides @Singleton - fun provideTransactionRunner( - database: JetcasterDatabase - ): TransactionRunner = database.transactionRunnerDao() + fun provideTransactionRunner(database: JetcasterDatabase): TransactionRunner = database.transactionRunnerDao() @Provides @Singleton @@ -136,9 +127,7 @@ object DataDiModule { @Provides @Singleton - fun provideEpisodeStore( - episodeDao: EpisodesDao - ): EpisodeStore = LocalEpisodeStore(episodeDao) + fun provideEpisodeStore(episodeDao: EpisodesDao): EpisodeStore = LocalEpisodeStore(episodeDao) @Provides @Singleton @@ -146,11 +135,12 @@ object DataDiModule { podcastDao: PodcastsDao, podcastFollowedEntryDao: PodcastFollowedEntryDao, transactionRunner: TransactionRunner, - ): PodcastStore = LocalPodcastStore( - podcastDao = podcastDao, - podcastFollowedEntryDao = podcastFollowedEntryDao, - transactionRunner = transactionRunner - ) + ): PodcastStore = + LocalPodcastStore( + podcastDao = podcastDao, + podcastFollowedEntryDao = podcastFollowedEntryDao, + transactionRunner = transactionRunner, + ) @Provides @Singleton @@ -159,10 +149,11 @@ object DataDiModule { podcastCategoryEntryDao: PodcastCategoryEntryDao, podcastDao: PodcastsDao, episodeDao: EpisodesDao, - ): CategoryStore = LocalCategoryStore( - episodesDao = episodeDao, - podcastsDao = podcastDao, - categoriesDao = categoriesDao, - categoryEntryDao = podcastCategoryEntryDao, - ) + ): CategoryStore = + LocalCategoryStore( + episodesDao = episodeDao, + podcastsDao = podcastDao, + categoriesDao = categoriesDao, + categoryEntryDao = podcastCategoryEntryDao, + ) } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/Feeds.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/Feeds.kt index 216cce6b9d..18e3a21335 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/Feeds.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/Feeds.kt @@ -24,24 +24,25 @@ private const val NowInAndroid = "https://feeds.libsyn.com/244409/rss" private const val AndroidDevelopersBackstage = "https://feeds.feedburner.com/blogspot/AndroidDevelopersBackstage" -val SampleFeeds = listOf( - NowInAndroid, - AndroidDevelopersBackstage, - "https://www.omnycontent.com/d/playlist/aaea4e69-af51-495e-afc9-a9760146922b/" + - "dc5b55ca-5f00-4063-b47f-ab870163d2b7/ca63aa52-ef7b-43ee-8ba5-ab8701645231/podcast.rss", - "https://audioboom.com/channels/2399216.rss", - "https://fragmentedpodcast.com/feed/", - "https://feeds.megaphone.fm/replyall", - "https://feeds.thisamericanlife.org/talpodcast", - "https://feeds.npr.org/510289/podcast.xml", - "https://feeds.99percentinvisible.org/99percentinvisible", - "https://www.howstuffworks.com/podcasts/stuff-you-should-know.rss", - "https://www.thenakedscientists.com/naked_scientists_podcast.xml", - "https://rss.art19.com/the-daily", - "https://rss.art19.com/lisk", - "https://omny.fm/shows/silence-is-not-an-option/playlists/podcast.rss", - "https://audioboom.com/channels/5025217.rss", - "https://feeds.simplecast.com/7PvD7RPL", - "https://feeds.buzzsprout.com/1006078.rss", - "https://feeds.megaphone.fm/HSW9992617712" -) +val SampleFeeds = + listOf( + NowInAndroid, + AndroidDevelopersBackstage, + "https://www.omnycontent.com/d/playlist/aaea4e69-af51-495e-afc9-a9760146922b/" + + "dc5b55ca-5f00-4063-b47f-ab870163d2b7/ca63aa52-ef7b-43ee-8ba5-ab8701645231/podcast.rss", + "https://audioboom.com/channels/2399216.rss", + "https://fragmentedpodcast.com/feed/", + "https://feeds.megaphone.fm/replyall", + "https://feeds.thisamericanlife.org/talpodcast", + "https://feeds.npr.org/510289/podcast.xml", + "https://feeds.99percentinvisible.org/99percentinvisible", + "https://www.howstuffworks.com/podcasts/stuff-you-should-know.rss", + "https://www.thenakedscientists.com/naked_scientists_podcast.xml", + "https://rss.art19.com/the-daily", + "https://rss.art19.com/lisk", + "https://omny.fm/shows/silence-is-not-an-option/playlists/podcast.rss", + "https://audioboom.com/channels/5025217.rss", + "https://feeds.simplecast.com/7PvD7RPL", + "https://feeds.buzzsprout.com/1006078.rss", + "https://feeds.megaphone.fm/HSW9992617712", + ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/OkHttpExtensions.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/OkHttpExtensions.kt index 147fed436e..38828251cb 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/OkHttpExtensions.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/OkHttpExtensions.kt @@ -16,41 +16,48 @@ package com.example.jetcaster.core.data.network -import java.io.IOException -import kotlin.coroutines.resumeWithException import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.Call import okhttp3.Callback import okhttp3.Response import okhttp3.internal.closeQuietly +import java.io.IOException +import kotlin.coroutines.resumeWithException /** * Suspending wrapper around an OkHttp [Call], using [Call.enqueue]. */ -suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation -> - enqueue( - object : Callback { - override fun onResponse(call: Call, response: Response) { - continuation.resume(response) { - // If we have a response but we're cancelled while resuming, we need to - // close() the unused response - if (response.body != null) { - response.closeQuietly() +suspend fun Call.await(): Response = + suspendCancellableCoroutine { continuation -> + enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + continuation.resume(response) { + // If we have a response but we're cancelled while resuming, we need to + // close() the unused response + if (response.body != null) { + response.closeQuietly() + } } } - } - override fun onFailure(call: Call, e: IOException) { - continuation.resumeWithException(e) - } - } - ) + override fun onFailure( + call: Call, + e: IOException, + ) { + continuation.resumeWithException(e) + } + }, + ) - continuation.invokeOnCancellation { - try { - cancel() - } catch (t: Throwable) { - // Ignore cancel exception + continuation.invokeOnCancellation { + try { + cancel() + } catch (t: Throwable) { + // Ignore cancel exception + } } } -} diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/PodcastFetcher.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/PodcastFetcher.kt index 34e7030c93..1bd9175883 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/PodcastFetcher.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/PodcastFetcher.kt @@ -27,11 +27,6 @@ import com.rometools.modules.itunes.FeedInformation import com.rometools.rome.feed.synd.SyndEntry import com.rometools.rome.feed.synd.SyndFeed import com.rometools.rome.io.SyndFeedInput -import java.time.Duration -import java.time.Instant -import java.time.ZoneOffset -import java.util.concurrent.TimeUnit -import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow @@ -42,6 +37,11 @@ import kotlinx.coroutines.withContext import okhttp3.CacheControl import okhttp3.OkHttpClient import okhttp3.Request +import java.time.Duration +import java.time.Instant +import java.time.ZoneOffset +import java.util.concurrent.TimeUnit +import javax.inject.Inject /** * A class which fetches some selected podcast RSS feeds. @@ -50,62 +50,66 @@ import okhttp3.Request * @param syndFeedInput [SyndFeedInput] to use for parsing RSS feeds. * @param ioDispatcher [CoroutineDispatcher] to use for running fetch requests. */ -class PodcastsFetcher @Inject constructor( - private val okHttpClient: OkHttpClient, - private val syndFeedInput: SyndFeedInput, - @Dispatcher(JetcasterDispatchers.IO) private val ioDispatcher: CoroutineDispatcher -) { - - /** - * It seems that most podcast hosts do not implement HTTP caching appropriately. - * Instead of fetching data on every app open, we instead allow the use of 'stale' - * network responses (up to 8 hours). - */ - private val cacheControl by lazy { - CacheControl.Builder().maxStale(8, TimeUnit.HOURS).build() - } +class PodcastsFetcher + @Inject + constructor( + private val okHttpClient: OkHttpClient, + private val syndFeedInput: SyndFeedInput, + @Dispatcher(JetcasterDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, + ) { + /** + * It seems that most podcast hosts do not implement HTTP caching appropriately. + * Instead of fetching data on every app open, we instead allow the use of 'stale' + * network responses (up to 8 hours). + */ + private val cacheControl by lazy { + CacheControl.Builder().maxStale(8, TimeUnit.HOURS).build() + } - /** - * Returns a [Flow] which fetches each podcast feed and emits it in turn. - * - * The feeds are fetched concurrently, meaning that the resulting emission order may not - * match the order of [feedUrls]. - */ - operator fun invoke(feedUrls: List): Flow { - // We use flatMapMerge here to achieve concurrent fetching/parsing of the feeds. - return feedUrls.asFlow() - .flatMapMerge { feedUrl -> - flow { - emit(fetchPodcast(feedUrl)) - }.catch { e -> - // If an exception was caught while fetching the podcast, wrap it in - // an Error instance. - emit(PodcastRssResponse.Error(e)) + /** + * Returns a [Flow] which fetches each podcast feed and emits it in turn. + * + * The feeds are fetched concurrently, meaning that the resulting emission order may not + * match the order of [feedUrls]. + */ + operator fun invoke(feedUrls: List): Flow { + // We use flatMapMerge here to achieve concurrent fetching/parsing of the feeds. + return feedUrls + .asFlow() + .flatMapMerge { feedUrl -> + flow { + emit(fetchPodcast(feedUrl)) + }.catch { e -> + // If an exception was caught while fetching the podcast, wrap it in + // an Error instance. + emit(PodcastRssResponse.Error(e)) + } } - } - } - - private suspend fun fetchPodcast(url: String): PodcastRssResponse { - val request = Request.Builder() - .url(url) - .cacheControl(cacheControl) - .build() - - val response = okHttpClient.newCall(request).await() - - // If the network request wasn't successful, throw an exception - if (!response.isSuccessful) throw HttpException(response) + } - // Otherwise we can parse the response using a Rome SyndFeedInput, then map it - // to a Podcast instance. We run this on the IO dispatcher since the parser is reading - // from a stream. - return withContext(ioDispatcher) { - response.body!!.use { body -> - syndFeedInput.build(body.charStream()).toPodcastResponse(url) + private suspend fun fetchPodcast(url: String): PodcastRssResponse { + val request = + Request + .Builder() + .url(url) + .cacheControl(cacheControl) + .build() + + val response = okHttpClient.newCall(request).await() + + // If the network request wasn't successful, throw an exception + if (!response.isSuccessful) throw HttpException(response) + + // Otherwise we can parse the response using a Rome SyndFeedInput, then map it + // to a Podcast instance. We run this on the IO dispatcher since the parser is reading + // from a stream. + return withContext(ioDispatcher) { + response.body!!.use { body -> + syndFeedInput.build(body.charStream()).toPodcastResponse(url) + } } } } -} sealed class PodcastRssResponse { data class Error( @@ -115,7 +119,7 @@ sealed class PodcastRssResponse { data class Success( val podcast: Podcast, val episodes: List, - val categories: Set + val categories: Set, ) : PodcastRssResponse() } @@ -127,18 +131,21 @@ private fun SyndFeed.toPodcastResponse(feedUrl: String): PodcastRssResponse { val episodes = entries.map { it.toEpisode(podcastUri) } val feedInfo = getModule(PodcastModuleDtd) as? FeedInformation - val podcast = Podcast( - uri = podcastUri, - title = title, - description = feedInfo?.summary ?: description, - author = author, - copyright = copyright, - imageUrl = feedInfo?.imageUri?.toString() - ) - - val categories = feedInfo?.categories - ?.map { Category(name = it.name) } - ?.toSet() ?: emptySet() + val podcast = + Podcast( + uri = podcastUri, + title = title, + description = feedInfo?.summary ?: description, + author = author, + copyright = copyright, + imageUrl = feedInfo?.imageUri?.toString(), + ) + + val categories = + feedInfo + ?.categories + ?.map { Category(name = it.name) } + ?.toSet() ?: emptySet() return PodcastRssResponse.Success(podcast, episodes, categories) } @@ -156,7 +163,7 @@ private fun SyndEntry.toEpisode(podcastUri: String): Episode { summary = entryInformation?.summary ?: description?.value, subtitle = entryInformation?.subtitle, published = Instant.ofEpochMilli(publishedDate.time).atOffset(ZoneOffset.UTC), - duration = entryInformation?.duration?.milliseconds?.let { Duration.ofMillis(it) } + duration = entryInformation?.duration?.milliseconds?.let { Duration.ofMillis(it) }, ) } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/CategoryStore.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/CategoryStore.kt index 0c29188054..637c6cd4a4 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/CategoryStore.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/CategoryStore.kt @@ -25,14 +25,13 @@ import com.example.jetcaster.core.data.database.model.EpisodeToPodcast import com.example.jetcaster.core.data.database.model.PodcastCategoryEntry import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo import kotlinx.coroutines.flow.Flow + interface CategoryStore { /** * Returns a flow containing a list of categories which is sorted by the number * of podcasts in each category. */ - fun categoriesSortedByPodcastCount( - limit: Int = Integer.MAX_VALUE - ): Flow> + fun categoriesSortedByPodcastCount(limit: Int = Integer.MAX_VALUE): Flow> /** * Returns a flow containing a list of podcasts in the category with the given [categoryId], @@ -40,7 +39,7 @@ interface CategoryStore { */ fun podcastsInCategorySortedByPodcastCount( categoryId: Long, - limit: Int = Int.MAX_VALUE + limit: Int = Int.MAX_VALUE, ): Flow> /** @@ -49,7 +48,7 @@ interface CategoryStore { */ fun episodesFromPodcastsInCategory( categoryId: Long, - limit: Int = Integer.MAX_VALUE + limit: Int = Integer.MAX_VALUE, ): Flow> /** @@ -59,7 +58,10 @@ interface CategoryStore { */ suspend fun addCategory(category: Category): Long - suspend fun addPodcastToCategory(podcastUri: String, categoryId: Long) + suspend fun addPodcastToCategory( + podcastUri: String, + categoryId: Long, + ) /** * @return gets the category with [name], if it exists, otherwise, null @@ -74,15 +76,13 @@ class LocalCategoryStore constructor( private val categoriesDao: CategoriesDao, private val categoryEntryDao: PodcastCategoryEntryDao, private val episodesDao: EpisodesDao, - private val podcastsDao: PodcastsDao + private val podcastsDao: PodcastsDao, ) : CategoryStore { /** * Returns a flow containing a list of categories which is sorted by the number * of podcasts in each category. */ - override fun categoriesSortedByPodcastCount(limit: Int): Flow> { - return categoriesDao.categoriesSortedByPodcastCount(limit) - } + override fun categoriesSortedByPodcastCount(limit: Int): Flow> = categoriesDao.categoriesSortedByPodcastCount(limit) /** * Returns a flow containing a list of podcasts in the category with the given [categoryId], @@ -90,10 +90,8 @@ class LocalCategoryStore constructor( */ override fun podcastsInCategorySortedByPodcastCount( categoryId: Long, - limit: Int - ): Flow> { - return podcastsDao.podcastsInCategorySortedByLastEpisode(categoryId, limit) - } + limit: Int, + ): Flow> = podcastsDao.podcastsInCategorySortedByLastEpisode(categoryId, limit) /** * Returns a flow containing a list of episodes from podcasts in the category with the @@ -101,29 +99,28 @@ class LocalCategoryStore constructor( */ override fun episodesFromPodcastsInCategory( categoryId: Long, - limit: Int - ): Flow> { - return episodesDao.episodesFromPodcastsInCategory(categoryId, limit) - } + limit: Int, + ): Flow> = episodesDao.episodesFromPodcastsInCategory(categoryId, limit) /** * Adds the category to the database if it doesn't already exist. * * @return the id of the newly inserted/existing category */ - override suspend fun addCategory(category: Category): Long { - return when (val local = categoriesDao.getCategoryWithName(category.name)) { + override suspend fun addCategory(category: Category): Long = + when (val local = categoriesDao.getCategoryWithName(category.name)) { null -> categoriesDao.insert(category) else -> local.id } - } - override suspend fun addPodcastToCategory(podcastUri: String, categoryId: Long) { + override suspend fun addPodcastToCategory( + podcastUri: String, + categoryId: Long, + ) { categoryEntryDao.insert( - PodcastCategoryEntry(podcastUri = podcastUri, categoryId = categoryId) + PodcastCategoryEntry(podcastUri = podcastUri, categoryId = categoryId), ) } - override fun getCategory(name: String): Flow = - categoriesDao.observeCategory(name) + override fun getCategory(name: String): Flow = categoriesDao.observeCategory(name) } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/EpisodeStore.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/EpisodeStore.kt index 26af92e97c..fb50e45eee 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/EpisodeStore.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/EpisodeStore.kt @@ -38,7 +38,7 @@ interface EpisodeStore { */ fun episodesInPodcast( podcastUri: String, - limit: Int = Integer.MAX_VALUE + limit: Int = Integer.MAX_VALUE, ): Flow> /** @@ -47,7 +47,7 @@ interface EpisodeStore { */ fun episodesInPodcasts( podcastUris: List, - limit: Int = Integer.MAX_VALUE + limit: Int = Integer.MAX_VALUE, ): Flow> /** @@ -64,17 +64,14 @@ interface EpisodeStore { * A data repository for [Episode] instances. */ class LocalEpisodeStore( - private val episodesDao: EpisodesDao + private val episodesDao: EpisodesDao, ) : EpisodeStore { /** * Returns a flow containing the episode given [episodeUri]. */ - override fun episodeWithUri(episodeUri: String): Flow { - return episodesDao.episode(episodeUri) - } + override fun episodeWithUri(episodeUri: String): Flow = episodesDao.episode(episodeUri) - override fun episodeAndPodcastWithUri(episodeUri: String): Flow = - episodesDao.episodeAndPodcast(episodeUri) + override fun episodeAndPodcastWithUri(episodeUri: String): Flow = episodesDao.episodeAndPodcast(episodeUri) /** * Returns a flow containing the list of episodes associated with the podcast with the @@ -82,27 +79,24 @@ class LocalEpisodeStore( */ override fun episodesInPodcast( podcastUri: String, - limit: Int - ): Flow> { - return episodesDao.episodesForPodcastUri(podcastUri, limit) - } + limit: Int, + ): Flow> = episodesDao.episodesForPodcastUri(podcastUri, limit) + /** * Returns a list of episodes for the given podcast URIs ordering by most recently published * to least recently published. */ override fun episodesInPodcasts( podcastUris: List, - limit: Int - ): Flow> = - episodesDao.episodesForPodcasts(podcastUris, limit) + limit: Int, + ): Flow> = episodesDao.episodesForPodcasts(podcastUris, limit) /** * Add a new [Episode] to this store. * * This automatically switches to the main thread to maintain thread consistency. */ - override suspend fun addEpisodes(episodes: Collection) = - episodesDao.insertAll(episodes) + override suspend fun addEpisodes(episodes: Collection) = episodesDao.insertAll(episodes) override suspend fun isEmpty(): Boolean = episodesDao.count() == 0 } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastStore.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastStore.kt index ee809c9e30..fa8399289c 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastStore.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastStore.kt @@ -40,17 +40,13 @@ interface PodcastStore { * Returns a flow containing the entire collection of podcasts, sorted by the last episode * publish date for each podcast. */ - fun podcastsSortedByLastEpisode( - limit: Int = Int.MAX_VALUE - ): Flow> + fun podcastsSortedByLastEpisode(limit: Int = Int.MAX_VALUE): Flow> /** * Returns a flow containing a list of all followed podcasts, sorted by the their last * episode date. */ - fun followedPodcastsSortedByLastEpisode( - limit: Int = Int.MAX_VALUE - ): Flow> + fun followedPodcastsSortedByLastEpisode(limit: Int = Int.MAX_VALUE): Flow> /** * Returns a flow containing a list of podcasts such that its name partially matches @@ -58,7 +54,7 @@ interface PodcastStore { */ fun searchPodcastByTitle( keyword: String, - limit: Int = Int.MAX_VALUE + limit: Int = Int.MAX_VALUE, ): Flow> /** @@ -69,7 +65,7 @@ interface PodcastStore { fun searchPodcastByTitleAndCategories( keyword: String, categories: List, - limit: Int = Int.MAX_VALUE + limit: Int = Int.MAX_VALUE, ): Flow> suspend fun togglePodcastFollowed(podcastUri: String) @@ -94,52 +90,40 @@ interface PodcastStore { class LocalPodcastStore constructor( private val podcastDao: PodcastsDao, private val podcastFollowedEntryDao: PodcastFollowedEntryDao, - private val transactionRunner: TransactionRunner + private val transactionRunner: TransactionRunner, ) : PodcastStore { /** * Return a flow containing the [Podcast] with the given [uri]. */ - override fun podcastWithUri(uri: String): Flow { - return podcastDao.podcastWithUri(uri) - } + override fun podcastWithUri(uri: String): Flow = podcastDao.podcastWithUri(uri) /** * Return a flow containing the [PodcastWithExtraInfo] with the given [podcastUri]. */ - override fun podcastWithExtraInfo(podcastUri: String): Flow = - podcastDao.podcastWithExtraInfo(podcastUri) + override fun podcastWithExtraInfo(podcastUri: String): Flow = podcastDao.podcastWithExtraInfo(podcastUri) /** * Returns a flow containing the entire collection of podcasts, sorted by the last episode * publish date for each podcast. */ - override fun podcastsSortedByLastEpisode( - limit: Int - ): Flow> { - return podcastDao.podcastsSortedByLastEpisode(limit) - } + override fun podcastsSortedByLastEpisode(limit: Int): Flow> = podcastDao.podcastsSortedByLastEpisode(limit) /** * Returns a flow containing a list of all followed podcasts, sorted by the their last * episode date. */ - override fun followedPodcastsSortedByLastEpisode( - limit: Int - ): Flow> { - return podcastDao.followedPodcastsSortedByLastEpisode(limit) - } + override fun followedPodcastsSortedByLastEpisode(limit: Int): Flow> = + podcastDao.followedPodcastsSortedByLastEpisode(limit) override fun searchPodcastByTitle( keyword: String, - limit: Int - ): Flow> { - return podcastDao.searchPodcastByTitle(keyword, limit) - } + limit: Int, + ): Flow> = podcastDao.searchPodcastByTitle(keyword, limit) override fun searchPodcastByTitleAndCategories( keyword: String, categories: List, - limit: Int + limit: Int, ): Flow> { val categoryIdList = categories.map { it.id } return podcastDao.searchPodcastByTitleAndCategory(keyword, categoryIdList, limit) @@ -149,13 +133,14 @@ class LocalPodcastStore constructor( podcastFollowedEntryDao.insert(PodcastFollowedEntry(podcastUri = podcastUri)) } - override suspend fun togglePodcastFollowed(podcastUri: String) = transactionRunner { - if (podcastFollowedEntryDao.isPodcastFollowed(podcastUri)) { - unfollowPodcast(podcastUri) - } else { - followPodcast(podcastUri) + override suspend fun togglePodcastFollowed(podcastUri: String) = + transactionRunner { + if (podcastFollowedEntryDao.isPodcastFollowed(podcastUri)) { + unfollowPodcast(podcastUri) + } else { + followPodcast(podcastUri) + } } - } override suspend fun unfollowPodcast(podcastUri: String) { podcastFollowedEntryDao.deleteWithPodcastUri(podcastUri) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt index cb9b308405..976a56f461 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt @@ -22,56 +22,58 @@ import com.example.jetcaster.core.data.database.dao.TransactionRunner import com.example.jetcaster.core.data.network.PodcastRssResponse import com.example.jetcaster.core.data.network.PodcastsFetcher import com.example.jetcaster.core.data.network.SampleFeeds -import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import javax.inject.Inject /** * Data repository for Podcasts. */ -class PodcastsRepository @Inject constructor( - private val podcastsFetcher: PodcastsFetcher, - private val podcastStore: PodcastStore, - private val episodeStore: EpisodeStore, - private val categoryStore: CategoryStore, - private val transactionRunner: TransactionRunner, - @Dispatcher(JetcasterDispatchers.Main) mainDispatcher: CoroutineDispatcher -) { - private var refreshingJob: Job? = null - - private val scope = CoroutineScope(mainDispatcher) +class PodcastsRepository + @Inject + constructor( + private val podcastsFetcher: PodcastsFetcher, + private val podcastStore: PodcastStore, + private val episodeStore: EpisodeStore, + private val categoryStore: CategoryStore, + private val transactionRunner: TransactionRunner, + @Dispatcher(JetcasterDispatchers.Main) mainDispatcher: CoroutineDispatcher, + ) { + private var refreshingJob: Job? = null - suspend fun updatePodcasts(force: Boolean) { - if (refreshingJob?.isActive == true) { - refreshingJob?.join() - } else if (force || podcastStore.isEmpty()) { + private val scope = CoroutineScope(mainDispatcher) - refreshingJob = scope.launch { - // Now fetch the podcasts, and add each to each store - podcastsFetcher(SampleFeeds) - .filter { it is PodcastRssResponse.Success } - .map { it as PodcastRssResponse.Success } - .collect { (podcast, episodes, categories) -> - transactionRunner { - podcastStore.addPodcast(podcast) - episodeStore.addEpisodes(episodes) + suspend fun updatePodcasts(force: Boolean) { + if (refreshingJob?.isActive == true) { + refreshingJob?.join() + } else if (force || podcastStore.isEmpty()) { + refreshingJob = + scope.launch { + // Now fetch the podcasts, and add each to each store + podcastsFetcher(SampleFeeds) + .filter { it is PodcastRssResponse.Success } + .map { it as PodcastRssResponse.Success } + .collect { (podcast, episodes, categories) -> + transactionRunner { + podcastStore.addPodcast(podcast) + episodeStore.addEpisodes(episodes) - categories.forEach { category -> - // First insert the category - val categoryId = categoryStore.addCategory(category) - // Now we can add the podcast to the category - categoryStore.addPodcastToCategory( - podcastUri = podcast.uri, - categoryId = categoryId - ) + categories.forEach { category -> + // First insert the category + val categoryId = categoryStore.addCategory(category) + // Now we can add the podcast to the category + categoryStore.addPodcastToCategory( + podcastUri = podcast.uri, + categoryId = categoryId, + ) + } + } } - } } } } } -} diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/util/Flows.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/util/Flows.kt index a9940f315d..c6b5a1eeb2 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/util/Flows.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/util/Flows.kt @@ -17,6 +17,7 @@ package com.example.jetcaster.core.util import kotlinx.coroutines.flow.Flow + /** * Combines 3 flows into a single flow by combining their latest values using the provided transform function. * @@ -32,7 +33,7 @@ fun combine( flow3: Flow, flow4: Flow, flow5: Flow, - transform: suspend (T1, T2, T3, T4, T5) -> R + transform: suspend (T1, T2, T3, T4, T5) -> R, ): Flow = kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5) { args: Array<*> -> transform( @@ -43,11 +44,11 @@ fun combine( args[4] as T5, ) } + fun combine( flow: Flow, flow2: Flow, - - transform: suspend (T1, T2) -> R + transform: suspend (T1, T2) -> R, ): Flow = kotlinx.coroutines.flow.combine(flow, flow2) { args: Array<*> -> transform( @@ -75,7 +76,7 @@ fun combine( flow4: Flow, flow5: Flow, flow6: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6) -> R + transform: suspend (T1, T2, T3, T4, T5, T6) -> R, ): Flow = kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> -> transform( @@ -109,7 +110,7 @@ fun combine( flow5: Flow, flow6: Flow, flow7: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R + transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R, ): Flow = kotlinx.coroutines.flow.combine( flow, @@ -118,7 +119,7 @@ fun combine( flow4, flow5, flow6, - flow7 + flow7, ) { args: Array<*> -> transform( args[0] as T1, diff --git a/Jetcaster/core/designsystem/build.gradle.kts b/Jetcaster/core/designsystem/build.gradle.kts index 30a8b624b2..48301bee3f 100644 --- a/Jetcaster/core/designsystem/build.gradle.kts +++ b/Jetcaster/core/designsystem/build.gradle.kts @@ -1,54 +1,77 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.compose) + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose) } // TODO(chris): Set up convention plugin android { - namespace = "com.example.jetcaster.core.designsystem" - compileSdk = libs.versions.compileSdk.get().toInt() - - defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() - vectorDrawables.useSupportLibrary = true - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + namespace = "com.example.jetcaster.core.designsystem" + compileSdk = + libs.versions.compileSdk + .get() + .toInt() + + defaultConfig { + minSdk = + libs.versions.minSdk + .get() + .toInt() + vectorDrawables.useSupportLibrary = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } } - } - buildFeatures { - compose = true - buildConfig = true - } + buildFeatures { + compose = true + buildConfig = true + } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } } kotlin { - jvmToolchain(17) + jvmToolchain(17) } composeCompiler { - enableStrongSkippingMode = true + enableStrongSkippingMode = true } dependencies { - val composeBom = platform(libs.androidx.compose.bom) - implementation(composeBom) - implementation(libs.androidx.compose.foundation) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.text) - implementation(libs.coil.kt.compose) - - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) + val composeBom = platform(libs.androidx.compose.bom) + implementation(composeBom) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.text) + implementation(libs.coil.kt.compose) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) } diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/HtmlTextContainer.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/HtmlTextContainer.kt index 502620e6bf..f919542b59 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/HtmlTextContainer.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/HtmlTextContainer.kt @@ -34,14 +34,15 @@ import androidx.core.text.HtmlCompat @Composable fun HtmlTextContainer( text: String, - content: @Composable (AnnotatedString) -> Unit + content: @Composable (AnnotatedString) -> Unit, ) { - val annotatedString = remember(key1 = text) { - buildAnnotatedString { - val htmlCompat = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_COMPACT) - append(htmlCompat) + val annotatedString = + remember(key1 = text) { + buildAnnotatedString { + val htmlCompat = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_COMPACT) + append(htmlCompat) + } } - } SelectionContainer { content(annotatedString) } diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/ImageBackground.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/ImageBackground.kt index 4cb124dc65..7db8d7e504 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/ImageBackground.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/ImageBackground.kt @@ -39,7 +39,7 @@ fun ImageBackgroundColorScrim( modifier = modifier, overlay = { drawRect(color) - } + }, ) } @@ -53,13 +53,14 @@ fun ImageBackgroundRadialGradientScrim( url = url, modifier = modifier, overlay = { - val brush = Brush.radialGradient( - colors = colors, - center = Offset(0f, size.height), - radius = size.width * 1.5f - ) + val brush = + Brush.radialGradient( + colors = colors, + center = Offset(0f, size.height), + radius = size.width * 1.5f, + ) drawRect(brush, blendMode = BlendMode.Multiply) - } + }, ) } @@ -76,13 +77,14 @@ fun ImageBackground( model = url, contentDescription = null, contentScale = ContentScale.Crop, - modifier = modifier - .fillMaxWidth() - .drawWithCache { - onDrawWithContent { - drawContent() - overlay() - } - } + modifier = + modifier + .fillMaxWidth() + .drawWithCache { + onDrawWithContent { + drawContent() + overlay() + } + }, ) } diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt index f7b8966196..cf74040267 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt @@ -55,35 +55,40 @@ fun PodcastImage( mutableStateOf(AsyncImagePainter.State.Empty) } - val imageLoader = rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalContext.current) - .data(podcastImageUrl) - .crossfade(true) - .build(), - contentScale = contentScale, - onState = { state -> imagePainterState = state } - ) + val imageLoader = + rememberAsyncImagePainter( + model = + ImageRequest + .Builder(LocalContext.current) + .data(podcastImageUrl) + .crossfade(true) + .build(), + contentScale = contentScale, + onState = { state -> imagePainterState = state }, + ) Box( modifier = modifier, - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { when (imagePainterState) { is AsyncImagePainter.State.Loading, - is AsyncImagePainter.State.Error -> { + is AsyncImagePainter.State.Error, + -> { Image( painter = painterResource(id = R.drawable.img_empty), contentDescription = null, - modifier = Modifier - .fillMaxSize() + modifier = + Modifier + .fillMaxSize(), ) } else -> { Box( - modifier = Modifier - .background(placeholderBrush) - .fillMaxSize() - + modifier = + Modifier + .background(placeholderBrush) + .fillMaxSize(), ) } } diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt index 865dac3130..6daf76744e 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt @@ -25,19 +25,12 @@ import com.example.jetcaster.designsystem.theme.surfaceVariantDark import com.example.jetcaster.designsystem.theme.surfaceVariantLight @Composable -internal fun thumbnailPlaceholderDefaultBrush( - color: Color = thumbnailPlaceHolderDefaultColor() -): Brush { - return SolidColor(color) -} +internal fun thumbnailPlaceholderDefaultBrush(color: Color = thumbnailPlaceHolderDefaultColor()): Brush = SolidColor(color) @Composable -private fun thumbnailPlaceHolderDefaultColor( - isInDarkMode: Boolean = isSystemInDarkTheme() -): Color { - return if (isInDarkMode) { +private fun thumbnailPlaceHolderDefaultColor(isInDarkMode: Boolean = isSystemInDarkTheme()): Color = + if (isInDarkMode) { surfaceVariantDark } else { surfaceVariantLight } -} diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt index 41bbfefbb6..b94006af97 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt @@ -20,8 +20,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp -val JetcasterShapes = Shapes( - small = RoundedCornerShape(percent = 50), - medium = RoundedCornerShape(size = 8.dp), - large = RoundedCornerShape(size = 16.dp) -) +val JetcasterShapes = + Shapes( + small = RoundedCornerShape(percent = 50), + medium = RoundedCornerShape(size = 8.dp), + large = RoundedCornerShape(size = 16.dp), + ) diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt index b9d6eb171e..336d3a05b6 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt @@ -20,104 +20,120 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp -val JetcasterTypography = androidx.compose.material3.Typography( - displayLarge = TextStyle( - fontFamily = Montserrat, - fontSize = 57.sp, - fontWeight = FontWeight.W400, - lineHeight = 64.sp, - letterSpacing = (-0.25).sp - ), - displayMedium = TextStyle( - fontFamily = Montserrat, - fontSize = 45.sp, - fontWeight = FontWeight.W400, - lineHeight = 52.sp - ), - displaySmall = TextStyle( - fontFamily = Montserrat, - fontSize = 36.sp, - fontWeight = FontWeight.W400, - lineHeight = 44.sp - ), - headlineLarge = TextStyle( - fontFamily = Montserrat, - fontSize = 32.sp, - fontWeight = FontWeight.W500, - lineHeight = 40.sp - ), - headlineMedium = TextStyle( - fontFamily = Montserrat, - fontSize = 28.sp, - fontWeight = FontWeight.W500, - lineHeight = 36.sp - ), - headlineSmall = TextStyle( - fontFamily = Montserrat, - fontSize = 24.sp, - fontWeight = FontWeight.W500, - lineHeight = 32.sp - ), - titleLarge = TextStyle( - fontFamily = Montserrat, - fontSize = 22.sp, - fontWeight = FontWeight.W400, - lineHeight = 28.sp - ), - titleMedium = TextStyle( - fontFamily = Montserrat, - fontSize = 16.sp, - fontWeight = FontWeight.W500, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - titleSmall = TextStyle( - fontFamily = Montserrat, - fontSize = 14.sp, - fontWeight = FontWeight.W500, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - labelLarge = TextStyle( - fontFamily = Montserrat, - fontSize = 14.sp, - fontWeight = FontWeight.W500, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - labelMedium = TextStyle( - fontFamily = Montserrat, - fontSize = 12.sp, - fontWeight = FontWeight.W500, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ), - labelSmall = TextStyle( - fontFamily = Montserrat, - fontSize = 11.sp, - fontWeight = FontWeight.W500, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ), - bodyLarge = TextStyle( - fontFamily = Montserrat, - fontSize = 16.sp, - fontWeight = FontWeight.W500, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ), - bodyMedium = TextStyle( - fontFamily = Montserrat, - fontSize = 14.sp, - fontWeight = FontWeight.W500, - lineHeight = 20.sp, - letterSpacing = 0.25.sp - ), - bodySmall = TextStyle( - fontFamily = Montserrat, - fontSize = 12.sp, - fontWeight = FontWeight.W500, - lineHeight = 16.sp, - letterSpacing = 0.4.sp - ), -) +val JetcasterTypography = + androidx.compose.material3.Typography( + displayLarge = + TextStyle( + fontFamily = Montserrat, + fontSize = 57.sp, + fontWeight = FontWeight.W400, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp, + ), + displayMedium = + TextStyle( + fontFamily = Montserrat, + fontSize = 45.sp, + fontWeight = FontWeight.W400, + lineHeight = 52.sp, + ), + displaySmall = + TextStyle( + fontFamily = Montserrat, + fontSize = 36.sp, + fontWeight = FontWeight.W400, + lineHeight = 44.sp, + ), + headlineLarge = + TextStyle( + fontFamily = Montserrat, + fontSize = 32.sp, + fontWeight = FontWeight.W500, + lineHeight = 40.sp, + ), + headlineMedium = + TextStyle( + fontFamily = Montserrat, + fontSize = 28.sp, + fontWeight = FontWeight.W500, + lineHeight = 36.sp, + ), + headlineSmall = + TextStyle( + fontFamily = Montserrat, + fontSize = 24.sp, + fontWeight = FontWeight.W500, + lineHeight = 32.sp, + ), + titleLarge = + TextStyle( + fontFamily = Montserrat, + fontSize = 22.sp, + fontWeight = FontWeight.W400, + lineHeight = 28.sp, + ), + titleMedium = + TextStyle( + fontFamily = Montserrat, + fontSize = 16.sp, + fontWeight = FontWeight.W500, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + ), + titleSmall = + TextStyle( + fontFamily = Montserrat, + fontSize = 14.sp, + fontWeight = FontWeight.W500, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + labelLarge = + TextStyle( + fontFamily = Montserrat, + fontSize = 14.sp, + fontWeight = FontWeight.W500, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + labelMedium = + TextStyle( + fontFamily = Montserrat, + fontSize = 12.sp, + fontWeight = FontWeight.W500, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), + labelSmall = + TextStyle( + fontFamily = Montserrat, + fontSize = 11.sp, + fontWeight = FontWeight.W500, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), + bodyLarge = + TextStyle( + fontFamily = Montserrat, + fontSize = 16.sp, + fontWeight = FontWeight.W500, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), + bodyMedium = + TextStyle( + fontFamily = Montserrat, + fontSize = 14.sp, + fontWeight = FontWeight.W500, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + bodySmall = + TextStyle( + fontFamily = Montserrat, + fontSize = 12.sp, + fontWeight = FontWeight.W500, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + ), + ) diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Typography.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Typography.kt index bac67e41f7..572b17fca1 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Typography.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Typography.kt @@ -21,9 +21,10 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import com.example.jetcaster.core.designsystem.R -val Montserrat = FontFamily( - Font(R.font.montserrat_light, FontWeight.Light), - Font(R.font.montserrat_regular, FontWeight.Normal), - Font(R.font.montserrat_medium, FontWeight.Medium), - Font(R.font.montserrat_semibold, FontWeight.SemiBold) -) +val Montserrat = + FontFamily( + Font(R.font.montserrat_light, FontWeight.Light), + Font(R.font.montserrat_regular, FontWeight.Normal), + Font(R.font.montserrat_medium, FontWeight.Medium), + Font(R.font.montserrat_semibold, FontWeight.SemiBold), + ) diff --git a/Jetcaster/core/domain-testing/build.gradle.kts b/Jetcaster/core/domain-testing/build.gradle.kts index 28c64e5809..a304eefdaa 100644 --- a/Jetcaster/core/domain-testing/build.gradle.kts +++ b/Jetcaster/core/domain-testing/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -5,10 +22,16 @@ plugins { android { namespace = "com.example.jetcaster.core.domain.testing" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -19,7 +42,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } diff --git a/Jetcaster/core/domain-testing/src/main/java/com/example/jetcaster/core/domain/testing/PreviewData.kt b/Jetcaster/core/domain-testing/src/main/java/com/example/jetcaster/core/domain/testing/PreviewData.kt index de1dfde9ab..85f4ece0f8 100644 --- a/Jetcaster/core/domain-testing/src/main/java/com/example/jetcaster/core/domain/testing/PreviewData.kt +++ b/Jetcaster/core/domain-testing/src/main/java/com/example/jetcaster/core/domain/testing/PreviewData.kt @@ -24,51 +24,64 @@ import com.example.jetcaster.core.player.model.PlayerEpisode import java.time.OffsetDateTime import java.time.ZoneOffset -val PreviewCategories = listOf( - CategoryInfo(id = 1, name = "Crime"), - CategoryInfo(id = 2, name = "News"), - CategoryInfo(id = 3, name = "Comedy") -) +val PreviewCategories = + listOf( + CategoryInfo(id = 1, name = "Crime"), + CategoryInfo(id = 2, name = "News"), + CategoryInfo(id = 3, name = "Comedy"), + ) -val PreviewPodcasts = listOf( - PodcastInfo( - uri = "fakeUri://podcast/1", - title = "Android Developers Backstage", - author = "Android Developers", - isSubscribed = true, - lastEpisodeDate = OffsetDateTime.now() - ), - PodcastInfo( - uri = "fakeUri://podcast/2", - title = "Google Developers podcast", - author = "Google Developers", - lastEpisodeDate = OffsetDateTime.now() +val PreviewPodcasts = + listOf( + PodcastInfo( + uri = "fakeUri://podcast/1", + title = "Android Developers Backstage", + author = "Android Developers", + isSubscribed = true, + lastEpisodeDate = OffsetDateTime.now(), + ), + PodcastInfo( + uri = "fakeUri://podcast/2", + title = "Google Developers podcast", + author = "Google Developers", + lastEpisodeDate = OffsetDateTime.now(), + ), ) -) -val PreviewEpisodes = listOf( - EpisodeInfo( - uri = "fakeUri://episode/1", - title = "Episode 140: Lorem ipsum dolor", - summary = "In this episode, Romain, Chet and Tor talked with Mady Melor and Artur " + - "Tsurkan from the System UI team about... Bubbles!", - published = OffsetDateTime.of( - 2020, 6, 2, 9, - 27, 0, 0, ZoneOffset.of("-0800") - ) +val PreviewEpisodes = + listOf( + EpisodeInfo( + uri = "fakeUri://episode/1", + title = "Episode 140: Lorem ipsum dolor", + summary = + "In this episode, Romain, Chet and Tor talked with Mady Melor and Artur " + + "Tsurkan from the System UI team about... Bubbles!", + published = + OffsetDateTime.of( + 2020, + 6, + 2, + 9, + 27, + 0, + 0, + ZoneOffset.of("-0800"), + ), + ), ) -) -val PreviewPlayerEpisodes = listOf( - PlayerEpisode( - PreviewPodcasts[0], - PreviewEpisodes[0] +val PreviewPlayerEpisodes = + listOf( + PlayerEpisode( + PreviewPodcasts[0], + PreviewEpisodes[0], + ), ) -) -val PreviewPodcastEpisodes = listOf( - PodcastToEpisodeInfo( - podcast = PreviewPodcasts[0], - episode = PreviewEpisodes[0], +val PreviewPodcastEpisodes = + listOf( + PodcastToEpisodeInfo( + podcast = PreviewPodcasts[0], + episode = PreviewEpisodes[0], + ), ) -) diff --git a/Jetcaster/core/domain/build.gradle.kts b/Jetcaster/core/domain/build.gradle.kts index a6803eda2b..c2e98288ca 100644 --- a/Jetcaster/core/domain/build.gradle.kts +++ b/Jetcaster/core/domain/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -6,11 +23,17 @@ plugins { } android { - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() namespace = "com.example.jetcaster.core.domain" defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -21,7 +44,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/di/DomainDiModule.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/di/DomainDiModule.kt index 13aa949a51..6331070af2 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/di/DomainDiModule.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/di/DomainDiModule.kt @@ -24,8 +24,8 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) @@ -33,6 +33,6 @@ object DomainDiModule { @Provides @Singleton fun provideEpisodePlayer( - @Dispatcher(JetcasterDispatchers.Main) mainDispatcher: CoroutineDispatcher + @Dispatcher(JetcasterDispatchers.Main) mainDispatcher: CoroutineDispatcher, ): EpisodePlayer = MockEpisodePlayer(mainDispatcher) } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/FilterableCategoriesUseCase.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/FilterableCategoriesUseCase.kt index 8b5f1a9b1c..2490435495 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/FilterableCategoriesUseCase.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/FilterableCategoriesUseCase.kt @@ -20,29 +20,33 @@ import com.example.jetcaster.core.data.repository.CategoryStore import com.example.jetcaster.core.model.CategoryInfo import com.example.jetcaster.core.model.FilterableCategoriesModel import com.example.jetcaster.core.model.asExternalModel -import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import javax.inject.Inject /** * Use case for categories that can be used to filter podcasts. */ -class FilterableCategoriesUseCase @Inject constructor( - private val categoryStore: CategoryStore -) { - /** - * Created a [FilterableCategoriesModel] from the list of categories in [categoryStore]. - * @param selectedCategory the currently selected category. If null, the first category - * returned by the backing category list will be selected in the returned - * FilterableCategoriesModel - */ - operator fun invoke(selectedCategory: CategoryInfo?): Flow = - categoryStore.categoriesSortedByPodcastCount() - .map { categories -> - FilterableCategoriesModel( - categories = categories.map { it.asExternalModel() }, - selectedCategory = selectedCategory - ?: categories.firstOrNull()?.asExternalModel() - ) - } -} +class FilterableCategoriesUseCase + @Inject + constructor( + private val categoryStore: CategoryStore, + ) { + /** + * Created a [FilterableCategoriesModel] from the list of categories in [categoryStore]. + * @param selectedCategory the currently selected category. If null, the first category + * returned by the backing category list will be selected in the returned + * FilterableCategoriesModel + */ + operator fun invoke(selectedCategory: CategoryInfo?): Flow = + categoryStore + .categoriesSortedByPodcastCount() + .map { categories -> + FilterableCategoriesModel( + categories = categories.map { it.asExternalModel() }, + selectedCategory = + selectedCategory + ?: categories.firstOrNull()?.asExternalModel(), + ) + } + } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCase.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCase.kt index 7e72545254..6adbbabb7b 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCase.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCase.kt @@ -19,25 +19,28 @@ package com.example.jetcaster.core.domain import com.example.jetcaster.core.data.database.model.EpisodeToPodcast import com.example.jetcaster.core.data.repository.EpisodeStore import com.example.jetcaster.core.data.repository.PodcastStore -import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest +import javax.inject.Inject /** * A use case which returns all the latest episodes from all the podcasts the user follows. */ -class GetLatestFollowedEpisodesUseCase @Inject constructor( - private val episodeStore: EpisodeStore, - private val podcastStore: PodcastStore, -) { - @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(): Flow> = - podcastStore.followedPodcastsSortedByLastEpisode() - .flatMapLatest { followedPodcasts -> - episodeStore.episodesInPodcasts( - followedPodcasts.map { it.podcast.uri }, - followedPodcasts.size * 5 - ) - } -} +class GetLatestFollowedEpisodesUseCase + @Inject + constructor( + private val episodeStore: EpisodeStore, + private val podcastStore: PodcastStore, + ) { + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke(): Flow> = + podcastStore + .followedPodcastsSortedByLastEpisode() + .flatMapLatest { followedPodcasts -> + episodeStore.episodesInPodcasts( + followedPodcasts.map { it.podcast.uri }, + followedPodcasts.size * 5, + ) + } + } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCase.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCase.kt index 97620a63c2..b9401305b8 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCase.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCase.kt @@ -22,38 +22,42 @@ import com.example.jetcaster.core.model.CategoryInfo import com.example.jetcaster.core.model.PodcastCategoryFilterResult import com.example.jetcaster.core.model.asExternalModel import com.example.jetcaster.core.model.asPodcastToEpisodeInfo -import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject /** * A use case which returns top podcasts and matching episodes in a given [Category]. */ -class PodcastCategoryFilterUseCase @Inject constructor( - private val categoryStore: CategoryStore -) { - operator fun invoke(category: CategoryInfo?): Flow { - if (category == null) { - return flowOf(PodcastCategoryFilterResult()) - } +class PodcastCategoryFilterUseCase + @Inject + constructor( + private val categoryStore: CategoryStore, + ) { + operator fun invoke(category: CategoryInfo?): Flow { + if (category == null) { + return flowOf(PodcastCategoryFilterResult()) + } - val recentPodcastsFlow = categoryStore.podcastsInCategorySortedByPodcastCount( - category.id, - limit = 10 - ) + val recentPodcastsFlow = + categoryStore.podcastsInCategorySortedByPodcastCount( + category.id, + limit = 10, + ) - val episodesFlow = categoryStore.episodesFromPodcastsInCategory( - category.id, - limit = 20 - ) + val episodesFlow = + categoryStore.episodesFromPodcastsInCategory( + category.id, + limit = 20, + ) - // Combine our flows and collect them into the view state StateFlow - return combine(recentPodcastsFlow, episodesFlow) { topPodcasts, episodes -> - PodcastCategoryFilterResult( - topPodcasts = topPodcasts.map { it.asExternalModel() }, - episodes = episodes.map { it.asPodcastToEpisodeInfo() } - ) + // Combine our flows and collect them into the view state StateFlow + return combine(recentPodcastsFlow, episodesFlow) { topPodcasts, episodes -> + PodcastCategoryFilterResult( + topPodcasts = topPodcasts.map { it.asExternalModel() }, + episodes = episodes.map { it.asPodcastToEpisodeInfo() }, + ) + } } } -} diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/CategoryInfo.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/CategoryInfo.kt index 833ada892c..8b8194ec25 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/CategoryInfo.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/CategoryInfo.kt @@ -20,7 +20,7 @@ import com.example.jetcaster.core.data.database.model.Category data class CategoryInfo( val id: Long, - val name: String + val name: String, ) const val CategoryTechnology = "Technology" @@ -28,5 +28,5 @@ const val CategoryTechnology = "Technology" fun Category.asExternalModel() = CategoryInfo( id = id, - name = name + name = name, ) diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/FilterableCategoriesModel.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/FilterableCategoriesModel.kt index 4cca646940..4b47d62dec 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/FilterableCategoriesModel.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/FilterableCategoriesModel.kt @@ -21,7 +21,7 @@ package com.example.jetcaster.core.model */ data class FilterableCategoriesModel( val categories: List = emptyList(), - val selectedCategory: CategoryInfo? = null + val selectedCategory: CategoryInfo? = null, ) { val isEmpty = categories.isEmpty() || selectedCategory == null } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/LibraryInfo.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/LibraryInfo.kt index 5731b07f80..f15ee0cfd0 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/LibraryInfo.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/LibraryInfo.kt @@ -17,5 +17,5 @@ package com.example.jetcaster.core.model data class LibraryInfo( - val episodes: List = emptyList() + val episodes: List = emptyList(), ) : List by episodes diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastCategoryFilterResult.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastCategoryFilterResult.kt index c0e6761ed0..871780034d 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastCategoryFilterResult.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastCategoryFilterResult.kt @@ -21,5 +21,5 @@ package com.example.jetcaster.core.model */ data class PodcastCategoryFilterResult( val topPodcasts: List = emptyList(), - val episodes: List = emptyList() + val episodes: List = emptyList(), ) diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt index eb88fdef51..02ce72b03b 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt @@ -17,10 +17,11 @@ package com.example.jetcaster.core.player import com.example.jetcaster.core.player.model.PlayerEpisode -import java.time.Duration import kotlinx.coroutines.flow.StateFlow +import java.time.Duration val DefaultPlaybackSpeed = Duration.ofSeconds(1) + data class EpisodePlayerState( val currentEpisode: PlayerEpisode? = null, val queue: List = emptyList(), @@ -34,7 +35,6 @@ data class EpisodePlayerState( * episodes, playing an episode, pausing, seeking, etc. */ interface EpisodePlayer { - /** * A StateFlow that emits the [EpisodePlayerState] as controls as invoked on this player. */ @@ -53,8 +53,8 @@ interface EpisodePlayer { fun addToQueue(episode: PlayerEpisode) /* - * Flushes the queue - */ + * Flushes the queue + */ fun removeAllFromQueue() /** diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt index f94a552b5b..ab4984c500 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt @@ -17,8 +17,6 @@ package com.example.jetcaster.core.player import com.example.jetcaster.core.player.model.PlayerEpisode -import java.time.Duration -import kotlin.reflect.KProperty import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -31,11 +29,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import java.time.Duration +import kotlin.reflect.KProperty class MockEpisodePlayer( - private val mainDispatcher: CoroutineDispatcher + private val mainDispatcher: CoroutineDispatcher, ) : EpisodePlayer { - private val _playerState = MutableStateFlow(EpisodePlayerState()) private val _currentEpisode = MutableStateFlow(null) private val queue = MutableStateFlow>(emptyList()) @@ -54,14 +53,14 @@ class MockEpisodePlayer( queue, isPlaying, timeElapsed, - _playerSpeed + _playerSpeed, ) { currentEpisode, queue, isPlaying, timeElapsed, playerSpeed -> EpisodePlayerState( currentEpisode = currentEpisode, queue = queue, isPlaying = isPlaying, timeElapsed = timeElapsed, - playbackSpeed = playerSpeed + playbackSpeed = playerSpeed, ) }.catch { // TODO handle error state @@ -77,6 +76,7 @@ class MockEpisodePlayer( override val playerState: StateFlow = _playerState.asStateFlow() override var currentEpisode: PlayerEpisode? by _currentEpisode + override fun addToQueue(episode: PlayerEpisode) { queue.update { it + episode @@ -96,21 +96,22 @@ class MockEpisodePlayer( val episode = _currentEpisode.value ?: return isPlaying.value = true - timerJob = coroutineScope.launch { - // Increment timer by a second - while (isActive && timeElapsed.value < episode.duration) { - delay(playerSpeed.toMillis()) - timeElapsed.update { it + playerSpeed } - } + timerJob = + coroutineScope.launch { + // Increment timer by a second + while (isActive && timeElapsed.value < episode.duration) { + delay(playerSpeed.toMillis()) + timeElapsed.update { it + playerSpeed } + } - // Once done playing, see if - isPlaying.value = false - timeElapsed.value = Duration.ZERO + // Once done playing, see if + isPlaying.value = false + timeElapsed.value = Duration.ZERO - if (hasNext()) { - next() + if (hasNext()) { + next() + } } - } } override fun play(playerEpisode: PlayerEpisode) { @@ -212,19 +213,19 @@ class MockEpisodePlayer( timerJob = null } - private fun hasNext(): Boolean { - return queue.value.isNotEmpty() - } + private fun hasNext(): Boolean = queue.value.isNotEmpty() } // Used to enable property delegation private operator fun MutableStateFlow.setValue( thisObj: Any?, property: KProperty<*>, - value: T + value: T, ) { this.value = value } -private operator fun MutableStateFlow.getValue(thisObj: Any?, property: KProperty<*>): T = - this.value +private operator fun MutableStateFlow.getValue( + thisObj: Any?, + property: KProperty<*>, +): T = this.value diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/model/PlayerEpisode.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/model/PlayerEpisode.kt index 8ebd257a88..391b1173f5 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/model/PlayerEpisode.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/model/PlayerEpisode.kt @@ -45,7 +45,7 @@ data class PlayerEpisode( author = episodeInfo.author, summary = episodeInfo.summary, podcastImageUrl = podcastInfo.imageUrl, - uri = episodeInfo.uri + uri = episodeInfo.uri, ) } diff --git a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/FilterableCategoriesUseCaseTest.kt b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/FilterableCategoriesUseCaseTest.kt index 4431dc29f3..5c591e3493 100644 --- a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/FilterableCategoriesUseCaseTest.kt +++ b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/FilterableCategoriesUseCaseTest.kt @@ -26,18 +26,19 @@ import org.junit.Before import org.junit.Test class FilterableCategoriesUseCaseTest { - private val categoriesStore = TestCategoryStore() - private val testCategories = listOf( - Category(1, "News"), - Category(2, "Arts"), - Category(4, "Technology"), - Category(2, "TV & Film"), - ) + private val testCategories = + listOf( + Category(1, "News"), + Category(2, "Arts"), + Category(4, "Technology"), + Category(2, "TV & Film"), + ) - val useCase = FilterableCategoriesUseCase( - categoryStore = categoriesStore - ) + val useCase = + FilterableCategoriesUseCase( + categoryStore = categoriesStore, + ) @Before fun setUp() { @@ -45,21 +46,23 @@ class FilterableCategoriesUseCaseTest { } @Test - fun whenNoSelectedCategory_onEmptySelectedCategoryInvoked() = runTest { - val filterableCategories = useCase(null).first() - assertEquals( - filterableCategories.categories[0], - filterableCategories.selectedCategory - ) - } + fun whenNoSelectedCategory_onEmptySelectedCategoryInvoked() = + runTest { + val filterableCategories = useCase(null).first() + assertEquals( + filterableCategories.categories[0], + filterableCategories.selectedCategory, + ) + } @Test - fun whenSelectedCategory_correctFilterableCategoryIsSelected() = runTest { - val selectedCategory = testCategories[2] - val filterableCategories = useCase(selectedCategory.asExternalModel()).first() - assertEquals( - selectedCategory.asExternalModel(), - filterableCategories.selectedCategory - ) - } + fun whenSelectedCategory_correctFilterableCategoryIsSelected() = + runTest { + val selectedCategory = testCategories[2] + val filterableCategories = useCase(selectedCategory.asExternalModel()).first() + assertEquals( + selectedCategory.asExternalModel(), + filterableCategories.selectedCategory, + ) + } } diff --git a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCaseTest.kt b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCaseTest.kt index c2a3133ed1..9658f12173 100644 --- a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCaseTest.kt +++ b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCaseTest.kt @@ -19,81 +19,86 @@ package com.example.jetcaster.core.domain import com.example.jetcaster.core.data.database.model.Episode import com.example.jetcaster.core.data.testing.repository.TestEpisodeStore import com.example.jetcaster.core.data.testing.repository.TestPodcastStore -import java.time.OffsetDateTime import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Assert.assertTrue import org.junit.Test +import java.time.OffsetDateTime class GetLatestFollowedEpisodesUseCaseTest { - private val episodeStore = TestEpisodeStore() private val podcastStore = TestPodcastStore() - val useCase = GetLatestFollowedEpisodesUseCase( - episodeStore = episodeStore, - podcastStore = podcastStore - ) + val useCase = + GetLatestFollowedEpisodesUseCase( + episodeStore = episodeStore, + podcastStore = podcastStore, + ) - val testEpisodes = listOf( - Episode( - uri = "", - podcastUri = testPodcasts[0].podcast.uri, - title = "title1", - published = OffsetDateTime.MIN - ), - Episode( - uri = "", - podcastUri = testPodcasts[0].podcast.uri, - title = "title2", - published = OffsetDateTime.now() - ), - Episode( - uri = "", - podcastUri = testPodcasts[1].podcast.uri, - title = "title3", - published = OffsetDateTime.MAX + val testEpisodes = + listOf( + Episode( + uri = "", + podcastUri = testPodcasts[0].podcast.uri, + title = "title1", + published = OffsetDateTime.MIN, + ), + Episode( + uri = "", + podcastUri = testPodcasts[0].podcast.uri, + title = "title2", + published = OffsetDateTime.now(), + ), + Episode( + uri = "", + podcastUri = testPodcasts[1].podcast.uri, + title = "title3", + published = OffsetDateTime.MAX, + ), ) - ) @Test - fun whenNoFollowedPodcasts_emptyFlow() = runTest { - val result = useCase() + fun whenNoFollowedPodcasts_emptyFlow() = + runTest { + val result = useCase() - episodeStore.addEpisodes(testEpisodes) - testPodcasts.forEach { - podcastStore.addPodcast(it.podcast) - } + episodeStore.addEpisodes(testEpisodes) + testPodcasts.forEach { + podcastStore.addPodcast(it.podcast) + } - assertTrue(result.first().isEmpty()) - } + assertTrue(result.first().isEmpty()) + } @Test - fun whenFollowedPodcasts_nonEmptyFlow() = runTest { - val result = useCase() + fun whenFollowedPodcasts_nonEmptyFlow() = + runTest { + val result = useCase() - episodeStore.addEpisodes(testEpisodes) - testPodcasts.forEach { - podcastStore.addPodcast(it.podcast) - } - podcastStore.togglePodcastFollowed(testPodcasts[0].podcast.uri) + episodeStore.addEpisodes(testEpisodes) + testPodcasts.forEach { + podcastStore.addPodcast(it.podcast) + } + podcastStore.togglePodcastFollowed(testPodcasts[0].podcast.uri) - assertTrue(result.first().isNotEmpty()) - } + assertTrue(result.first().isNotEmpty()) + } @Test - fun whenFollowedPodcasts_sortedByPublished() = runTest { - val result = useCase() + fun whenFollowedPodcasts_sortedByPublished() = + runTest { + val result = useCase() - episodeStore.addEpisodes(testEpisodes) - testPodcasts.forEach { - podcastStore.addPodcast(it.podcast) - } - podcastStore.togglePodcastFollowed(testPodcasts[0].podcast.uri) + episodeStore.addEpisodes(testEpisodes) + testPodcasts.forEach { + podcastStore.addPodcast(it.podcast) + } + podcastStore.togglePodcastFollowed(testPodcasts[0].podcast.uri) - result.first().zipWithNext { - ep1, ep2 -> - ep1.episode.published > ep2.episode.published - }.all { it } - } + result + .first() + .zipWithNext { ep1, ep2 -> + ep1.episode.published > ep2.episode.published + }.all { it } + } } diff --git a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCaseTest.kt b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCaseTest.kt index 7568d175b8..cebf792c81 100644 --- a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCaseTest.kt +++ b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCaseTest.kt @@ -24,123 +24,134 @@ import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo import com.example.jetcaster.core.data.testing.repository.TestCategoryStore import com.example.jetcaster.core.model.asExternalModel import com.example.jetcaster.core.model.asPodcastToEpisodeInfo -import java.time.OffsetDateTime import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test +import java.time.OffsetDateTime class PodcastCategoryFilterUseCaseTest { - private val categoriesStore = TestCategoryStore() - private val testEpisodeToPodcast = listOf( - EpisodeToPodcast().apply { - episode = Episode( - "", - "", - "Episode 1", - published = OffsetDateTime.now() - ) - _podcasts = listOf( - Podcast( - uri = "", - title = "Podcast 1" - ) - ) - }, - EpisodeToPodcast().apply { - episode = Episode( - "", - "", - "Episode 2", - published = OffsetDateTime.now() - ) - _podcasts = listOf( - Podcast( - uri = "", - title = "Podcast 2" - ) - ) - }, - EpisodeToPodcast().apply { - episode = Episode( - "", - "", - "Episode 3", - published = OffsetDateTime.now() - ) - _podcasts = listOf( - Podcast( - uri = "", - title = "Podcast 3" - ) - ) - } - ) + private val testEpisodeToPodcast = + listOf( + EpisodeToPodcast().apply { + episode = + Episode( + "", + "", + "Episode 1", + published = OffsetDateTime.now(), + ) + podcasts = + listOf( + Podcast( + uri = "", + title = "Podcast 1", + ), + ) + }, + EpisodeToPodcast().apply { + episode = + Episode( + "", + "", + "Episode 2", + published = OffsetDateTime.now(), + ) + podcasts = + listOf( + Podcast( + uri = "", + title = "Podcast 2", + ), + ) + }, + EpisodeToPodcast().apply { + episode = + Episode( + "", + "", + "Episode 3", + published = OffsetDateTime.now(), + ) + podcasts = + listOf( + Podcast( + uri = "", + title = "Podcast 3", + ), + ) + }, + ) private val testCategory = Category(1, "Technology") - val useCase = PodcastCategoryFilterUseCase( - categoryStore = categoriesStore - ) + val useCase = + PodcastCategoryFilterUseCase( + categoryStore = categoriesStore, + ) @Test - fun whenCategoryNull_emptyFlow() = runTest { - val resultFlow = useCase(null) + fun whenCategoryNull_emptyFlow() = + runTest { + val resultFlow = useCase(null) - categoriesStore.setEpisodesFromPodcast(testCategory.id, testEpisodeToPodcast) - categoriesStore.setPodcastsInCategory(testCategory.id, testPodcasts) + categoriesStore.setEpisodesFromPodcast(testCategory.id, testEpisodeToPodcast) + categoriesStore.setPodcastsInCategory(testCategory.id, testPodcasts) - val result = resultFlow.first() - assertTrue(result.topPodcasts.isEmpty()) - assertTrue(result.episodes.isEmpty()) - } + val result = resultFlow.first() + assertTrue(result.topPodcasts.isEmpty()) + assertTrue(result.episodes.isEmpty()) + } @Test - fun whenCategoryNotNull_validFlow() = runTest { - val resultFlow = useCase(testCategory.asExternalModel()) + fun whenCategoryNotNull_validFlow() = + runTest { + val resultFlow = useCase(testCategory.asExternalModel()) - categoriesStore.setEpisodesFromPodcast(testCategory.id, testEpisodeToPodcast) - categoriesStore.setPodcastsInCategory(testCategory.id, testPodcasts) + categoriesStore.setEpisodesFromPodcast(testCategory.id, testEpisodeToPodcast) + categoriesStore.setPodcastsInCategory(testCategory.id, testPodcasts) - val result = resultFlow.first() - assertEquals( - testPodcasts.map { it.asExternalModel() }, - result.topPodcasts - ) - assertEquals( - testEpisodeToPodcast.map { it.asPodcastToEpisodeInfo() }, - result.episodes - ) - } + val result = resultFlow.first() + assertEquals( + testPodcasts.map { it.asExternalModel() }, + result.topPodcasts, + ) + assertEquals( + testEpisodeToPodcast.map { it.asPodcastToEpisodeInfo() }, + result.episodes, + ) + } @Test - fun whenCategoryInfoNotNull_verifyLimitFlow() = runTest { - val resultFlow = useCase(testCategory.asExternalModel()) + fun whenCategoryInfoNotNull_verifyLimitFlow() = + runTest { + val resultFlow = useCase(testCategory.asExternalModel()) - categoriesStore.setEpisodesFromPodcast( - testCategory.id, - List(8) { testEpisodeToPodcast }.flatten() - ) - categoriesStore.setPodcastsInCategory( - testCategory.id, - List(4) { testPodcasts }.flatten() - ) + categoriesStore.setEpisodesFromPodcast( + testCategory.id, + List(8) { testEpisodeToPodcast }.flatten(), + ) + categoriesStore.setPodcastsInCategory( + testCategory.id, + List(4) { testPodcasts }.flatten(), + ) - val result = resultFlow.first() - assertEquals(20, result.episodes.size) - assertEquals(10, result.topPodcasts.size) - } + val result = resultFlow.first() + assertEquals(20, result.episodes.size) + assertEquals(10, result.topPodcasts.size) + } } -val testPodcasts = listOf( - PodcastWithExtraInfo().apply { - podcast = Podcast(uri = "nia", title = "Now in Android") - }, - PodcastWithExtraInfo().apply { - podcast = Podcast(uri = "adb", title = "Android Developers Backstage") - }, - PodcastWithExtraInfo().apply { - podcast = Podcast(uri = "techcrunch", title = "Techcrunch") - }, -) +val testPodcasts = + listOf( + PodcastWithExtraInfo().apply { + podcast = Podcast(uri = "nia", title = "Now in Android") + }, + PodcastWithExtraInfo().apply { + podcast = Podcast(uri = "adb", title = "Android Developers Backstage") + }, + PodcastWithExtraInfo().apply { + podcast = Podcast(uri = "techcrunch", title = "Techcrunch") + }, + ) diff --git a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/player/MockEpisodePlayerTest.kt b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/player/MockEpisodePlayerTest.kt index 96c91a66ce..f24b3c036e 100644 --- a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/player/MockEpisodePlayerTest.kt +++ b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/player/MockEpisodePlayerTest.kt @@ -18,7 +18,6 @@ package com.example.jetcaster.core.domain.player import com.example.jetcaster.core.player.MockEpisodePlayer import com.example.jetcaster.core.player.model.PlayerEpisode -import java.time.Duration import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceTimeBy @@ -27,104 +26,118 @@ import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test +import java.time.Duration @OptIn(ExperimentalCoroutinesApi::class) class MockEpisodePlayerTest { - private val testDispatcher = StandardTestDispatcher() private val mockEpisodePlayer = MockEpisodePlayer(testDispatcher) - private val testEpisodes = listOf( - PlayerEpisode( - uri = "uri1", - duration = Duration.ofSeconds(60) - ), - PlayerEpisode( - uri = "uri2", - duration = Duration.ofSeconds(60) - ), - PlayerEpisode( - uri = "uri3", - duration = Duration.ofSeconds(60) - ), - ) - - @Test - fun whenPlay_incrementsByPlaySpeed() = runTest(testDispatcher) { - val playSpeed = Duration.ofSeconds(2) - val currEpisode = PlayerEpisode( - uri = "currentEpisode", - duration = Duration.ofSeconds(60) + private val testEpisodes = + listOf( + PlayerEpisode( + uri = "uri1", + duration = Duration.ofSeconds(60), + ), + PlayerEpisode( + uri = "uri2", + duration = Duration.ofSeconds(60), + ), + PlayerEpisode( + uri = "uri3", + duration = Duration.ofSeconds(60), + ), ) - mockEpisodePlayer.currentEpisode = currEpisode - mockEpisodePlayer.playerSpeed = playSpeed - - mockEpisodePlayer.play() - advanceTimeBy(playSpeed.toMillis() + 300) - - assertEquals(playSpeed, mockEpisodePlayer.playerState.value.timeElapsed) - } @Test - fun whenPlayDone_playerAutoPlaysNextEpisode() = runTest(testDispatcher) { - val duration = Duration.ofSeconds(60) - val currEpisode = PlayerEpisode( - uri = "currentEpisode", - duration = duration - ) - mockEpisodePlayer.currentEpisode = currEpisode - testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } - - mockEpisodePlayer.play() - advanceTimeBy(duration.toMillis() + 1) - - assertEquals(testEpisodes.first(), mockEpisodePlayer.currentEpisode) - } + fun whenPlay_incrementsByPlaySpeed() = + runTest(testDispatcher) { + val playSpeed = Duration.ofSeconds(2) + val currEpisode = + PlayerEpisode( + uri = "currentEpisode", + duration = Duration.ofSeconds(60), + ) + mockEpisodePlayer.currentEpisode = currEpisode + mockEpisodePlayer.playerSpeed = playSpeed + + mockEpisodePlayer.play() + advanceTimeBy(playSpeed.toMillis() + 300) + + assertEquals(playSpeed, mockEpisodePlayer.playerState.value.timeElapsed) + } @Test - fun whenNext_queueIsNotEmpty_autoPlaysNextEpisode() = runTest(testDispatcher) { - val duration = Duration.ofSeconds(60) - val currEpisode = PlayerEpisode( - uri = "currentEpisode", - duration = duration - ) - - mockEpisodePlayer.currentEpisode = currEpisode - testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } - - mockEpisodePlayer.next() - advanceTimeBy(100) + fun whenPlayDone_playerAutoPlaysNextEpisode() = + runTest(testDispatcher) { + val duration = Duration.ofSeconds(60) + val currEpisode = + PlayerEpisode( + uri = "currentEpisode", + duration = duration, + ) + mockEpisodePlayer.currentEpisode = currEpisode + testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } + + mockEpisodePlayer.play() + advanceTimeBy(duration.toMillis() + 1) + + assertEquals(testEpisodes.first(), mockEpisodePlayer.currentEpisode) + } - assertTrue(mockEpisodePlayer.playerState.value.isPlaying) - } @Test - fun whenPlayListOfEpisodes_playerAutoPlaysNextEpisode() = runTest(testDispatcher) { - val duration = Duration.ofSeconds(60) - val currEpisode = PlayerEpisode( - uri = "currentEpisode", - duration = duration - ) - val firstEpisodeFromList = PlayerEpisode( - uri = "firstEpisodeFromList", - duration = duration - ) - val secondEpisodeFromList = PlayerEpisode( - uri = "secondEpisodeFromList", - duration = duration - ) - val episodeListToBeAddedToTheQueue: List = listOf( - firstEpisodeFromList, secondEpisodeFromList - ) - mockEpisodePlayer.currentEpisode = currEpisode - - mockEpisodePlayer.play(episodeListToBeAddedToTheQueue) - assertEquals(firstEpisodeFromList, mockEpisodePlayer.currentEpisode) - - advanceTimeBy(duration.toMillis() + 1) - assertEquals(secondEpisodeFromList, mockEpisodePlayer.currentEpisode) + fun whenNext_queueIsNotEmpty_autoPlaysNextEpisode() = + runTest(testDispatcher) { + val duration = Duration.ofSeconds(60) + val currEpisode = + PlayerEpisode( + uri = "currentEpisode", + duration = duration, + ) + + mockEpisodePlayer.currentEpisode = currEpisode + testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } + + mockEpisodePlayer.next() + advanceTimeBy(100) + + assertTrue(mockEpisodePlayer.playerState.value.isPlaying) + } - advanceTimeBy(duration.toMillis() + 1) - assertEquals(currEpisode, mockEpisodePlayer.currentEpisode) - } + @Test + fun whenPlayListOfEpisodes_playerAutoPlaysNextEpisode() = + runTest(testDispatcher) { + val duration = Duration.ofSeconds(60) + val currEpisode = + PlayerEpisode( + uri = "currentEpisode", + duration = duration, + ) + val firstEpisodeFromList = + PlayerEpisode( + uri = "firstEpisodeFromList", + duration = duration, + ) + val secondEpisodeFromList = + PlayerEpisode( + uri = "secondEpisodeFromList", + duration = duration, + ) + val episodeListToBeAddedToTheQueue: List = + listOf( + firstEpisodeFromList, + secondEpisodeFromList, + ) + mockEpisodePlayer.currentEpisode = currEpisode + + mockEpisodePlayer.play(episodeListToBeAddedToTheQueue) + assertEquals(firstEpisodeFromList, mockEpisodePlayer.currentEpisode) + + advanceTimeBy(duration.toMillis() + 1) + assertEquals(secondEpisodeFromList, mockEpisodePlayer.currentEpisode) + + advanceTimeBy(duration.toMillis() + 1) + assertEquals(currEpisode, mockEpisodePlayer.currentEpisode) + } @Test fun whenNext_queueIsEmpty_doesNothing() { @@ -138,66 +151,76 @@ class MockEpisodePlayerTest { } @Test - fun whenAddToQueue_queueIsNotEmpty() = runTest(testDispatcher) { - testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } + fun whenAddToQueue_queueIsNotEmpty() = + runTest(testDispatcher) { + testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } - advanceUntilIdle() + advanceUntilIdle() - val queue = mockEpisodePlayer.playerState.value.queue - assertEquals(testEpisodes.size, queue.size) - testEpisodes.forEachIndexed { index, playerEpisode -> - assertEquals(playerEpisode, queue[index]) + val queue = mockEpisodePlayer.playerState.value.queue + assertEquals(testEpisodes.size, queue.size) + testEpisodes.forEachIndexed { index, playerEpisode -> + assertEquals(playerEpisode, queue[index]) + } } - } @Test - fun whenNext_queueIsNotEmpty_removeFromQueue() = runTest(testDispatcher) { - mockEpisodePlayer.currentEpisode = PlayerEpisode( - uri = "currentEpisode", - duration = Duration.ofSeconds(60) - ) - testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } + fun whenNext_queueIsNotEmpty_removeFromQueue() = + runTest(testDispatcher) { + mockEpisodePlayer.currentEpisode = + PlayerEpisode( + uri = "currentEpisode", + duration = Duration.ofSeconds(60), + ) + testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } - mockEpisodePlayer.play() - advanceTimeBy(100) + mockEpisodePlayer.play() + advanceTimeBy(100) - mockEpisodePlayer.next() - advanceTimeBy(100) + mockEpisodePlayer.next() + advanceTimeBy(100) - assertEquals(testEpisodes.first(), mockEpisodePlayer.currentEpisode) + assertEquals(testEpisodes.first(), mockEpisodePlayer.currentEpisode) - val queue = mockEpisodePlayer.playerState.value.queue - assertEquals(testEpisodes.size - 1, queue.size) - } + val queue = mockEpisodePlayer.playerState.value.queue + assertEquals(testEpisodes.size - 1, queue.size) + } @Test - fun whenNext_queueIsNotEmpty_notRemovedFromQueue() = runTest(testDispatcher) { - mockEpisodePlayer.currentEpisode = PlayerEpisode( - uri = "currentEpisode", - duration = Duration.ofSeconds(60) - ) - testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } + fun whenNext_queueIsNotEmpty_notRemovedFromQueue() = + runTest(testDispatcher) { + mockEpisodePlayer.currentEpisode = + PlayerEpisode( + uri = "currentEpisode", + duration = Duration.ofSeconds(60), + ) + testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } - mockEpisodePlayer.play() - advanceTimeBy(100) + mockEpisodePlayer.play() + advanceTimeBy(100) - mockEpisodePlayer.next() - advanceTimeBy(100) + mockEpisodePlayer.next() + advanceTimeBy(100) - assertEquals(testEpisodes.first(), mockEpisodePlayer.currentEpisode) + assertEquals(testEpisodes.first(), mockEpisodePlayer.currentEpisode) - val queue = mockEpisodePlayer.playerState.value.queue - assertEquals(testEpisodes.size - 1, queue.size) - } + val queue = mockEpisodePlayer.playerState.value.queue + assertEquals(testEpisodes.size - 1, queue.size) + } @Test - fun whenPrevious_queueIsEmpty_resetSameEpisode() = runTest(testDispatcher) { - mockEpisodePlayer.currentEpisode = testEpisodes[0] - mockEpisodePlayer.play() - advanceTimeBy(1000L) - - mockEpisodePlayer.previous() - assertEquals(0, mockEpisodePlayer.playerState.value.timeElapsed.toMillis()) - assertEquals(testEpisodes[0], mockEpisodePlayer.currentEpisode) - } + fun whenPrevious_queueIsEmpty_resetSameEpisode() = + runTest(testDispatcher) { + mockEpisodePlayer.currentEpisode = testEpisodes[0] + mockEpisodePlayer.play() + advanceTimeBy(1000L) + + mockEpisodePlayer.previous() + assertEquals( + 0, + mockEpisodePlayer.playerState.value.timeElapsed + .toMillis(), + ) + assertEquals(testEpisodes[0], mockEpisodePlayer.currentEpisode) + } } diff --git a/Jetcaster/glancewidget/build.gradle.kts b/Jetcaster/glancewidget/build.gradle.kts index e1320ffbc5..93982eb558 100644 --- a/Jetcaster/glancewidget/build.gradle.kts +++ b/Jetcaster/glancewidget/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -6,10 +23,16 @@ plugins { android { namespace = "com.example.jetcaster.glancewidget" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/Colors.kt b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/Colors.kt index 8aa3901790..e7c4207b95 100644 --- a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/Colors.kt +++ b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/Colors.kt @@ -92,81 +92,83 @@ import com.example.jetcaster.designsystem.theme.tertiaryLight /** * Todo, this is copied from the core module. Refactor colors out of that so we can reference them. */ -private val lightJetcasterColors = lightColorScheme( - primary = primaryLight, - onPrimary = onPrimaryLight, - primaryContainer = primaryContainerLight, - onPrimaryContainer = onPrimaryContainerLight, - secondary = secondaryLight, - onSecondary = onSecondaryLight, - secondaryContainer = secondaryContainerLight, - onSecondaryContainer = onSecondaryContainerLight, - tertiary = tertiaryLight, - onTertiary = onTertiaryLight, - tertiaryContainer = tertiaryContainerLight, - onTertiaryContainer = onTertiaryContainerLight, - error = errorLight, - onError = onErrorLight, - errorContainer = errorContainerLight, - onErrorContainer = onErrorContainerLight, - background = backgroundLight, - onBackground = onBackgroundLight, - surface = surfaceLight, - onSurface = onSurfaceLight, - surfaceVariant = surfaceVariantLight, - onSurfaceVariant = onSurfaceVariantLight, - outline = outlineLight, - outlineVariant = outlineVariantLight, - scrim = scrimLight, - inverseSurface = inverseSurfaceLight, - inverseOnSurface = inverseOnSurfaceLight, - inversePrimary = inversePrimaryLight, - surfaceDim = surfaceDimLight, - surfaceBright = surfaceBrightLight, - surfaceContainerLowest = surfaceContainerLowestLight, - surfaceContainerLow = surfaceContainerLowLight, - surfaceContainer = surfaceContainerLight, - surfaceContainerHigh = surfaceContainerHighLight, - surfaceContainerHighest = surfaceContainerHighestLight, -) +private val lightJetcasterColors = + lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, + ) /** * Todo, this is copied from the core module. Refactor colors out of that so we can reference them. */ -internal val DarkJetcasterColors = darkColorScheme( - primary = primaryDark, - onPrimary = onPrimaryDark, - primaryContainer = primaryContainerDark, - onPrimaryContainer = onPrimaryContainerDark, - secondary = secondaryDark, - onSecondary = onSecondaryDark, - secondaryContainer = secondaryContainerDark, - onSecondaryContainer = onSecondaryContainerDark, - tertiary = tertiaryDark, - onTertiary = onTertiaryDark, - tertiaryContainer = tertiaryContainerDark, - onTertiaryContainer = onTertiaryContainerDark, - error = errorDark, - onError = onErrorDark, - errorContainer = errorContainerDark, - onErrorContainer = onErrorContainerDark, - background = backgroundDark, - onBackground = onBackgroundDark, - surface = surfaceDark, - onSurface = onSurfaceDark, - surfaceVariant = surfaceVariantDark, - onSurfaceVariant = onSurfaceVariantDark, - outline = outlineDark, - outlineVariant = outlineVariantDark, - scrim = scrimDark, - inverseSurface = inverseSurfaceDark, - inverseOnSurface = inverseOnSurfaceDark, - inversePrimary = inversePrimaryDark, - surfaceDim = surfaceDimDark, - surfaceBright = surfaceBrightDark, - surfaceContainerLowest = surfaceContainerLowestDark, - surfaceContainerLow = surfaceContainerLowDark, - surfaceContainer = surfaceContainerDark, - surfaceContainerHigh = surfaceContainerHighDark, - surfaceContainerHighest = surfaceContainerHighestDark, -) +internal val DarkJetcasterColors = + darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, + ) diff --git a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt index 0fa21520e8..7184f253d3 100644 --- a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt +++ b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt @@ -85,12 +85,14 @@ data class JetcasterAppWidgetViewState( val podcastTitle: String, val isPlaying: Boolean, val albumArtUri: String, - val useDynamicColor: Boolean + val useDynamicColor: Boolean, ) private object Sizes { val minWidth = 140.dp - val smallBucketCutoffWidth = 250.dp // anything from minWidth to this will have no title + + // anything from minWidth to this will have no title + val smallBucketCutoffWidth = 250.dp val imageNormal = 80.dp val imageCondensed = 60.dp @@ -114,17 +116,21 @@ class JetcasterAppWidget : GlanceAppWidget() { override val sizeMode: SizeMode get() = SizeMode.Exact - override suspend fun provideGlance(context: Context, id: GlanceId) { - - val testState = JetcasterAppWidgetViewState( - episodeTitle = - "100 - Android 15 DP 1, Stable Studio Iguana, Cloud Photo Picker, and more!", - podcastTitle = "Now in Android", - isPlaying = false, - albumArtUri = "https://static.libsyn.com/p/assets/9/f/f/3/" + - "9ff3cb5dc6cfb3e2e5bbc093207a2619/NIA000_PodcastThumbnail.png", - useDynamicColor = false - ) + override suspend fun provideGlance( + context: Context, + id: GlanceId, + ) { + val testState = + JetcasterAppWidgetViewState( + episodeTitle = + "100 - Android 15 DP 1, Stable Studio Iguana, Cloud Photo Picker, and more!", + podcastTitle = "Now in Android", + isPlaying = false, + albumArtUri = + "https://static.libsyn.com/p/assets/9/f/f/3/" + + "9ff3cb5dc6cfb3e2e5bbc093207a2619/NIA000_PodcastThumbnail.png", + useDynamicColor = false, + ) provideContent { val sizeBucket = calculateSizeBucket() @@ -132,24 +138,27 @@ class JetcasterAppWidget : GlanceAppWidget() { val artUri = Uri.parse(testState.albumArtUri) GlanceTheme( - colors = ColorProviders( - light = lightColorScheme(), - dark = darkColorScheme() - ) + colors = + ColorProviders( + light = lightColorScheme(), + dark = darkColorScheme(), + ), ) { when (sizeBucket) { SizeBucket.Invalid -> WidgetUiInvalidSize() - SizeBucket.Narrow -> WidgetUiNarrow( - imageUri = artUri, - playPauseIcon = playPauseIcon - ) + SizeBucket.Narrow -> + WidgetUiNarrow( + imageUri = artUri, + playPauseIcon = playPauseIcon, + ) - SizeBucket.Normal -> WidgetUiNormal( - title = testState.episodeTitle, - subtitle = testState.podcastTitle, - imageUri = artUri, - playPauseIcon = playPauseIcon - ) + SizeBucket.Normal -> + WidgetUiNormal( + title = testState.episodeTitle, + subtitle = testState.podcastTitle, + imageUri = artUri, + playPauseIcon = playPauseIcon, + ) } } } @@ -163,9 +172,11 @@ private fun WidgetUiNormal( imageUri: Uri, playPauseIcon: PlayPauseIcon, ) { - Scaffold(titleBar = {} /* title bar will be optional starting in glance 1.1.0-beta3*/) { + // title bar will be optional in scaffold in glance 1.1.0-beta3 + Scaffold(titleBar = {}) { Row( - GlanceModifier.fillMaxSize(), verticalAlignment = Alignment.Vertical.CenterVertically + GlanceModifier.fillMaxSize(), + verticalAlignment = Alignment.Vertical.CenterVertically, ) { AlbumArt(imageUri, GlanceModifier.size(Sizes.imageNormal)) PodcastText(title, subtitle, modifier = GlanceModifier.padding(16.dp).defaultWeight()) @@ -179,10 +190,11 @@ private fun WidgetUiNarrow( imageUri: Uri, playPauseIcon: PlayPauseIcon, ) { - Scaffold(titleBar = {} /* title bar will be optional in scaffold in glance 1.1.0-beta3*/) { + // title bar will be optional in scaffold in glance 1.1.0-beta3 + Scaffold(titleBar = {}) { Row( modifier = GlanceModifier.fillMaxSize(), - verticalAlignment = Alignment.Vertical.CenterVertically + verticalAlignment = Alignment.Vertical.CenterVertically, ) { AlbumArt(imageUri, GlanceModifier.size(Sizes.imageCondensed)) Spacer(GlanceModifier.defaultWeight()) @@ -201,13 +213,17 @@ private fun WidgetUiInvalidSize() { @Composable private fun AlbumArt( imageUri: Uri, - modifier: GlanceModifier = GlanceModifier + modifier: GlanceModifier = GlanceModifier, ) { WidgetAsyncImage(uri = imageUri, contentDescription = null, modifier = modifier) } @Composable -fun PodcastText(title: String, subtitle: String, modifier: GlanceModifier = GlanceModifier) { +fun PodcastText( + title: String, + subtitle: String, + modifier: GlanceModifier = GlanceModifier, +) { val fgColor = GlanceTheme.colors.onPrimaryContainer Column(modifier) { Text( @@ -224,11 +240,15 @@ fun PodcastText(title: String, subtitle: String, modifier: GlanceModifier = Glan } @Composable -private fun PlayPauseButton(state: PlayPauseIcon, onClick: () -> Unit) { - val (iconRes: Int, description: Int) = when (state) { - PlayPauseIcon.Play -> R.drawable.outline_play_arrow_24 to R.string.content_description_play - PlayPauseIcon.Pause -> R.drawable.outline_pause_24 to R.string.content_description_pause - } +private fun PlayPauseButton( + state: PlayPauseIcon, + onClick: () -> Unit, +) { + val (iconRes: Int, description: Int) = + when (state) { + PlayPauseIcon.Play -> R.drawable.outline_play_arrow_24 to R.string.content_description_play + PlayPauseIcon.Pause -> R.drawable.outline_pause_24 to R.string.content_description_pause + } val provider = ImageProvider(iconRes) val contentDescription = LocalContext.current.getString(description) @@ -236,7 +256,7 @@ private fun PlayPauseButton(state: PlayPauseIcon, onClick: () -> Unit) { SquareIconButton( provider, contentDescription = contentDescription, - onClick = onClick + onClick = onClick, ) } @@ -249,20 +269,21 @@ enum class PlayPauseIcon { Play, Pause } private fun WidgetAsyncImage( uri: Uri, contentDescription: String?, - modifier: GlanceModifier = GlanceModifier + modifier: GlanceModifier = GlanceModifier, ) { var bitmap by remember { mutableStateOf(null) } val context = LocalContext.current val scope = rememberCoroutineScope() LaunchedEffect(key1 = uri) { - val request = ImageRequest.Builder(context) - .data(uri) - .size(200, 200) - .target { data: Drawable -> - bitmap = (data as BitmapDrawable).bitmap - } - .build() + val request = + ImageRequest + .Builder(context) + .data(uri) + .size(200, 200) + .target { data: Drawable -> + bitmap = (data as BitmapDrawable).bitmap + }.build() scope.launch(Dispatchers.IO) { val result = ImageLoader(context).execute(request) @@ -278,7 +299,8 @@ private fun WidgetAsyncImage( provider = ImageProvider(bitmap), contentDescription = contentDescription, contentScale = ContentScale.FillBounds, - modifier = modifier.cornerRadius(12.dp) // TODO: confirm radius with design + // TODO: confirm radius with design + modifier = modifier.cornerRadius(12.dp), ) } } diff --git a/Jetcaster/gradle/libs.versions.toml b/Jetcaster/gradle/libs.versions.toml index 2c34e54e54..016f154a0d 100644 --- a/Jetcaster/gradle/libs.versions.toml +++ b/Jetcaster/gradle/libs.versions.toml @@ -47,6 +47,7 @@ junit = "4.13.2" kotlin = "2.0.0" kotlinx_immutable = "0.3.7" ksp = "2.0.0-1.0.21" +ktlint = "1.3.1" maps-compose = "3.1.1" # @keep minSdk = "21" @@ -57,6 +58,7 @@ roborazzi = "1.12.0" rome = "1.18.0" room = "2.6.0" secrets = "2.0.1" +spotless = "6.25.0" # @keep targetSdk = "33" version-catalog-update = "0.8.4" @@ -179,4 +181,5 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } diff --git a/Jetcaster/mobile/build.gradle.kts b/Jetcaster/mobile/build.gradle.kts index 1c5098c549..07de934cd9 100644 --- a/Jetcaster/mobile/build.gradle.kts +++ b/Jetcaster/mobile/build.gradle.kts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,6 +14,7 @@ * limitations under the License. */ + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -22,15 +23,23 @@ plugins { alias(libs.plugins.compose) } - android { - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() namespace = "com.example.jetcaster" defaultConfig { applicationId = "com.example.jetcaster" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() + targetSdk = + libs.versions.targetSdk + .get() + .toInt() versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -52,7 +61,6 @@ android { buildTypes { getByName("debug") { - } getByName("release") { @@ -60,7 +68,7 @@ android { signingConfig = signingConfigs.getByName("release") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/JetcasterApplication.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/JetcasterApplication.kt index 120187d2d5..a16c159074 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/JetcasterApplication.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/JetcasterApplication.kt @@ -26,8 +26,9 @@ import javax.inject.Inject * Application which sets up our dependency [Graph] with a context. */ @HiltAndroidApp -class JetcasterApplication : Application(), ImageLoaderFactory { - +class JetcasterApplication : + Application(), + ImageLoaderFactory { @Inject lateinit var imageLoader: ImageLoader override fun newImageLoader(): ImageLoader = imageLoader diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt index 15c9472cdd..4ff3580e08 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt @@ -34,27 +34,27 @@ import com.example.jetcaster.ui.player.PlayerScreen @Composable fun JetcasterApp( displayFeatures: List, - appState: JetcasterAppState = rememberJetcasterAppState() + appState: JetcasterAppState = rememberJetcasterAppState(), ) { val adaptiveInfo = currentWindowAdaptiveInfo() if (appState.isOnline) { NavHost( navController = appState.navController, - startDestination = Screen.Home.route + startDestination = Screen.Home.route, ) { composable(Screen.Home.route) { backStackEntry -> MainScreen( windowSizeClass = adaptiveInfo.windowSizeClass, navigateToPlayer = { episode -> appState.navigateToPlayer(episode.uri, backStackEntry) - } + }, ) } composable(Screen.Player.route) { PlayerScreen( windowSizeClass = adaptiveInfo.windowSizeClass, displayFeatures = displayFeatures, - onBackPress = appState::navigateBack + onBackPress = appState::navigateBack, ) } } @@ -73,6 +73,6 @@ fun OfflineDialog(onRetry: () -> Unit) { TextButton(onClick = onRetry) { Text(stringResource(R.string.retry_label)) } - } + }, ) } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterAppState.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterAppState.kt index ee938066a7..a98fdd90f9 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterAppState.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterAppState.kt @@ -36,15 +36,18 @@ import androidx.navigation.compose.rememberNavController /** * List of screens for [JetcasterApp] */ -sealed class Screen(val route: String) { +sealed class Screen( + val route: String, +) { object Home : Screen("home") + object Player : Screen("player/{$ARG_EPISODE_URI}") { fun createRoute(episodeUri: String) = "player/$episodeUri" } object PodcastDetails : Screen("podcast/{$ARG_PODCAST_URI}") { - val PODCAST_URI = "podcastUri" + fun createRoute(podcastUri: String) = "podcast/$podcastUri" } @@ -57,14 +60,14 @@ sealed class Screen(val route: String) { @Composable fun rememberJetcasterAppState( navController: NavHostController = rememberNavController(), - context: Context = LocalContext.current + context: Context = LocalContext.current, ) = remember(navController, context) { JetcasterAppState(navController, context) } class JetcasterAppState( val navController: NavHostController, - private val context: Context + private val context: Context, ) { var isOnline by mutableStateOf(checkIfOnline()) private set @@ -73,7 +76,10 @@ class JetcasterAppState( isOnline = checkIfOnline() } - fun navigateToPlayer(episodeUri: String, from: NavBackStackEntry) { + fun navigateToPlayer( + episodeUri: String, + from: NavBackStackEntry, + ) { // In order to discard duplicated navigation events, we check the Lifecycle if (from.lifecycleIsResumed()) { val encodedUri = Uri.encode(episodeUri) @@ -81,7 +87,10 @@ class JetcasterAppState( } } - fun navigateToPodcastDetails(podcastUri: String, from: NavBackStackEntry) { + fun navigateToPodcastDetails( + podcastUri: String, + from: NavBackStackEntry, + ) { if (from.lifecycleIsResumed()) { val encodedUri = Uri.encode(podcastUri) navController.navigate(Screen.PodcastDetails.createRoute(encodedUri)) @@ -111,5 +120,4 @@ class JetcasterAppState( * * This is used to de-duplicate navigation events. */ -private fun NavBackStackEntry.lifecycleIsResumed() = - this.lifecycle.currentState == Lifecycle.State.RESUMED +private fun NavBackStackEntry.lifecycleIsResumed() = this.lifecycle.currentState == Lifecycle.State.RESUMED diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/MainActivity.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/MainActivity.kt index 8e777683da..ad056ffa10 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/MainActivity.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/MainActivity.kt @@ -36,7 +36,7 @@ class MainActivity : ComponentActivity() { JetcasterTheme { JetcasterApp( - displayFeatures + displayFeatures, ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt index 46a0cffbf0..8196f398aa 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt @@ -124,12 +124,12 @@ import com.example.jetcaster.util.fullWidthItem import com.example.jetcaster.util.isCompact import com.example.jetcaster.util.quantityStringResource import com.example.jetcaster.util.radialGradientScrim -import java.time.Duration -import java.time.LocalDateTime -import java.time.OffsetDateTime import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch +import java.time.Duration +import java.time.LocalDateTime +import java.time.OffsetDateTime data class HomeState( val windowSizeClass: WindowSizeClass, @@ -154,18 +154,16 @@ private val HomeState.showHomeCategoryTabs: Boolean get() = featuredPodcasts.isNotEmpty() && homeCategories.isNotEmpty() @OptIn(ExperimentalMaterial3AdaptiveApi::class) -private fun HomeState.showGrid( - scaffoldValue: ThreePaneScaffoldValue -): Boolean = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED || - ( - windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM && - scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden +private fun HomeState.showGrid(scaffoldValue: ThreePaneScaffoldValue): Boolean = + windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED || + ( + windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM && + scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden ) @OptIn(ExperimentalMaterial3AdaptiveApi::class) -private fun ThreePaneScaffoldNavigator.isMainPaneHidden(): Boolean { - return scaffoldValue[SupportingPaneScaffoldRole.Main] == PaneAdaptedValue.Hidden -} +private fun ThreePaneScaffoldNavigator.isMainPaneHidden(): Boolean = + scaffoldValue[SupportingPaneScaffoldRole.Main] == PaneAdaptedValue.Hidden /** * Copied from `calculatePaneScaffoldDirective()` in [PaneScaffoldDirective], with modifications to @@ -174,7 +172,7 @@ private fun ThreePaneScaffoldNavigator.isMainPaneHidden(): Boolean { @OptIn(ExperimentalMaterial3AdaptiveApi::class) fun calculateScaffoldDirective( windowAdaptiveInfo: WindowAdaptiveInfo, - verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating + verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating, ): PaneScaffoldDirective { val maxHorizontalPartitions: Int val verticalSpacerSize: Dp @@ -217,7 +215,7 @@ fun calculateScaffoldDirective( maxVerticalPartitions, horizontalSpacerSize, defaultPanePreferredWidth, - getExcludedVerticalBounds(windowAdaptiveInfo.windowPosture, verticalHingePolicy) + getExcludedVerticalBounds(windowAdaptiveInfo.windowPosture, verticalHingePolicy), ) } @@ -225,20 +223,22 @@ fun calculateScaffoldDirective( * Copied from `getExcludedVerticalBounds()` in [PaneScaffoldDirective] since it is private. */ @OptIn(ExperimentalMaterial3AdaptiveApi::class) -private fun getExcludedVerticalBounds(posture: Posture, hingePolicy: HingePolicy): List { - return when (hingePolicy) { +private fun getExcludedVerticalBounds( + posture: Posture, + hingePolicy: HingePolicy, +): List = + when (hingePolicy) { HingePolicy.AvoidSeparating -> posture.separatingVerticalHingeBounds HingePolicy.AvoidOccluding -> posture.occludingVerticalHingeBounds HingePolicy.AlwaysAvoid -> posture.allVerticalHingeBounds else -> emptyList() } -} @Composable fun MainScreen( windowSizeClass: WindowSizeClass, navigateToPlayer: (EpisodeInfo) -> Unit, - viewModel: HomeViewModel = hiltViewModel() + viewModel: HomeViewModel = hiltViewModel(), ) { val homeScreenUiState by viewModel.state.collectAsStateWithLifecycle() when (val uiState = homeScreenUiState) { @@ -260,14 +260,17 @@ private fun HomeScreenLoading(modifier: Modifier = Modifier) { Surface(modifier.fillMaxSize()) { Box { CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center) + modifier = Modifier.align(Alignment.Center), ) } } } @Composable -private fun HomeScreenError(onRetry: () -> Unit, modifier: Modifier = Modifier) { +private fun HomeScreenError( + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { Surface(modifier = modifier) { Column( verticalArrangement = Arrangement.Center, @@ -276,7 +279,7 @@ private fun HomeScreenError(onRetry: () -> Unit, modifier: Modifier = Modifier) ) { Text( text = stringResource(id = R.string.an_error_has_occurred), - modifier = Modifier.padding(16.dp) + modifier = Modifier.padding(16.dp), ) Button(onClick = onRetry) { Text(text = stringResource(id = R.string.retry_label)) @@ -299,34 +302,36 @@ private fun HomeScreenReady( uiState: HomeScreenUiState.Ready, windowSizeClass: WindowSizeClass, navigateToPlayer: (EpisodeInfo) -> Unit, - viewModel: HomeViewModel = hiltViewModel() + viewModel: HomeViewModel = hiltViewModel(), ) { - val navigator = rememberSupportingPaneScaffoldNavigator( - scaffoldDirective = calculateScaffoldDirective(currentWindowAdaptiveInfo()) - ) + val navigator = + rememberSupportingPaneScaffoldNavigator( + scaffoldDirective = calculateScaffoldDirective(currentWindowAdaptiveInfo()), + ) BackHandler(enabled = navigator.canNavigateBack()) { navigator.navigateBack() } - val homeState = HomeState( - windowSizeClass = windowSizeClass, - featuredPodcasts = uiState.featuredPodcasts, - homeCategories = uiState.homeCategories, - selectedHomeCategory = uiState.selectedHomeCategory, - filterableCategoriesModel = uiState.filterableCategoriesModel, - podcastCategoryFilterResult = uiState.podcastCategoryFilterResult, - library = uiState.library, - onHomeCategorySelected = viewModel::onHomeCategorySelected, - onCategorySelected = viewModel::onCategorySelected, - onPodcastUnfollowed = viewModel::onPodcastUnfollowed, - navigateToPodcastDetails = { - navigator.navigateTo(SupportingPaneScaffoldRole.Supporting, it.uri) - }, - navigateToPlayer = navigateToPlayer, - onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed, - onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected, - onQueueEpisode = viewModel::onQueueEpisode - ) + val homeState = + HomeState( + windowSizeClass = windowSizeClass, + featuredPodcasts = uiState.featuredPodcasts, + homeCategories = uiState.homeCategories, + selectedHomeCategory = uiState.selectedHomeCategory, + filterableCategoriesModel = uiState.filterableCategoriesModel, + podcastCategoryFilterResult = uiState.podcastCategoryFilterResult, + library = uiState.library, + onHomeCategorySelected = viewModel::onHomeCategorySelected, + onCategorySelected = viewModel::onCategorySelected, + onPodcastUnfollowed = viewModel::onPodcastUnfollowed, + navigateToPodcastDetails = { + navigator.navigateTo(SupportingPaneScaffoldRole.Supporting, it.uri) + }, + navigateToPlayer = navigateToPlayer, + onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed, + onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected, + onQueueEpisode = viewModel::onQueueEpisode, + ) Surface { val podcastUri = navigator.currentDestination?.content @@ -335,7 +340,7 @@ private fun HomeScreenReady( HomeScreen( homeState = homeState, showGrid = showGrid, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } else { SupportingPaneScaffold( @@ -344,7 +349,7 @@ private fun HomeScreenReady( supportingPane = { val podcastDetailsViewModel = hiltViewModel( - key = podcastUri + key = podcastUri, ) { it.create(podcastUri) } @@ -363,10 +368,10 @@ private fun HomeScreenReady( HomeScreen( homeState = homeState, showGrid = showGrid, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) }, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } } @@ -380,10 +385,11 @@ private fun HomeAppBar( ) { Row( horizontalArrangement = Arrangement.End, - modifier = modifier - .fillMaxWidth() - .background(Color.Transparent) - .padding(end = 16.dp, top = 8.dp, bottom = 8.dp) + modifier = + modifier + .fillMaxWidth() + .background(Color.Transparent) + .padding(end = 16.dp, top = 8.dp, bottom = 8.dp), ) { SearchBar( query = "", @@ -397,16 +403,16 @@ private fun HomeAppBar( leadingIcon = { Icon( imageVector = Icons.Default.Search, - contentDescription = null + contentDescription = null, ) }, trailingIcon = { Icon( imageVector = Icons.Default.AccountCircle, - contentDescription = stringResource(R.string.cd_account) + contentDescription = stringResource(R.string.cd_account), ) }, - modifier = if (isExpanded) Modifier else Modifier.fillMaxWidth() + modifier = if (isExpanded) Modifier else Modifier.fillMaxWidth(), ) { } } } @@ -414,16 +420,18 @@ private fun HomeAppBar( @Composable private fun HomeScreenBackground( modifier: Modifier = Modifier, - content: @Composable BoxScope.() -> Unit + content: @Composable BoxScope.() -> Unit, ) { Box( - modifier = modifier - .background(MaterialTheme.colorScheme.background) + modifier = + modifier + .background(MaterialTheme.colorScheme.background), ) { Box( - modifier = Modifier - .fillMaxSize() - .radialGradientScrim(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)) + modifier = + Modifier + .fillMaxSize() + .radialGradientScrim(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)), ) content() } @@ -433,7 +441,7 @@ private fun HomeScreenBackground( private fun HomeScreen( homeState: HomeState, showGrid: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { // Effect that changes the home category selection when there are no subscribed podcasts LaunchedEffect(key1 = homeState.featuredPodcasts) { @@ -445,7 +453,7 @@ private fun HomeScreen( val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } HomeScreenBackground( - modifier = modifier.windowInsetsPadding(WindowInsets.navigationBars) + modifier = modifier.windowInsetsPadding(WindowInsets.navigationBars), ) { Scaffold( topBar = { @@ -457,7 +465,7 @@ private fun HomeScreen( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - containerColor = Color.Transparent + containerColor = Color.Transparent, ) { contentPadding -> // Main Content val snackBarText = stringResource(id = R.string.episode_added_to_your_queue) @@ -483,7 +491,7 @@ private fun HomeScreen( snackbarHostState.showSnackbar(snackBarText) } homeState.onQueueEpisode(it) - } + }, ) } } @@ -584,7 +592,7 @@ private fun HomeContentColumn( onQueueEpisode: (PlayerEpisode) -> Unit, ) { LazyColumn( - modifier = modifier.fillMaxSize() + modifier = modifier.fillMaxSize(), ) { if (featuredPodcasts.isNotEmpty()) { item { @@ -593,8 +601,9 @@ private fun HomeContentColumn( items = featuredPodcasts, onPodcastUnfollowed = onPodcastUnfollowed, navigateToPodcastDetails = navigateToPodcastDetails, - modifier = Modifier - .fillMaxWidth() + modifier = + Modifier + .fillMaxWidth(), ) } } @@ -605,7 +614,7 @@ private fun HomeContentColumn( categories = homeCategories, selectedCategory = selectedHomeCategory, showHorizontalLine = true, - onCategorySelected = onHomeCategorySelected + onCategorySelected = onHomeCategorySelected, ) } } @@ -615,7 +624,7 @@ private fun HomeContentColumn( libraryItems( library = library, navigateToPlayer = navigateToPlayer, - onQueueEpisode = onQueueEpisode + onQueueEpisode = onQueueEpisode, ) } @@ -627,7 +636,7 @@ private fun HomeContentColumn( navigateToPlayer = navigateToPlayer, onCategorySelected = onCategorySelected, onTogglePodcastFollowed = onTogglePodcastFollowed, - onQueueEpisode = onQueueEpisode + onQueueEpisode = onQueueEpisode, ) } } @@ -655,7 +664,7 @@ private fun HomeContentGrid( ) { LazyVerticalGrid( columns = GridCells.Adaptive(362.dp), - modifier = modifier.fillMaxSize() + modifier = modifier.fillMaxSize(), ) { if (featuredPodcasts.isNotEmpty()) { fullWidthItem { @@ -664,8 +673,9 @@ private fun HomeContentGrid( items = featuredPodcasts, onPodcastUnfollowed = onPodcastUnfollowed, navigateToPodcastDetails = navigateToPodcastDetails, - modifier = Modifier - .fillMaxWidth() + modifier = + Modifier + .fillMaxWidth(), ) } } @@ -678,7 +688,7 @@ private fun HomeContentGrid( selectedCategory = selectedHomeCategory, showHorizontalLine = false, onCategorySelected = onHomeCategorySelected, - modifier = Modifier.width(240.dp) + modifier = Modifier.width(240.dp), ) } } @@ -689,7 +699,7 @@ private fun HomeContentGrid( libraryItems( library = library, navigateToPlayer = navigateToPlayer, - onQueueEpisode = onQueueEpisode + onQueueEpisode = onQueueEpisode, ) } @@ -701,7 +711,7 @@ private fun HomeContentGrid( navigateToPlayer = navigateToPlayer, onCategorySelected = onCategorySelected, onTogglePodcastFollowed = onTogglePodcastFollowed, - onQueueEpisode = onQueueEpisode + onQueueEpisode = onQueueEpisode, ) } } @@ -724,7 +734,7 @@ private fun FollowedPodcastItem( items = items, onPodcastUnfollowed = onPodcastUnfollowed, navigateToPodcastDetails = navigateToPodcastDetails, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(16.dp)) @@ -746,7 +756,7 @@ private fun HomeCategoryTabs( val selectedIndex = categories.indexOfFirst { it == selectedCategory } val indicator = @Composable { tabPositions: List -> HomeCategoryTabIndicator( - Modifier.tabIndicatorOffset(tabPositions[selectedIndex]) + Modifier.tabIndicatorOffset(tabPositions[selectedIndex]), ) } @@ -759,7 +769,7 @@ private fun HomeCategoryTabs( if (showHorizontalLine) { HorizontalDivider() } - } + }, ) { categories.forEachIndexed { index, category -> Tab( @@ -767,13 +777,14 @@ private fun HomeCategoryTabs( onClick = { onCategorySelected(category) }, text = { Text( - text = when (category) { - HomeCategory.Library -> stringResource(R.string.home_library) - HomeCategory.Discover -> stringResource(R.string.home_discover) - }, - style = MaterialTheme.typography.bodyMedium + text = + when (category) { + HomeCategory.Library -> stringResource(R.string.home_library) + HomeCategory.Discover -> stringResource(R.string.home_discover) + }, + style = MaterialTheme.typography.bodyMedium, ) - } + }, ) } } @@ -782,13 +793,13 @@ private fun HomeCategoryTabs( @Composable private fun HomeCategoryTabIndicator( modifier: Modifier = Modifier, - color: Color = MaterialTheme.colorScheme.onSurface + color: Color = MaterialTheme.colorScheme.onSurface, ) { Spacer( modifier .padding(horizontal = 24.dp) .height(4.dp) - .background(color, RoundedCornerShape(topStartPercent = 100, topEndPercent = 100)) + .background(color, RoundedCornerShape(topStartPercent = 100, topEndPercent = 100)), ) } @@ -808,17 +819,18 @@ private fun FollowedPodcasts( // which solves this problem and avoids this calculation altogether. Once 1.7.0 is // stable, this implementation can be updated. BoxWithConstraints( - modifier = modifier.background(Color.Transparent) + modifier = modifier.background(Color.Transparent), ) { val horizontalPadding = (this.maxWidth - FEATURED_PODCAST_IMAGE_SIZE_DP) / 2 HorizontalPager( state = pagerState, - contentPadding = PaddingValues( - horizontal = horizontalPadding, - vertical = 16.dp, - ), + contentPadding = + PaddingValues( + horizontal = horizontalPadding, + vertical = 16.dp, + ), pageSpacing = 24.dp, - pageSize = PageSize.Fixed(FEATURED_PODCAST_IMAGE_SIZE_DP) + pageSize = PageSize.Fixed(FEATURED_PODCAST_IMAGE_SIZE_DP), ) { page -> val podcast = items[page] FollowedPodcastCarouselItem( @@ -826,11 +838,12 @@ private fun FollowedPodcasts( podcastTitle = podcast.title, onUnfollowedClick = { onPodcastUnfollowed(podcast) }, lastEpisodeDateText = podcast.lastEpisodeDate?.let { lastUpdated(it) }, - modifier = Modifier - .fillMaxSize() - .clickable { - navigateToPodcastDetails(podcast) - } + modifier = + Modifier + .fillMaxSize() + .clickable { + navigateToPodcastDetails(podcast) + }, ) } } @@ -848,20 +861,21 @@ private fun FollowedPodcastCarouselItem( Box( Modifier .size(FEATURED_PODCAST_IMAGE_SIZE_DP) - .align(Alignment.CenterHorizontally) + .align(Alignment.CenterHorizontally), ) { PodcastImage( podcastImageUrl = podcastImageUrl, contentDescription = podcastTitle, - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.medium), + modifier = + Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium), ) ToggleFollowPodcastIconButton( onClick = onUnfollowedClick, - isFollowed = true, /* All podcasts are followed in this feed */ - modifier = Modifier.align(Alignment.BottomEnd) + isFollowed = true, // All podcasts are followed in this feed + modifier = Modifier.align(Alignment.BottomEnd), ) } @@ -871,9 +885,10 @@ private fun FollowedPodcastCarouselItem( style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier - .padding(top = 8.dp) - .align(Alignment.CenterHorizontally) + modifier = + Modifier + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally), ) } } @@ -913,32 +928,35 @@ private val CompactWindowSizeClass = WindowSizeClass.compute(360f, 780f) @Composable private fun PreviewHome() { JetcasterTheme { - val homeState = HomeState( - windowSizeClass = CompactWindowSizeClass, - featuredPodcasts = PreviewPodcasts.toPersistentList(), - homeCategories = HomeCategory.entries, - selectedHomeCategory = HomeCategory.Discover, - filterableCategoriesModel = FilterableCategoriesModel( - categories = PreviewCategories, - selectedCategory = PreviewCategories.firstOrNull() - ), - podcastCategoryFilterResult = PodcastCategoryFilterResult( - topPodcasts = PreviewPodcasts, - episodes = PreviewPodcastEpisodes - ), - library = LibraryInfo(), - onCategorySelected = {}, - onPodcastUnfollowed = {}, - navigateToPodcastDetails = {}, - navigateToPlayer = {}, - onHomeCategorySelected = {}, - onTogglePodcastFollowed = {}, - onLibraryPodcastSelected = {}, - onQueueEpisode = {} - ) + val homeState = + HomeState( + windowSizeClass = CompactWindowSizeClass, + featuredPodcasts = PreviewPodcasts.toPersistentList(), + homeCategories = HomeCategory.entries, + selectedHomeCategory = HomeCategory.Discover, + filterableCategoriesModel = + FilterableCategoriesModel( + categories = PreviewCategories, + selectedCategory = PreviewCategories.firstOrNull(), + ), + podcastCategoryFilterResult = + PodcastCategoryFilterResult( + topPodcasts = PreviewPodcasts, + episodes = PreviewPodcastEpisodes, + ), + library = LibraryInfo(), + onCategorySelected = {}, + onPodcastUnfollowed = {}, + navigateToPodcastDetails = {}, + navigateToPlayer = {}, + onHomeCategorySelected = {}, + onTogglePodcastFollowed = {}, + onLibraryPodcastSelected = {}, + onQueueEpisode = {}, + ) HomeScreen( homeState = homeState, - showGrid = false + showGrid = false, ) } } @@ -951,7 +969,7 @@ private fun PreviewPodcastCard() { modifier = Modifier.size(128.dp), podcastTitle = "", podcastImageUrl = "", - onUnfollowedClick = {} + onUnfollowedClick = {}, ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt index 3d5450464d..57bb9f77b7 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt @@ -35,7 +35,6 @@ import com.example.jetcaster.core.model.asPodcastToEpisodeInfo import com.example.jetcaster.core.player.EpisodePlayer import com.example.jetcaster.core.player.model.PlayerEpisode import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -47,151 +46,164 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch +import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) - @HiltViewModel -class HomeViewModel @Inject constructor( - private val podcastsRepository: PodcastsRepository, - private val podcastStore: PodcastStore, - private val episodeStore: EpisodeStore, - private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase, - private val filterableCategoriesUseCase: FilterableCategoriesUseCase, - private val episodePlayer: EpisodePlayer, -) : ViewModel() { - // Holds our currently selected podcast in the library - private val selectedLibraryPodcast = MutableStateFlow(null) - // Holds our currently selected home category - private val selectedHomeCategory = MutableStateFlow(HomeCategory.Discover) - // Holds the currently available home categories - private val homeCategories = MutableStateFlow(HomeCategory.entries) - // Holds our currently selected category - private val _selectedCategory = MutableStateFlow(null) - // Holds our view state which the UI collects via [state] - private val _state = MutableStateFlow(HomeScreenUiState.Loading) - // Holds the view state if the UI is refreshing for new data - private val refreshing = MutableStateFlow(false) - - private val subscribedPodcasts = podcastStore.followedPodcastsSortedByLastEpisode(limit = 10) - .shareIn(viewModelScope, SharingStarted.WhileSubscribed()) - - val state: StateFlow - get() = _state - - init { - viewModelScope.launch { - // Combines the latest value from each of the flows, allowing us to generate a - // view state instance which only contains the latest values. - com.example.jetcaster.core.util.combine( - homeCategories, - selectedHomeCategory, - subscribedPodcasts, - refreshing, - _selectedCategory.flatMapLatest { selectedCategory -> - filterableCategoriesUseCase(selectedCategory) - }, - _selectedCategory.flatMapLatest { - podcastCategoryFilterUseCase(it) - }, - subscribedPodcasts.flatMapLatest { podcasts -> - episodeStore.episodesInPodcasts( - podcastUris = podcasts.map { it.podcast.uri }, - limit = 20 - ) - } - ) { homeCategories, - homeCategory, - podcasts, - refreshing, - filterableCategories, - podcastCategoryFilterResult, - libraryEpisodes -> - - if (refreshing) { - Log.d("Jetcaster", "refreshing: $refreshing, podcasts $podcasts") - return@combine HomeScreenUiState.Loading - } - - _selectedCategory.value = filterableCategories.selectedCategory - - // Override selected home category to show 'DISCOVER' if there are no - // featured podcasts - selectedHomeCategory.value = - if (podcasts.isEmpty()) HomeCategory.Discover else homeCategory - - HomeScreenUiState.Ready( - homeCategories = homeCategories, - selectedHomeCategory = homeCategory, - featuredPodcasts = podcasts.map { it.asExternalModel() }.toPersistentList(), - filterableCategoriesModel = filterableCategories, - podcastCategoryFilterResult = podcastCategoryFilterResult, - library = libraryEpisodes.asLibrary() - ) - }.catch { throwable -> - _state.value = HomeScreenUiState.Error(throwable.message) - }.collect { - _state.value = it +class HomeViewModel + @Inject + constructor( + private val podcastsRepository: PodcastsRepository, + private val podcastStore: PodcastStore, + private val episodeStore: EpisodeStore, + private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase, + private val filterableCategoriesUseCase: FilterableCategoriesUseCase, + private val episodePlayer: EpisodePlayer, + ) : ViewModel() { + // Holds our currently selected podcast in the library + private val selectedLibraryPodcast = MutableStateFlow(null) + + // Holds our currently selected home category + private val selectedHomeCategory = MutableStateFlow(HomeCategory.Discover) + + // Holds the currently available home categories + private val homeCategories = MutableStateFlow(HomeCategory.entries) + + // Holds our currently selected category + private val selectedCategory = MutableStateFlow(null) + + // Holds our view state which the UI collects via [state] + private val _state = MutableStateFlow(HomeScreenUiState.Loading) + + // Holds the view state if the UI is refreshing for new data + private val refreshing = MutableStateFlow(false) + + private val subscribedPodcasts = + podcastStore + .followedPodcastsSortedByLastEpisode(limit = 10) + .shareIn(viewModelScope, SharingStarted.WhileSubscribed()) + + val state: StateFlow + get() = _state + + init { + viewModelScope.launch { + // Combines the latest value from each of the flows, allowing us to generate a + // view state instance which only contains the latest values. + com.example.jetcaster.core.util + .combine( + homeCategories, + selectedHomeCategory, + subscribedPodcasts, + refreshing, + selectedCategory.flatMapLatest { selectedCategory -> + filterableCategoriesUseCase(selectedCategory) + }, + selectedCategory.flatMapLatest { + podcastCategoryFilterUseCase(it) + }, + subscribedPodcasts.flatMapLatest { podcasts -> + episodeStore.episodesInPodcasts( + podcastUris = podcasts.map { it.podcast.uri }, + limit = 20, + ) + }, + ) { + homeCategories, + homeCategory, + podcasts, + refreshing, + filterableCategories, + podcastCategoryFilterResult, + libraryEpisodes, + -> + + if (refreshing) { + Log.d("Jetcaster", "refreshing: $refreshing, podcasts $podcasts") + return@combine HomeScreenUiState.Loading + } + + selectedCategory.value = filterableCategories.selectedCategory + + // Override selected home category to show 'DISCOVER' if there are no + // featured podcasts + selectedHomeCategory.value = + if (podcasts.isEmpty()) HomeCategory.Discover else homeCategory + + HomeScreenUiState.Ready( + homeCategories = homeCategories, + selectedHomeCategory = homeCategory, + featuredPodcasts = podcasts.map { it.asExternalModel() }.toPersistentList(), + filterableCategoriesModel = filterableCategories, + podcastCategoryFilterResult = podcastCategoryFilterResult, + library = libraryEpisodes.asLibrary(), + ) + }.catch { throwable -> + _state.value = HomeScreenUiState.Error(throwable.message) + }.collect { + _state.value = it + } } + + refresh(force = false) } - refresh(force = false) - } + fun refresh(force: Boolean = true) { + viewModelScope.launch { + runCatching { + refreshing.value = true + podcastsRepository.updatePodcasts(force) + } + // TODO: look at result of runCatching and show any errors - fun refresh(force: Boolean = true) { - viewModelScope.launch { - runCatching { - refreshing.value = true - podcastsRepository.updatePodcasts(force) + refreshing.value = false } - // TODO: look at result of runCatching and show any errors - - refreshing.value = false } - } - fun onCategorySelected(category: CategoryInfo) { - _selectedCategory.value = category - } + fun onCategorySelected(category: CategoryInfo) { + selectedCategory.value = category + } - fun onHomeCategorySelected(category: HomeCategory) { - selectedHomeCategory.value = category - } + fun onHomeCategorySelected(category: HomeCategory) { + selectedHomeCategory.value = category + } - fun onPodcastUnfollowed(podcast: PodcastInfo) { - viewModelScope.launch { - podcastStore.unfollowPodcast(podcast.uri) + fun onPodcastUnfollowed(podcast: PodcastInfo) { + viewModelScope.launch { + podcastStore.unfollowPodcast(podcast.uri) + } } - } - fun onTogglePodcastFollowed(podcast: PodcastInfo) { - viewModelScope.launch { - podcastStore.togglePodcastFollowed(podcast.uri) + fun onTogglePodcastFollowed(podcast: PodcastInfo) { + viewModelScope.launch { + podcastStore.togglePodcastFollowed(podcast.uri) + } } - } - fun onLibraryPodcastSelected(podcast: PodcastInfo?) { - selectedLibraryPodcast.value = podcast - } + fun onLibraryPodcastSelected(podcast: PodcastInfo?) { + selectedLibraryPodcast.value = podcast + } - fun onQueueEpisode(episode: PlayerEpisode) { - episodePlayer.addToQueue(episode) + fun onQueueEpisode(episode: PlayerEpisode) { + episodePlayer.addToQueue(episode) + } } -} private fun List.asLibrary(): LibraryInfo = LibraryInfo( - episodes = this.map { it.asPodcastToEpisodeInfo() } + episodes = this.map { it.asPodcastToEpisodeInfo() }, ) enum class HomeCategory { - Library, Discover + Library, + Discover, } sealed interface HomeScreenUiState { data object Loading : HomeScreenUiState data class Error( - val errorMessage: String? = null + val errorMessage: String? = null, ) : HomeScreenUiState data class Ready( diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt index 0f6e021c82..b1b43ec775 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt @@ -65,7 +65,7 @@ fun LazyListScope.podcastCategory( CategoryPodcasts( topPodcasts = podcastCategoryFilterResult.topPodcasts, navigateToPodcastDetails = navigateToPodcastDetails, - onTogglePodcastFollowed = onTogglePodcastFollowed + onTogglePodcastFollowed = onTogglePodcastFollowed, ) } @@ -76,7 +76,7 @@ fun LazyListScope.podcastCategory( podcast = item.podcast, onClick = navigateToPlayer, onQueueEpisode = onQueueEpisode, - modifier = Modifier.fillParentMaxWidth() + modifier = Modifier.fillParentMaxWidth(), ) } } @@ -92,7 +92,7 @@ fun LazyGridScope.podcastCategory( CategoryPodcasts( topPodcasts = podcastCategoryFilterResult.topPodcasts, navigateToPodcastDetails = navigateToPodcastDetails, - onTogglePodcastFollowed = onTogglePodcastFollowed + onTogglePodcastFollowed = onTogglePodcastFollowed, ) } @@ -103,7 +103,7 @@ fun LazyGridScope.podcastCategory( podcast = item.podcast, onClick = navigateToPlayer, onQueueEpisode = onQueueEpisode, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } } @@ -112,13 +112,13 @@ fun LazyGridScope.podcastCategory( private fun CategoryPodcasts( topPodcasts: List, navigateToPodcastDetails: (PodcastInfo) -> Unit, - onTogglePodcastFollowed: (PodcastInfo) -> Unit + onTogglePodcastFollowed: (PodcastInfo) -> Unit, ) { CategoryPodcastRow( podcasts = topPodcasts, onTogglePodcastFollowed = onTogglePodcastFollowed, navigateToPodcastDetails = navigateToPodcastDetails, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } @@ -127,32 +127,34 @@ private fun CategoryPodcastRow( podcasts: List, onTogglePodcastFollowed: (PodcastInfo) -> Unit, navigateToPodcastDetails: (PodcastInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { LazyRow( modifier = modifier, - contentPadding = PaddingValues( - start = Keyline1, - top = 8.dp, - end = Keyline1, - bottom = 24.dp - ), - horizontalArrangement = Arrangement.spacedBy(24.dp) + contentPadding = + PaddingValues( + start = Keyline1, + top = 8.dp, + end = Keyline1, + bottom = 24.dp, + ), + horizontalArrangement = Arrangement.spacedBy(24.dp), ) { items( items = podcasts, - key = { it.uri } + key = { it.uri }, ) { podcast -> TopPodcastRowItem( podcastTitle = podcast.title, podcastImageUrl = podcast.imageUrl, isFollowed = podcast.isSubscribed ?: false, onToggleFollowClicked = { onTogglePodcastFollowed(podcast) }, - modifier = Modifier - .width(128.dp) - .clickable { - navigateToPodcastDetails(podcast) - } + modifier = + Modifier + .width(128.dp) + .clickable { + navigateToPodcastDetails(podcast) + }, ) } } @@ -167,26 +169,27 @@ private fun TopPodcastRowItem( onToggleFollowClicked: () -> Unit, ) { Column( - modifier.semantics(mergeDescendants = true) {} + modifier.semantics(mergeDescendants = true) {}, ) { Box( Modifier .fillMaxWidth() .aspectRatio(1f) - .align(Alignment.CenterHorizontally) + .align(Alignment.CenterHorizontally), ) { PodcastImage( - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.medium), + modifier = + Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium), podcastImageUrl = podcastImageUrl, - contentDescription = podcastTitle + contentDescription = podcastTitle, ) ToggleFollowPodcastIconButton( onClick = onToggleFollowClicked, isFollowed = isFollowed, - modifier = Modifier.align(Alignment.BottomEnd) + modifier = Modifier.align(Alignment.BottomEnd), ) } @@ -195,9 +198,10 @@ private fun TopPodcastRowItem( style = MaterialTheme.typography.bodyMedium, maxLines = 2, overflow = TextOverflow.Ellipsis, - modifier = Modifier - .padding(top = 8.dp) - .fillMaxWidth() + modifier = + Modifier + .padding(top = 8.dp) + .fillMaxWidth(), ) } } @@ -211,7 +215,7 @@ fun PreviewEpisodeListItem() { podcast = PreviewPodcasts[0], onClick = { }, onQueueEpisode = { }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt index adab7e487b..2e53c3bf82 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt @@ -71,7 +71,7 @@ fun LazyListScope.discoverItems( PodcastCategoryTabs( filterableCategoriesModel = filterableCategoriesModel, onCategorySelected = onCategorySelected, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(8.dp)) @@ -106,7 +106,7 @@ fun LazyGridScope.discoverItems( PodcastCategoryTabs( filterableCategoriesModel = filterableCategoriesModel, onCategorySelected = onCategorySelected, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(8.dp)) @@ -127,18 +127,19 @@ private val emptyTabIndicator: @Composable (List) -> Unit = {} private fun PodcastCategoryTabs( filterableCategoriesModel: FilterableCategoriesModel, onCategorySelected: (CategoryInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val selectedIndex = filterableCategoriesModel.categories.indexOf( - filterableCategoriesModel.selectedCategory - ) + val selectedIndex = + filterableCategoriesModel.categories.indexOf( + filterableCategoriesModel.selectedCategory, + ) ScrollableTabRow( selectedTabIndex = selectedIndex, containerColor = Color.Transparent, - divider = {}, /* Disable the built-in divider */ + divider = {}, // Disable the built-in divider edgePadding = Keyline1, indicator = emptyTabIndicator, - modifier = modifier + modifier = modifier, ) { filterableCategoriesModel.categories.forEachIndexed { index, category -> ChoiceChipContent( @@ -157,42 +158,47 @@ private fun ChoiceChipContent( text: String, selected: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { // When adding onClick to Surface, it automatically makes this item higher. // On the other hand, adding .clickable modifier, doesn't use the same shape as Surface. // This way we disable the minimum height requirement CompositionLocalProvider(value = LocalMinimumInteractiveComponentEnforcement provides false) { Surface( - color = when { - selected -> MaterialTheme.colorScheme.secondaryContainer - else -> MaterialTheme.colorScheme.surfaceContainer - }, - contentColor = when { - selected -> MaterialTheme.colorScheme.onSecondaryContainer - else -> MaterialTheme.colorScheme.onSurfaceVariant - }, + color = + when { + selected -> MaterialTheme.colorScheme.secondaryContainer + else -> MaterialTheme.colorScheme.surfaceContainer + }, + contentColor = + when { + selected -> MaterialTheme.colorScheme.onSecondaryContainer + else -> MaterialTheme.colorScheme.onSurfaceVariant + }, shape = MaterialTheme.shapes.medium, modifier = modifier, onClick = onClick, ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding( - horizontal = when { - selected -> 8.dp - else -> 16.dp - }, - vertical = 8.dp - ) + modifier = + Modifier.padding( + horizontal = + when { + selected -> 8.dp + else -> 16.dp + }, + vertical = 8.dp, + ), ) { if (selected) { Icon( imageVector = Icons.Default.Check, contentDescription = stringResource(id = R.string.cd_selected_category), - modifier = Modifier - .height(18.dp) - .padding(end = 8.dp) + modifier = + Modifier + .height(18.dp) + .padding(end = 8.dp), ) } Text( diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt index 425f964d17..c61f706d9c 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt @@ -38,22 +38,23 @@ import com.example.jetcaster.util.fullWidthItem fun LazyListScope.libraryItems( library: LibraryInfo, navigateToPlayer: (EpisodeInfo) -> Unit, - onQueueEpisode: (PlayerEpisode) -> Unit + onQueueEpisode: (PlayerEpisode) -> Unit, ) { item { Text( text = stringResource(id = R.string.latest_episodes), - modifier = Modifier.padding( - start = Keyline1, - top = 16.dp, - ), + modifier = + Modifier.padding( + start = Keyline1, + top = 16.dp, + ), style = MaterialTheme.typography.titleLarge, ) } items( library, - key = { it.episode.uri } + key = { it.episode.uri }, ) { item -> EpisodeListItem( episode = item.episode, @@ -68,29 +69,30 @@ fun LazyListScope.libraryItems( fun LazyGridScope.libraryItems( library: LibraryInfo, navigateToPlayer: (EpisodeInfo) -> Unit, - onQueueEpisode: (PlayerEpisode) -> Unit + onQueueEpisode: (PlayerEpisode) -> Unit, ) { fullWidthItem { Text( text = stringResource(id = R.string.latest_episodes), - modifier = Modifier.padding( - start = Keyline1, - top = 16.dp, - ), + modifier = + Modifier.padding( + start = Keyline1, + top = 16.dp, + ), style = MaterialTheme.typography.headlineLarge, ) } items( library, - key = { it.episode.uri } + key = { it.episode.uri }, ) { item -> EpisodeListItem( episode = item.episode, podcast = item.podcast, onClick = navigateToPlayer, onQueueEpisode = onQueueEpisode, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt index 6a82a232cc..93bb5014e2 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt @@ -107,8 +107,8 @@ import com.example.jetcaster.util.verticalGradientScrim import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy import com.google.accompanist.adaptive.TwoPane import com.google.accompanist.adaptive.VerticalTwoPaneStrategy -import java.time.Duration import kotlinx.coroutines.launch +import java.time.Duration /** * Stateful version of the Podcast player @@ -128,16 +128,17 @@ fun PlayerScreen( onBackPress = onBackPress, onAddToQueue = viewModel::onAddToQueue, onStop = viewModel::onStop, - playerControlActions = PlayerControlActions( - onPlayPress = viewModel::onPlay, - onPausePress = viewModel::onPause, - onAdvanceBy = viewModel::onAdvanceBy, - onRewindBy = viewModel::onRewindBy, - onSeekingStarted = viewModel::onSeekingStarted, - onSeekingFinished = viewModel::onSeekingFinished, - onNext = viewModel::onNext, - onPrevious = viewModel::onPrevious, - ), + playerControlActions = + PlayerControlActions( + onPlayPress = viewModel::onPlay, + onPausePress = viewModel::onPause, + onAdvanceBy = viewModel::onAdvanceBy, + onRewindBy = viewModel::onRewindBy, + onSeekingStarted = viewModel::onSeekingStarted, + onSeekingFinished = viewModel::onSeekingFinished, + onNext = viewModel::onNext, + onPrevious = viewModel::onPrevious, + ), ) } @@ -153,7 +154,7 @@ private fun PlayerScreen( onAddToQueue: () -> Unit, onStop: () -> Unit, playerControlActions: PlayerControlActions, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { DisposableEffect(Unit) { onDispose { @@ -168,7 +169,7 @@ private fun PlayerScreen( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - modifier = modifier + modifier = modifier, ) { contentPadding -> if (uiState.episodePlayerState.currentEpisode != null) { PlayerContentWithBackground( @@ -212,14 +213,15 @@ fun PlayerContentWithBackground( onAddToQueue: () -> Unit, playerControlActions: PlayerControlActions, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp) + contentPadding: PaddingValues = PaddingValues(0.dp), ) { Box(modifier = modifier, contentAlignment = Alignment.Center) { PlayerBackground( episode = uiState.episodePlayerState.currentEpisode, - modifier = Modifier - .fillMaxSize() - .padding(contentPadding) + modifier = + Modifier + .fillMaxSize() + .padding(contentPadding), ) PlayerContent( uiState = uiState, @@ -254,7 +256,7 @@ fun PlayerContent( onBackPress: () -> Unit, onAddToQueue: () -> Unit, playerControlActions: PlayerControlActions, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val foldingFeature = displayFeatures.filterIsInstance().firstOrNull() @@ -274,7 +276,7 @@ fun PlayerContent( ( isSeparatingPosture(foldingFeature) && foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL - ) + ) if (usingVerticalStrategy) { TwoPane( @@ -297,15 +299,15 @@ fun PlayerContent( ) } else { Column( - modifier = modifier - .fillMaxSize() - .verticalGradientScrim( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f), - startYPercentage = 1f, - endYPercentage = 0f - ) - .systemBarsPadding() - .padding(horizontal = 8.dp) + modifier = + modifier + .fillMaxSize() + .verticalGradientScrim( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f), + startYPercentage = 1f, + endYPercentage = 0f, + ).systemBarsPadding() + .padding(horizontal = 8.dp), ) { TopAppBar( onBackPress = onBackPress, @@ -322,7 +324,7 @@ fun PlayerContent( ) }, strategy = HorizontalTwoPaneStrategy(splitFraction = 0.5f), - displayFeatures = displayFeatures + displayFeatures = displayFeatures, ) } } @@ -346,20 +348,20 @@ private fun PlayerContentRegular( onBackPress: () -> Unit, onAddToQueue: () -> Unit, playerControlActions: PlayerControlActions, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val playerEpisode = uiState.episodePlayerState val currentEpisode = playerEpisode.currentEpisode ?: return Column( - modifier = modifier - .fillMaxSize() - .verticalGradientScrim( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f), - startYPercentage = 1f, - endYPercentage = 0f - ) - .systemBarsPadding() - .padding(horizontal = 8.dp) + modifier = + modifier + .fillMaxSize() + .verticalGradientScrim( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f), + startYPercentage = 1f, + endYPercentage = 0f, + ).systemBarsPadding() + .padding(horizontal = 8.dp), ) { TopAppBar( onBackPress = onBackPress, @@ -367,25 +369,25 @@ private fun PlayerContentRegular( ) Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 8.dp) + modifier = Modifier.padding(horizontal = 8.dp), ) { Spacer(modifier = Modifier.weight(1f)) PlayerImage( podcastImageUrl = currentEpisode.podcastImageUrl, - modifier = Modifier.weight(10f) + modifier = Modifier.weight(10f), ) Spacer(modifier = Modifier.height(32.dp)) PodcastDescription(currentEpisode.title, currentEpisode.podcastName) Spacer(modifier = Modifier.height(32.dp)) Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.weight(10f) + modifier = Modifier.weight(10f), ) { PlayerSlider( timeElapsed = playerEpisode.timeElapsed, episodeDuration = currentEpisode.duration, onSeekingStarted = playerControlActions.onSeekingStarted, - onSeekingFinished = playerControlActions.onSeekingFinished + onSeekingFinished = playerControlActions.onSeekingFinished, ) PlayerButtons( hasNext = playerEpisode.queue.isNotEmpty(), @@ -396,7 +398,7 @@ private fun PlayerContentRegular( onRewindBy = playerControlActions.onRewindBy, onNext = playerControlActions.onNext, onPrevious = playerControlActions.onPrevious, - Modifier.padding(vertical = 8.dp) + Modifier.padding(vertical = 8.dp), ) } Spacer(modifier = Modifier.weight(1f)) @@ -410,25 +412,24 @@ private fun PlayerContentRegular( @Composable private fun PlayerContentTableTopTop( uiState: PlayerUiState, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { // Content for the top part of the screen val episode = uiState.episodePlayerState.currentEpisode ?: return Column( - modifier = modifier - .fillMaxWidth() - .verticalGradientScrim( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f), - startYPercentage = 1f, - endYPercentage = 0f - ) - .windowInsetsPadding( - WindowInsets.systemBars.only( - WindowInsetsSides.Horizontal + WindowInsetsSides.Top - ) - ) - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + modifier + .fillMaxWidth() + .verticalGradientScrim( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f), + startYPercentage = 1f, + endYPercentage = 0f, + ).windowInsetsPadding( + WindowInsets.systemBars.only( + WindowInsetsSides.Horizontal + WindowInsetsSides.Top, + ), + ).padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { PlayerImage(episode.podcastImageUrl) } @@ -443,20 +444,20 @@ private fun PlayerContentTableTopBottom( onBackPress: () -> Unit, onAddToQueue: () -> Unit, playerControlActions: PlayerControlActions, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val episodePlayerState = uiState.episodePlayerState val episode = uiState.episodePlayerState.currentEpisode ?: return // Content for the table part of the screen Column( - modifier = modifier - .windowInsetsPadding( - WindowInsets.systemBars.only( - WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom - ) - ) - .padding(horizontal = 32.dp, vertical = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + modifier + .windowInsetsPadding( + WindowInsets.systemBars.only( + WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom, + ), + ).padding(horizontal = 32.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { TopAppBar( onBackPress = onBackPress, @@ -465,12 +466,12 @@ private fun PlayerContentTableTopBottom( PodcastDescription( title = episode.title, podcastName = episode.podcastName, - titleTextStyle = MaterialTheme.typography.titleLarge + titleTextStyle = MaterialTheme.typography.titleLarge, ) Spacer(modifier = Modifier.weight(0.5f)) Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.weight(10f) + modifier = Modifier.weight(10f), ) { PlayerButtons( hasNext = episodePlayerState.queue.isNotEmpty(), @@ -482,13 +483,13 @@ private fun PlayerContentTableTopBottom( onRewindBy = playerControlActions.onRewindBy, onNext = playerControlActions.onNext, onPrevious = playerControlActions.onPrevious, - modifier = Modifier.padding(top = 8.dp) + modifier = Modifier.padding(top = 8.dp), ) PlayerSlider( timeElapsed = episodePlayerState.timeElapsed, episodeDuration = episode.duration, onSeekingStarted = playerControlActions.onSeekingStarted, - onSeekingFinished = playerControlActions.onSeekingFinished + onSeekingFinished = playerControlActions.onSeekingFinished, ) } } @@ -500,17 +501,18 @@ private fun PlayerContentTableTopBottom( @Composable private fun PlayerContentBookStart( uiState: PlayerUiState, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val episode = uiState.episodePlayerState.currentEpisode ?: return Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding( - vertical = 40.dp, - horizontal = 16.dp - ), + modifier = + modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding( + vertical = 40.dp, + horizontal = 16.dp, + ), horizontalAlignment = Alignment.CenterHorizontally, ) { PodcastInformation( @@ -528,22 +530,24 @@ private fun PlayerContentBookStart( private fun PlayerContentBookEnd( uiState: PlayerUiState, playerControlActions: PlayerControlActions, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val episodePlayerState = uiState.episodePlayerState val episode = episodePlayerState.currentEpisode ?: return Column( - modifier = modifier - .fillMaxSize() - .padding(8.dp), + modifier = + modifier + .fillMaxSize() + .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceAround, ) { PlayerImage( podcastImageUrl = episode.podcastImageUrl, - modifier = Modifier - .padding(vertical = 16.dp) - .weight(1f) + modifier = + Modifier + .padding(vertical = 16.dp) + .weight(1f), ) PlayerSlider( timeElapsed = episodePlayerState.timeElapsed, @@ -560,7 +564,7 @@ private fun PlayerContentBookEnd( onRewindBy = playerControlActions.onRewindBy, onNext = playerControlActions.onNext, onPrevious = playerControlActions.onPrevious, - Modifier.padding(vertical = 8.dp) + Modifier.padding(vertical = 8.dp), ) } } @@ -574,20 +578,20 @@ private fun TopAppBar( IconButton(onClick = onBackPress) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.cd_back) + contentDescription = stringResource(R.string.cd_back), ) } Spacer(Modifier.weight(1f)) IconButton(onClick = onAddToQueue) { Icon( imageVector = Icons.AutoMirrored.Filled.PlaylistAdd, - contentDescription = stringResource(R.string.cd_add) + contentDescription = stringResource(R.string.cd_add), ) } IconButton(onClick = { /* TODO */ }) { Icon( imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.cd_more) + contentDescription = stringResource(R.string.cd_more), ) } } @@ -596,16 +600,17 @@ private fun TopAppBar( @Composable private fun PlayerImage( podcastImageUrl: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { PodcastImage( podcastImageUrl = podcastImageUrl, contentDescription = null, contentScale = ContentScale.Crop, - modifier = modifier - .sizeIn(maxWidth = 500.dp, maxHeight = 500.dp) - .aspectRatio(1f) - .clip(MaterialTheme.shapes.medium) + modifier = + modifier + .sizeIn(maxWidth = 500.dp, maxHeight = 500.dp) + .aspectRatio(1f) + .clip(MaterialTheme.shapes.medium), ) } @@ -614,20 +619,20 @@ private fun PlayerImage( private fun PodcastDescription( title: String, podcastName: String, - titleTextStyle: TextStyle = MaterialTheme.typography.headlineSmall + titleTextStyle: TextStyle = MaterialTheme.typography.headlineSmall, ) { Text( text = title, style = titleTextStyle, maxLines = 1, color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.basicMarquee() + modifier = Modifier.basicMarquee(), ) Text( text = podcastName, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, - maxLines = 1 + maxLines = 1, ) } @@ -649,19 +654,19 @@ private fun PodcastInformation( text = name, style = nameTextStyle, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) Text( text = title, style = titleTextStyle, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) HtmlTextContainer(text = summary) { Text( text = it, style = MaterialTheme.typography.bodyMedium, - color = LocalContentColor.current + color = LocalContentColor.current, ) } } @@ -683,7 +688,7 @@ private fun PlayerSlider( Column( Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .padding(horizontal = 16.dp), ) { var sliderValue by remember(timeElapsed) { mutableStateOf(timeElapsed) } val maxRange = (episodeDuration?.toSeconds() ?: 0).toFloat() @@ -692,7 +697,7 @@ private fun PlayerSlider( Text( text = "${sliderValue.formatString()} • ${episodeDuration?.formatString()}", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -703,7 +708,7 @@ private fun PlayerSlider( onSeekingStarted() sliderValue = Duration.ofSeconds(it.toLong()) }, - onValueChangeFinished = { onSeekingFinished(sliderValue) } + onValueChangeFinished = { onSeekingFinished(sliderValue) }, ) } } @@ -725,42 +730,44 @@ private fun PlayerButtons( Row( modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly + horizontalArrangement = Arrangement.SpaceEvenly, ) { - val sideButtonsModifier = Modifier - .size(sideButtonSize) - .background( - color = MaterialTheme.colorScheme.surfaceContainerHighest, - shape = CircleShape - ) - .semantics { role = Role.Button } + val sideButtonsModifier = + Modifier + .size(sideButtonSize) + .background( + color = MaterialTheme.colorScheme.surfaceContainerHighest, + shape = CircleShape, + ).semantics { role = Role.Button } - val primaryButtonModifier = Modifier - .size(playerButtonSize) - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = CircleShape - ) - .semantics { role = Role.Button } + val primaryButtonModifier = + Modifier + .size(playerButtonSize) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + shape = CircleShape, + ).semantics { role = Role.Button } Image( imageVector = Icons.Filled.SkipPrevious, contentDescription = stringResource(R.string.cd_skip_previous), contentScale = ContentScale.Inside, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), - modifier = sideButtonsModifier - .clickable(enabled = isPlaying, onClick = onPrevious) - .alpha(if (isPlaying) 1f else 0.25f) + modifier = + sideButtonsModifier + .clickable(enabled = isPlaying, onClick = onPrevious) + .alpha(if (isPlaying) 1f else 0.25f), ) Image( imageVector = Icons.Filled.Replay10, contentDescription = stringResource(R.string.cd_replay10), contentScale = ContentScale.Inside, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), - modifier = sideButtonsModifier - .clickable { - onRewindBy(Duration.ofSeconds(10)) - } + modifier = + sideButtonsModifier + .clickable { + onRewindBy(Duration.ofSeconds(10)) + }, ) if (isPlaying) { Image( @@ -768,11 +775,12 @@ private fun PlayerButtons( contentDescription = stringResource(R.string.cd_pause), contentScale = ContentScale.Fit, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer), - modifier = primaryButtonModifier - .padding(8.dp) - .clickable { - onPausePress() - } + modifier = + primaryButtonModifier + .padding(8.dp) + .clickable { + onPausePress() + }, ) } else { Image( @@ -780,11 +788,12 @@ private fun PlayerButtons( contentDescription = stringResource(R.string.cd_play), contentScale = ContentScale.Fit, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer), - modifier = primaryButtonModifier - .padding(8.dp) - .clickable { - onPlayPress() - } + modifier = + primaryButtonModifier + .padding(8.dp) + .clickable { + onPlayPress() + }, ) } Image( @@ -792,19 +801,21 @@ private fun PlayerButtons( contentDescription = stringResource(R.string.cd_forward10), contentScale = ContentScale.Inside, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), - modifier = sideButtonsModifier - .clickable { - onAdvanceBy(Duration.ofSeconds(10)) - } + modifier = + sideButtonsModifier + .clickable { + onAdvanceBy(Duration.ofSeconds(10)) + }, ) Image( imageVector = Icons.Filled.SkipNext, contentDescription = stringResource(R.string.cd_skip_next), contentScale = ContentScale.Inside, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), - modifier = sideButtonsModifier - .clickable(enabled = hasNext, onClick = onNext) - .alpha(if (hasNext) 1f else 0.25f) + modifier = + sideButtonsModifier + .clickable(enabled = hasNext, onClick = onNext) + .alpha(if (hasNext) 1f else 0.25f), ) } } @@ -815,9 +826,10 @@ private fun PlayerButtons( @Composable private fun FullScreenLoading(modifier: Modifier = Modifier) { Box( - modifier = modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center) + modifier = + modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center), ) { CircularProgressIndicator() } @@ -858,35 +870,39 @@ fun PlayerScreenPreview() { BoxWithConstraints { PlayerScreen( PlayerUiState( - episodePlayerState = EpisodePlayerState( - currentEpisode = PlayerEpisode( - title = "Title", - duration = Duration.ofHours(2), - podcastName = "Podcast", + episodePlayerState = + EpisodePlayerState( + currentEpisode = + PlayerEpisode( + title = "Title", + duration = Duration.ofHours(2), + podcastName = "Podcast", + ), + isPlaying = false, + queue = + listOf( + PlayerEpisode(), + PlayerEpisode(), + PlayerEpisode(), + ), ), - isPlaying = false, - queue = listOf( - PlayerEpisode(), - PlayerEpisode(), - PlayerEpisode(), - ) - ), ), displayFeatures = emptyList(), windowSizeClass = WindowSizeClass.compute(maxWidth.value, maxHeight.value), onBackPress = { }, onAddToQueue = {}, onStop = {}, - playerControlActions = PlayerControlActions( - onPlayPress = {}, - onPausePress = {}, - onAdvanceBy = {}, - onRewindBy = {}, - onSeekingStarted = {}, - onSeekingFinished = {}, - onNext = {}, - onPrevious = {}, - ) + playerControlActions = + PlayerControlActions( + onPlayPress = {}, + onPausePress = {}, + onAdvanceBy = {}, + onRewindBy = {}, + onSeekingStarted = {}, + onSeekingFinished = {}, + onNext = {}, + onPrevious = {}, + ), ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt index a264db77cb..97fe0c281f 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt @@ -29,15 +29,15 @@ import com.example.jetcaster.core.player.EpisodePlayerState import com.example.jetcaster.core.player.model.toPlayerEpisode import com.example.jetcaster.ui.Screen import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.Duration -import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import java.time.Duration +import javax.inject.Inject data class PlayerUiState( - val episodePlayerState: EpisodePlayerState = EpisodePlayerState() + val episodePlayerState: EpisodePlayerState = EpisodePlayerState(), ) /** @@ -45,72 +45,75 @@ data class PlayerUiState( */ @OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel -class PlayerViewModel @Inject constructor( - episodeStore: EpisodeStore, - private val episodePlayer: EpisodePlayer, - savedStateHandle: SavedStateHandle -) : ViewModel() { - - // episodeUri should always be present in the PlayerViewModel. - // If that's not the case, fail crashing the app! - private val episodeUri: String = - Uri.decode(savedStateHandle.get(Screen.ARG_EPISODE_URI)!!) - - var uiState by mutableStateOf(PlayerUiState()) - private set - - init { - viewModelScope.launch { - episodeStore.episodeAndPodcastWithUri(episodeUri).flatMapConcat { - episodePlayer.currentEpisode = it.toPlayerEpisode() - episodePlayer.playerState - }.map { - PlayerUiState(episodePlayerState = it) - }.collect { - uiState = it +class PlayerViewModel + @Inject + constructor( + episodeStore: EpisodeStore, + private val episodePlayer: EpisodePlayer, + savedStateHandle: SavedStateHandle, + ) : ViewModel() { + // episodeUri should always be present in the PlayerViewModel. + // If that's not the case, fail crashing the app! + private val episodeUri: String = + Uri.decode(savedStateHandle.get(Screen.ARG_EPISODE_URI)!!) + + var uiState by mutableStateOf(PlayerUiState()) + private set + + init { + viewModelScope.launch { + episodeStore + .episodeAndPodcastWithUri(episodeUri) + .flatMapConcat { + episodePlayer.currentEpisode = it.toPlayerEpisode() + episodePlayer.playerState + }.map { + PlayerUiState(episodePlayerState = it) + }.collect { + uiState = it + } } } - } - fun onPlay() { - episodePlayer.play() - } + fun onPlay() { + episodePlayer.play() + } - fun onPause() { - episodePlayer.pause() - } + fun onPause() { + episodePlayer.pause() + } - fun onStop() { - episodePlayer.stop() - } + fun onStop() { + episodePlayer.stop() + } - fun onPrevious() { - episodePlayer.previous() - } + fun onPrevious() { + episodePlayer.previous() + } - fun onNext() { - episodePlayer.next() - } + fun onNext() { + episodePlayer.next() + } - fun onAdvanceBy(duration: Duration) { - episodePlayer.advanceBy(duration) - } + fun onAdvanceBy(duration: Duration) { + episodePlayer.advanceBy(duration) + } - fun onRewindBy(duration: Duration) { - episodePlayer.rewindBy(duration) - } + fun onRewindBy(duration: Duration) { + episodePlayer.rewindBy(duration) + } - fun onSeekingStarted() { - episodePlayer.onSeekingStarted() - } + fun onSeekingStarted() { + episodePlayer.onSeekingStarted() + } - fun onSeekingFinished(duration: Duration) { - episodePlayer.onSeekingFinished(duration) - } + fun onSeekingFinished(duration: Duration) { + episodePlayer.onSeekingFinished(duration) + } - fun onAddToQueue() { - uiState.episodePlayerState.currentEpisode?.let { - episodePlayer.addToQueue(it) + fun onAddToQueue() { + uiState.episodePlayerState.currentEpisode?.let { + episodePlayer.addToQueue(it) + } } } -} diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt index 4d96997193..255774a1ec 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt @@ -87,13 +87,13 @@ fun PodcastDetailsScreen( navigateToPlayer: (EpisodeInfo) -> Unit, navigateBack: () -> Unit, showBackButton: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val state by viewModel.state.collectAsStateWithLifecycle() when (val s = state) { is PodcastUiState.Loading -> { PodcastDetailsLoadingScreen( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } is PodcastUiState.Ready -> { @@ -112,9 +112,7 @@ fun PodcastDetailsScreen( } @Composable -private fun PodcastDetailsLoadingScreen( - modifier: Modifier = Modifier -) { +private fun PodcastDetailsLoadingScreen(modifier: Modifier = Modifier) { Loading(modifier = modifier) } @@ -127,7 +125,7 @@ fun PodcastDetailsScreen( navigateToPlayer: (EpisodeInfo) -> Unit, navigateBack: () -> Unit, showBackButton: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } @@ -138,13 +136,13 @@ fun PodcastDetailsScreen( if (showBackButton) { PodcastDetailsTopAppBar( navigateBack = navigateBack, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) - } + }, ) { contentPadding -> PodcastDetailsContent( podcast = podcast, @@ -157,7 +155,7 @@ fun PodcastDetailsScreen( onQueueEpisode(it) }, navigateToPlayer = navigateToPlayer, - modifier = Modifier.padding(contentPadding) + modifier = Modifier.padding(contentPadding), ) } } @@ -169,17 +167,17 @@ fun PodcastDetailsContent( toggleSubscribe: (PodcastInfo) -> Unit, onQueueEpisode: (PlayerEpisode) -> Unit, navigateToPlayer: (EpisodeInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { LazyVerticalGrid( columns = GridCells.Adaptive(362.dp), - modifier.fillMaxSize() + modifier.fillMaxSize(), ) { fullWidthItem { PodcastDetailsHeaderItem( podcast = podcast, toggleSubscribe = toggleSubscribe, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } items(episodes, key = { it.uri }) { episode -> @@ -190,7 +188,7 @@ fun PodcastDetailsContent( onQueueEpisode = onQueueEpisode, modifier = Modifier.fillMaxWidth(), showPodcastImage = false, - showSummary = true + showSummary = true, ) } } @@ -200,48 +198,50 @@ fun PodcastDetailsContent( fun PodcastDetailsHeaderItem( podcast: PodcastInfo, toggleSubscribe: (PodcastInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { BoxWithConstraints( - modifier = modifier.padding(Keyline1) + modifier = modifier.padding(Keyline1), ) { val maxImageSize = this.maxWidth / 2 val imageSize = min(maxImageSize, 148.dp) Column { Row( verticalAlignment = Alignment.Bottom, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) { PodcastImage( - modifier = Modifier - .size(imageSize) - .clip(MaterialTheme.shapes.large), + modifier = + Modifier + .size(imageSize) + .clip(MaterialTheme.shapes.large), podcastImageUrl = podcast.imageUrl, - contentDescription = podcast.title + contentDescription = podcast.title, ) Column( - modifier = Modifier.padding(start = 16.dp) + modifier = Modifier.padding(start = 16.dp), ) { Text( text = podcast.title, maxLines = 2, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.headlineMedium + style = MaterialTheme.typography.headlineMedium, ) PodcastDetailsHeaderItemButtons( isSubscribed = podcast.isSubscribed ?: false, onClick = { toggleSubscribe(podcast) }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } } PodcastDetailsDescription( podcast = podcast, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), ) } } @@ -250,12 +250,12 @@ fun PodcastDetailsHeaderItem( @Composable fun PodcastDetailsDescription( podcast: PodcastInfo, - modifier: Modifier + modifier: Modifier, ) { var isExpanded by remember { mutableStateOf(false) } var showSeeMore by remember { mutableStateOf(false) } Box( - modifier = modifier.clickable { isExpanded = !isExpanded } + modifier = modifier.clickable { isExpanded = !isExpanded }, ) { Text( text = podcast.description, @@ -265,27 +265,31 @@ fun PodcastDetailsDescription( onTextLayout = { result -> showSeeMore = result.hasVisualOverflow }, - modifier = Modifier.animateContentSize( - animationSpec = tween( - durationMillis = 200, - easing = EaseOutExpo - ) - ) + modifier = + Modifier.animateContentSize( + animationSpec = + tween( + durationMillis = 200, + easing = EaseOutExpo, + ), + ), ) if (showSeeMore) { Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .background(MaterialTheme.colorScheme.surface) + modifier = + Modifier + .align(Alignment.BottomEnd) + .background(MaterialTheme.colorScheme.surface), ) { // TODO: Add gradient effect Text( text = stringResource(id = R.string.see_more), - style = MaterialTheme.typography.bodyMedium.copy( - textDecoration = TextDecoration.Underline, - fontWeight = FontWeight.Bold - ), - modifier = Modifier.padding(start = 16.dp) + style = + MaterialTheme.typography.bodyMedium.copy( + textDecoration = TextDecoration.Underline, + fontWeight = FontWeight.Bold, + ), + modifier = Modifier.padding(start = 16.dp), ) } } @@ -296,32 +300,39 @@ fun PodcastDetailsDescription( fun PodcastDetailsHeaderItemButtons( isSubscribed: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row(modifier.padding(top = 16.dp)) { Button( onClick = onClick, - colors = ButtonDefaults.buttonColors( - containerColor = if (isSubscribed) - MaterialTheme.colorScheme.tertiary - else - MaterialTheme.colorScheme.secondary - ), - modifier = Modifier.semantics(mergeDescendants = true) { } + colors = + ButtonDefaults.buttonColors( + containerColor = + if (isSubscribed) { + MaterialTheme.colorScheme.tertiary + } else { + MaterialTheme.colorScheme.secondary + }, + ), + modifier = Modifier.semantics(mergeDescendants = true) { }, ) { Icon( - imageVector = if (isSubscribed) - Icons.Default.Check - else - Icons.Default.Add, - contentDescription = null + imageVector = + if (isSubscribed) { + Icons.Default.Check + } else { + Icons.Default.Add + }, + contentDescription = null, ) Text( - text = if (isSubscribed) - stringResource(id = R.string.subscribed) - else - stringResource(id = R.string.subscribe), - modifier = Modifier.padding(start = 8.dp) + text = + if (isSubscribed) { + stringResource(id = R.string.subscribed) + } else { + stringResource(id = R.string.subscribe) + }, + modifier = Modifier.padding(start = 8.dp), ) } @@ -329,11 +340,11 @@ fun PodcastDetailsHeaderItemButtons( IconButton( onClick = { /* TODO */ }, - modifier = Modifier.padding(start = 8.dp) + modifier = Modifier.padding(start = 8.dp), ) { Icon( imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.cd_more) + contentDescription = stringResource(R.string.cd_more), ) } } @@ -343,7 +354,7 @@ fun PodcastDetailsHeaderItemButtons( @Composable fun PodcastDetailsTopAppBar( navigateBack: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { TopAppBar( title = { }, @@ -351,11 +362,11 @@ fun PodcastDetailsTopAppBar( IconButton(onClick = navigateBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(id = R.string.cd_back) + contentDescription = stringResource(id = R.string.cd_back), ) } }, - modifier = modifier + modifier = modifier, ) } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt index ac3c6a4267..10c19022ec 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.launch sealed interface PodcastUiState { data object Loading : PodcastUiState + data class Ready( val podcast: PodcastInfo, val episodes: List, @@ -48,43 +49,44 @@ sealed interface PodcastUiState { * ViewModel that handles the business logic and screen state of the Podcast details screen. */ @HiltViewModel(assistedFactory = PodcastDetailsViewModel.Factory::class) -class PodcastDetailsViewModel @AssistedInject constructor( - private val episodeStore: EpisodeStore, - private val episodePlayer: EpisodePlayer, - private val podcastStore: PodcastStore, - @Assisted private val podcastUri: String, -) : ViewModel() { - - private val decodedPodcastUri = Uri.decode(podcastUri) +class PodcastDetailsViewModel + @AssistedInject + constructor( + private val episodeStore: EpisodeStore, + private val episodePlayer: EpisodePlayer, + private val podcastStore: PodcastStore, + @Assisted private val podcastUri: String, + ) : ViewModel() { + private val decodedPodcastUri = Uri.decode(podcastUri) - val state: StateFlow = - combine( - podcastStore.podcastWithExtraInfo(decodedPodcastUri), - episodeStore.episodesInPodcast(decodedPodcastUri) - ) { podcast, episodeToPodcasts -> - val episodes = episodeToPodcasts.map { it.episode.asExternalModel() } - PodcastUiState.Ready( - podcast = podcast.podcast.asExternalModel().copy(isSubscribed = podcast.isFollowed), - episodes = episodes, + val state: StateFlow = + combine( + podcastStore.podcastWithExtraInfo(decodedPodcastUri), + episodeStore.episodesInPodcast(decodedPodcastUri), + ) { podcast, episodeToPodcasts -> + val episodes = episodeToPodcasts.map { it.episode.asExternalModel() } + PodcastUiState.Ready( + podcast = podcast.podcast.asExternalModel().copy(isSubscribed = podcast.isFollowed), + episodes = episodes, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = PodcastUiState.Loading, ) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = PodcastUiState.Loading - ) - fun toggleSusbcribe(podcast: PodcastInfo) { - viewModelScope.launch { - podcastStore.togglePodcastFollowed(podcast.uri) + fun toggleSusbcribe(podcast: PodcastInfo) { + viewModelScope.launch { + podcastStore.togglePodcastFollowed(podcast.uri) + } } - } - fun onQueueEpisode(playerEpisode: PlayerEpisode) { - episodePlayer.addToQueue(playerEpisode) - } + fun onQueueEpisode(playerEpisode: PlayerEpisode) { + episodePlayer.addToQueue(playerEpisode) + } - @AssistedFactory - interface Factory { - fun create(podcastUri: String): PodcastDetailsViewModel + @AssistedFactory + interface Factory { + fun create(podcastUri: String): PodcastDetailsViewModel + } } -} diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/EpisodeListItem.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/EpisodeListItem.kt index dd5e409704..8b52221b56 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/EpisodeListItem.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/EpisodeListItem.kt @@ -75,10 +75,10 @@ fun EpisodeListItem( Surface( shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surfaceContainer, - onClick = { onClick(episode) } + onClick = { onClick(episode) }, ) { Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) { // Top Part EpisodeListItemHeader( @@ -86,7 +86,7 @@ fun EpisodeListItem( podcast = podcast, showPodcastImage = showPodcastImage, showSummary = showSummary, - modifier = Modifier.padding(bottom = 8.dp) + modifier = Modifier.padding(bottom = 8.dp), ) // Bottom Part @@ -105,48 +105,51 @@ private fun EpisodeListItemFooter( episode: EpisodeInfo, podcast: PodcastInfo, onQueueEpisode: (PlayerEpisode) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = modifier + modifier = modifier, ) { Image( imageVector = Icons.Rounded.PlayCircleFilled, contentDescription = stringResource(R.string.cd_play), contentScale = ContentScale.Fit, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), - modifier = Modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = 24.dp) - ) { /* TODO */ } - .size(48.dp) - .padding(6.dp) - .semantics { role = Role.Button } + modifier = + Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = 24.dp), + ) { /* TODO */ } + .size(48.dp) + .padding(6.dp) + .semantics { role = Role.Button }, ) val duration = episode.duration Text( - text = when { - duration != null -> { - // If we have the duration, we combine the date/duration via a - // formatted string - stringResource( - R.string.episode_date_duration, - MediumDateFormatter.format(episode.published), - duration.toMinutes().toInt() - ) - } - // Otherwise we just use the date - else -> MediumDateFormatter.format(episode.published) - }, + text = + when { + duration != null -> { + // If we have the duration, we combine the date/duration via a + // formatted string + stringResource( + R.string.episode_date_duration, + MediumDateFormatter.format(episode.published), + duration.toMinutes().toInt(), + ) + } + // Otherwise we just use the date + else -> MediumDateFormatter.format(episode.published) + }, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall, - modifier = Modifier - .padding(horizontal = 8.dp) - .weight(1f) + modifier = + Modifier + .padding(horizontal = 8.dp) + .weight(1f), ) IconButton( @@ -154,15 +157,15 @@ private fun EpisodeListItemFooter( onQueueEpisode( PlayerEpisode( podcastInfo = podcast, - episodeInfo = episode - ) + episodeInfo = episode, + ), ) }, ) { Icon( imageVector = Icons.AutoMirrored.Filled.PlaylistAdd, contentDescription = stringResource(R.string.cd_add), - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -172,7 +175,7 @@ private fun EpisodeListItemFooter( Icon( imageVector = Icons.Default.MoreVert, contentDescription = stringResource(R.string.cd_more), - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -184,14 +187,14 @@ private fun EpisodeListItemHeader( podcast: PodcastInfo, showPodcastImage: Boolean, showSummary: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row(modifier = modifier) { Column( modifier = - Modifier - .weight(1f) - .padding(end = 16.dp) + Modifier + .weight(1f) + .padding(end = 16.dp), ) { Text( text = episode.title, @@ -199,7 +202,7 @@ private fun EpisodeListItemHeader( minLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(vertical = 2.dp) + modifier = Modifier.padding(vertical = 2.dp), ) if (showSummary) { @@ -225,9 +228,10 @@ private fun EpisodeListItemHeader( if (showPodcastImage) { EpisodeListItemImage( podcast = podcast, - modifier = Modifier - .size(56.dp) - .clip(MaterialTheme.shapes.medium) + modifier = + Modifier + .size(56.dp) + .clip(MaterialTheme.shapes.medium), ) } } @@ -236,7 +240,7 @@ private fun EpisodeListItemHeader( @Composable private fun EpisodeListItemImage( podcast: PodcastInfo, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { PodcastImage( podcastImageUrl = podcast.imageUrl, @@ -248,12 +252,12 @@ private fun EpisodeListItemImage( @Preview( name = "Light Mode", showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO + uiMode = Configuration.UI_MODE_NIGHT_NO, ) @Preview( name = "Dark Mode", showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_YES + uiMode = Configuration.UI_MODE_NIGHT_YES, ) @Composable private fun EpisodeListItemPreview() { @@ -263,7 +267,7 @@ private fun EpisodeListItemPreview() { podcast = PreviewPodcasts[0], onClick = {}, onQueueEpisode = {}, - showSummary = true + showSummary = true, ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/Loading.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/Loading.kt index 4b96dc6e8a..4b81f235d9 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/Loading.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/Loading.kt @@ -28,10 +28,10 @@ import androidx.compose.ui.Modifier fun Loading(modifier: Modifier = Modifier) { Surface(modifier = modifier) { Box( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { CircularProgressIndicator( - Modifier.align(Alignment.Center) + Modifier.align(Alignment.Center), ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/theme/Theme.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/theme/Theme.kt index 46fabe361a..9f56e5d890 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/theme/Theme.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/theme/Theme.kt @@ -245,262 +245,273 @@ import com.example.jetcaster.designsystem.theme.tertiaryLight import com.example.jetcaster.designsystem.theme.tertiaryLightHighContrast import com.example.jetcaster.designsystem.theme.tertiaryLightMediumContrast -private val lightScheme = lightColorScheme( - primary = primaryLight, - onPrimary = onPrimaryLight, - primaryContainer = primaryContainerLight, - onPrimaryContainer = onPrimaryContainerLight, - secondary = secondaryLight, - onSecondary = onSecondaryLight, - secondaryContainer = secondaryContainerLight, - onSecondaryContainer = onSecondaryContainerLight, - tertiary = tertiaryLight, - onTertiary = onTertiaryLight, - tertiaryContainer = tertiaryContainerLight, - onTertiaryContainer = onTertiaryContainerLight, - error = errorLight, - onError = onErrorLight, - errorContainer = errorContainerLight, - onErrorContainer = onErrorContainerLight, - background = backgroundLight, - onBackground = onBackgroundLight, - surface = surfaceLight, - onSurface = onSurfaceLight, - surfaceVariant = surfaceVariantLight, - onSurfaceVariant = onSurfaceVariantLight, - outline = outlineLight, - outlineVariant = outlineVariantLight, - scrim = scrimLight, - inverseSurface = inverseSurfaceLight, - inverseOnSurface = inverseOnSurfaceLight, - inversePrimary = inversePrimaryLight, - surfaceDim = surfaceDimLight, - surfaceBright = surfaceBrightLight, - surfaceContainerLowest = surfaceContainerLowestLight, - surfaceContainerLow = surfaceContainerLowLight, - surfaceContainer = surfaceContainerLight, - surfaceContainerHigh = surfaceContainerHighLight, - surfaceContainerHighest = surfaceContainerHighestLight, -) +private val lightScheme = + lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, + ) -private val darkScheme = darkColorScheme( - primary = primaryDark, - onPrimary = onPrimaryDark, - primaryContainer = primaryContainerDark, - onPrimaryContainer = onPrimaryContainerDark, - secondary = secondaryDark, - onSecondary = onSecondaryDark, - secondaryContainer = secondaryContainerDark, - onSecondaryContainer = onSecondaryContainerDark, - tertiary = tertiaryDark, - onTertiary = onTertiaryDark, - tertiaryContainer = tertiaryContainerDark, - onTertiaryContainer = onTertiaryContainerDark, - error = errorDark, - onError = onErrorDark, - errorContainer = errorContainerDark, - onErrorContainer = onErrorContainerDark, - background = backgroundDark, - onBackground = onBackgroundDark, - surface = surfaceDark, - onSurface = onSurfaceDark, - surfaceVariant = surfaceVariantDark, - onSurfaceVariant = onSurfaceVariantDark, - outline = outlineDark, - outlineVariant = outlineVariantDark, - scrim = scrimDark, - inverseSurface = inverseSurfaceDark, - inverseOnSurface = inverseOnSurfaceDark, - inversePrimary = inversePrimaryDark, - surfaceDim = surfaceDimDark, - surfaceBright = surfaceBrightDark, - surfaceContainerLowest = surfaceContainerLowestDark, - surfaceContainerLow = surfaceContainerLowDark, - surfaceContainer = surfaceContainerDark, - surfaceContainerHigh = surfaceContainerHighDark, - surfaceContainerHighest = surfaceContainerHighestDark, -) +private val darkScheme = + darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, + ) -private val mediumContrastLightColorScheme = lightColorScheme( - primary = primaryLightMediumContrast, - onPrimary = onPrimaryLightMediumContrast, - primaryContainer = primaryContainerLightMediumContrast, - onPrimaryContainer = onPrimaryContainerLightMediumContrast, - secondary = secondaryLightMediumContrast, - onSecondary = onSecondaryLightMediumContrast, - secondaryContainer = secondaryContainerLightMediumContrast, - onSecondaryContainer = onSecondaryContainerLightMediumContrast, - tertiary = tertiaryLightMediumContrast, - onTertiary = onTertiaryLightMediumContrast, - tertiaryContainer = tertiaryContainerLightMediumContrast, - onTertiaryContainer = onTertiaryContainerLightMediumContrast, - error = errorLightMediumContrast, - onError = onErrorLightMediumContrast, - errorContainer = errorContainerLightMediumContrast, - onErrorContainer = onErrorContainerLightMediumContrast, - background = backgroundLightMediumContrast, - onBackground = onBackgroundLightMediumContrast, - surface = surfaceLightMediumContrast, - onSurface = onSurfaceLightMediumContrast, - surfaceVariant = surfaceVariantLightMediumContrast, - onSurfaceVariant = onSurfaceVariantLightMediumContrast, - outline = outlineLightMediumContrast, - outlineVariant = outlineVariantLightMediumContrast, - scrim = scrimLightMediumContrast, - inverseSurface = inverseSurfaceLightMediumContrast, - inverseOnSurface = inverseOnSurfaceLightMediumContrast, - inversePrimary = inversePrimaryLightMediumContrast, - surfaceDim = surfaceDimLightMediumContrast, - surfaceBright = surfaceBrightLightMediumContrast, - surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, - surfaceContainerLow = surfaceContainerLowLightMediumContrast, - surfaceContainer = surfaceContainerLightMediumContrast, - surfaceContainerHigh = surfaceContainerHighLightMediumContrast, - surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, -) +private val mediumContrastLightColorScheme = + lightColorScheme( + primary = primaryLightMediumContrast, + onPrimary = onPrimaryLightMediumContrast, + primaryContainer = primaryContainerLightMediumContrast, + onPrimaryContainer = onPrimaryContainerLightMediumContrast, + secondary = secondaryLightMediumContrast, + onSecondary = onSecondaryLightMediumContrast, + secondaryContainer = secondaryContainerLightMediumContrast, + onSecondaryContainer = onSecondaryContainerLightMediumContrast, + tertiary = tertiaryLightMediumContrast, + onTertiary = onTertiaryLightMediumContrast, + tertiaryContainer = tertiaryContainerLightMediumContrast, + onTertiaryContainer = onTertiaryContainerLightMediumContrast, + error = errorLightMediumContrast, + onError = onErrorLightMediumContrast, + errorContainer = errorContainerLightMediumContrast, + onErrorContainer = onErrorContainerLightMediumContrast, + background = backgroundLightMediumContrast, + onBackground = onBackgroundLightMediumContrast, + surface = surfaceLightMediumContrast, + onSurface = onSurfaceLightMediumContrast, + surfaceVariant = surfaceVariantLightMediumContrast, + onSurfaceVariant = onSurfaceVariantLightMediumContrast, + outline = outlineLightMediumContrast, + outlineVariant = outlineVariantLightMediumContrast, + scrim = scrimLightMediumContrast, + inverseSurface = inverseSurfaceLightMediumContrast, + inverseOnSurface = inverseOnSurfaceLightMediumContrast, + inversePrimary = inversePrimaryLightMediumContrast, + surfaceDim = surfaceDimLightMediumContrast, + surfaceBright = surfaceBrightLightMediumContrast, + surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, + surfaceContainerLow = surfaceContainerLowLightMediumContrast, + surfaceContainer = surfaceContainerLightMediumContrast, + surfaceContainerHigh = surfaceContainerHighLightMediumContrast, + surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, + ) -private val highContrastLightColorScheme = lightColorScheme( - primary = primaryLightHighContrast, - onPrimary = onPrimaryLightHighContrast, - primaryContainer = primaryContainerLightHighContrast, - onPrimaryContainer = onPrimaryContainerLightHighContrast, - secondary = secondaryLightHighContrast, - onSecondary = onSecondaryLightHighContrast, - secondaryContainer = secondaryContainerLightHighContrast, - onSecondaryContainer = onSecondaryContainerLightHighContrast, - tertiary = tertiaryLightHighContrast, - onTertiary = onTertiaryLightHighContrast, - tertiaryContainer = tertiaryContainerLightHighContrast, - onTertiaryContainer = onTertiaryContainerLightHighContrast, - error = errorLightHighContrast, - onError = onErrorLightHighContrast, - errorContainer = errorContainerLightHighContrast, - onErrorContainer = onErrorContainerLightHighContrast, - background = backgroundLightHighContrast, - onBackground = onBackgroundLightHighContrast, - surface = surfaceLightHighContrast, - onSurface = onSurfaceLightHighContrast, - surfaceVariant = surfaceVariantLightHighContrast, - onSurfaceVariant = onSurfaceVariantLightHighContrast, - outline = outlineLightHighContrast, - outlineVariant = outlineVariantLightHighContrast, - scrim = scrimLightHighContrast, - inverseSurface = inverseSurfaceLightHighContrast, - inverseOnSurface = inverseOnSurfaceLightHighContrast, - inversePrimary = inversePrimaryLightHighContrast, - surfaceDim = surfaceDimLightHighContrast, - surfaceBright = surfaceBrightLightHighContrast, - surfaceContainerLowest = surfaceContainerLowestLightHighContrast, - surfaceContainerLow = surfaceContainerLowLightHighContrast, - surfaceContainer = surfaceContainerLightHighContrast, - surfaceContainerHigh = surfaceContainerHighLightHighContrast, - surfaceContainerHighest = surfaceContainerHighestLightHighContrast, -) +private val highContrastLightColorScheme = + lightColorScheme( + primary = primaryLightHighContrast, + onPrimary = onPrimaryLightHighContrast, + primaryContainer = primaryContainerLightHighContrast, + onPrimaryContainer = onPrimaryContainerLightHighContrast, + secondary = secondaryLightHighContrast, + onSecondary = onSecondaryLightHighContrast, + secondaryContainer = secondaryContainerLightHighContrast, + onSecondaryContainer = onSecondaryContainerLightHighContrast, + tertiary = tertiaryLightHighContrast, + onTertiary = onTertiaryLightHighContrast, + tertiaryContainer = tertiaryContainerLightHighContrast, + onTertiaryContainer = onTertiaryContainerLightHighContrast, + error = errorLightHighContrast, + onError = onErrorLightHighContrast, + errorContainer = errorContainerLightHighContrast, + onErrorContainer = onErrorContainerLightHighContrast, + background = backgroundLightHighContrast, + onBackground = onBackgroundLightHighContrast, + surface = surfaceLightHighContrast, + onSurface = onSurfaceLightHighContrast, + surfaceVariant = surfaceVariantLightHighContrast, + onSurfaceVariant = onSurfaceVariantLightHighContrast, + outline = outlineLightHighContrast, + outlineVariant = outlineVariantLightHighContrast, + scrim = scrimLightHighContrast, + inverseSurface = inverseSurfaceLightHighContrast, + inverseOnSurface = inverseOnSurfaceLightHighContrast, + inversePrimary = inversePrimaryLightHighContrast, + surfaceDim = surfaceDimLightHighContrast, + surfaceBright = surfaceBrightLightHighContrast, + surfaceContainerLowest = surfaceContainerLowestLightHighContrast, + surfaceContainerLow = surfaceContainerLowLightHighContrast, + surfaceContainer = surfaceContainerLightHighContrast, + surfaceContainerHigh = surfaceContainerHighLightHighContrast, + surfaceContainerHighest = surfaceContainerHighestLightHighContrast, + ) -private val mediumContrastDarkColorScheme = darkColorScheme( - primary = primaryDarkMediumContrast, - onPrimary = onPrimaryDarkMediumContrast, - primaryContainer = primaryContainerDarkMediumContrast, - onPrimaryContainer = onPrimaryContainerDarkMediumContrast, - secondary = secondaryDarkMediumContrast, - onSecondary = onSecondaryDarkMediumContrast, - secondaryContainer = secondaryContainerDarkMediumContrast, - onSecondaryContainer = onSecondaryContainerDarkMediumContrast, - tertiary = tertiaryDarkMediumContrast, - onTertiary = onTertiaryDarkMediumContrast, - tertiaryContainer = tertiaryContainerDarkMediumContrast, - onTertiaryContainer = onTertiaryContainerDarkMediumContrast, - error = errorDarkMediumContrast, - onError = onErrorDarkMediumContrast, - errorContainer = errorContainerDarkMediumContrast, - onErrorContainer = onErrorContainerDarkMediumContrast, - background = backgroundDarkMediumContrast, - onBackground = onBackgroundDarkMediumContrast, - surface = surfaceDarkMediumContrast, - onSurface = onSurfaceDarkMediumContrast, - surfaceVariant = surfaceVariantDarkMediumContrast, - onSurfaceVariant = onSurfaceVariantDarkMediumContrast, - outline = outlineDarkMediumContrast, - outlineVariant = outlineVariantDarkMediumContrast, - scrim = scrimDarkMediumContrast, - inverseSurface = inverseSurfaceDarkMediumContrast, - inverseOnSurface = inverseOnSurfaceDarkMediumContrast, - inversePrimary = inversePrimaryDarkMediumContrast, - surfaceDim = surfaceDimDarkMediumContrast, - surfaceBright = surfaceBrightDarkMediumContrast, - surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, - surfaceContainerLow = surfaceContainerLowDarkMediumContrast, - surfaceContainer = surfaceContainerDarkMediumContrast, - surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, - surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, -) +private val mediumContrastDarkColorScheme = + darkColorScheme( + primary = primaryDarkMediumContrast, + onPrimary = onPrimaryDarkMediumContrast, + primaryContainer = primaryContainerDarkMediumContrast, + onPrimaryContainer = onPrimaryContainerDarkMediumContrast, + secondary = secondaryDarkMediumContrast, + onSecondary = onSecondaryDarkMediumContrast, + secondaryContainer = secondaryContainerDarkMediumContrast, + onSecondaryContainer = onSecondaryContainerDarkMediumContrast, + tertiary = tertiaryDarkMediumContrast, + onTertiary = onTertiaryDarkMediumContrast, + tertiaryContainer = tertiaryContainerDarkMediumContrast, + onTertiaryContainer = onTertiaryContainerDarkMediumContrast, + error = errorDarkMediumContrast, + onError = onErrorDarkMediumContrast, + errorContainer = errorContainerDarkMediumContrast, + onErrorContainer = onErrorContainerDarkMediumContrast, + background = backgroundDarkMediumContrast, + onBackground = onBackgroundDarkMediumContrast, + surface = surfaceDarkMediumContrast, + onSurface = onSurfaceDarkMediumContrast, + surfaceVariant = surfaceVariantDarkMediumContrast, + onSurfaceVariant = onSurfaceVariantDarkMediumContrast, + outline = outlineDarkMediumContrast, + outlineVariant = outlineVariantDarkMediumContrast, + scrim = scrimDarkMediumContrast, + inverseSurface = inverseSurfaceDarkMediumContrast, + inverseOnSurface = inverseOnSurfaceDarkMediumContrast, + inversePrimary = inversePrimaryDarkMediumContrast, + surfaceDim = surfaceDimDarkMediumContrast, + surfaceBright = surfaceBrightDarkMediumContrast, + surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, + surfaceContainerLow = surfaceContainerLowDarkMediumContrast, + surfaceContainer = surfaceContainerDarkMediumContrast, + surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, + surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, + ) -private val highContrastDarkColorScheme = darkColorScheme( - primary = primaryDarkHighContrast, - onPrimary = onPrimaryDarkHighContrast, - primaryContainer = primaryContainerDarkHighContrast, - onPrimaryContainer = onPrimaryContainerDarkHighContrast, - secondary = secondaryDarkHighContrast, - onSecondary = onSecondaryDarkHighContrast, - secondaryContainer = secondaryContainerDarkHighContrast, - onSecondaryContainer = onSecondaryContainerDarkHighContrast, - tertiary = tertiaryDarkHighContrast, - onTertiary = onTertiaryDarkHighContrast, - tertiaryContainer = tertiaryContainerDarkHighContrast, - onTertiaryContainer = onTertiaryContainerDarkHighContrast, - error = errorDarkHighContrast, - onError = onErrorDarkHighContrast, - errorContainer = errorContainerDarkHighContrast, - onErrorContainer = onErrorContainerDarkHighContrast, - background = backgroundDarkHighContrast, - onBackground = onBackgroundDarkHighContrast, - surface = surfaceDarkHighContrast, - onSurface = onSurfaceDarkHighContrast, - surfaceVariant = surfaceVariantDarkHighContrast, - onSurfaceVariant = onSurfaceVariantDarkHighContrast, - outline = outlineDarkHighContrast, - outlineVariant = outlineVariantDarkHighContrast, - scrim = scrimDarkHighContrast, - inverseSurface = inverseSurfaceDarkHighContrast, - inverseOnSurface = inverseOnSurfaceDarkHighContrast, - inversePrimary = inversePrimaryDarkHighContrast, - surfaceDim = surfaceDimDarkHighContrast, - surfaceBright = surfaceBrightDarkHighContrast, - surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, - surfaceContainerLow = surfaceContainerLowDarkHighContrast, - surfaceContainer = surfaceContainerDarkHighContrast, - surfaceContainerHigh = surfaceContainerHighDarkHighContrast, - surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, -) +private val highContrastDarkColorScheme = + darkColorScheme( + primary = primaryDarkHighContrast, + onPrimary = onPrimaryDarkHighContrast, + primaryContainer = primaryContainerDarkHighContrast, + onPrimaryContainer = onPrimaryContainerDarkHighContrast, + secondary = secondaryDarkHighContrast, + onSecondary = onSecondaryDarkHighContrast, + secondaryContainer = secondaryContainerDarkHighContrast, + onSecondaryContainer = onSecondaryContainerDarkHighContrast, + tertiary = tertiaryDarkHighContrast, + onTertiary = onTertiaryDarkHighContrast, + tertiaryContainer = tertiaryContainerDarkHighContrast, + onTertiaryContainer = onTertiaryContainerDarkHighContrast, + error = errorDarkHighContrast, + onError = onErrorDarkHighContrast, + errorContainer = errorContainerDarkHighContrast, + onErrorContainer = onErrorContainerDarkHighContrast, + background = backgroundDarkHighContrast, + onBackground = onBackgroundDarkHighContrast, + surface = surfaceDarkHighContrast, + onSurface = onSurfaceDarkHighContrast, + surfaceVariant = surfaceVariantDarkHighContrast, + onSurfaceVariant = onSurfaceVariantDarkHighContrast, + outline = outlineDarkHighContrast, + outlineVariant = outlineVariantDarkHighContrast, + scrim = scrimDarkHighContrast, + inverseSurface = inverseSurfaceDarkHighContrast, + inverseOnSurface = inverseOnSurfaceDarkHighContrast, + inversePrimary = inversePrimaryDarkHighContrast, + surfaceDim = surfaceDimDarkHighContrast, + surfaceBright = surfaceBrightDarkHighContrast, + surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, + surfaceContainerLow = surfaceContainerLowDarkHighContrast, + surfaceContainer = surfaceContainerDarkHighContrast, + surfaceContainerHigh = surfaceContainerHighDarkHighContrast, + surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, + ) @Immutable data class ColorFamily( val color: Color, val onColor: Color, val colorContainer: Color, - val onColorContainer: Color + val onColorContainer: Color, ) -val unspecified_scheme = ColorFamily( - Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified -) +val unspecified_scheme = + ColorFamily( + Color.Unspecified, + Color.Unspecified, + Color.Unspecified, + Color.Unspecified, + ) @Composable fun JetcasterTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = false, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } + val colorScheme = + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } - darkTheme -> darkScheme - else -> lightScheme - } + darkTheme -> darkScheme + else -> lightScheme + } val view = LocalView.current if (!view.isInEditMode) { SideEffect { @@ -514,6 +525,6 @@ fun JetcasterTheme( colorScheme = colorScheme, shapes = JetcasterShapes, typography = JetcasterTypography, - content = content + content = content, ) } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/Buttons.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/Buttons.kt index c90ffc9d82..dcd0999f2e 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/Buttons.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/Buttons.kt @@ -40,46 +40,50 @@ import com.example.jetcaster.R fun ToggleFollowPodcastIconButton( isFollowed: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val clickLabel = stringResource(if (isFollowed) R.string.cd_unfollow else R.string.cd_follow) IconButton( onClick = onClick, - modifier = modifier.semantics { - onClick(label = clickLabel, action = null) - } + modifier = + modifier.semantics { + onClick(label = clickLabel, action = null) + }, ) { Icon( // TODO: think about animating these icons - imageVector = when { - isFollowed -> Icons.Default.Check - else -> Icons.Default.Add - }, - contentDescription = when { - isFollowed -> stringResource(R.string.cd_following) - else -> stringResource(R.string.cd_not_following) - }, - tint = animateColorAsState( + imageVector = + when { + isFollowed -> Icons.Default.Check + else -> Icons.Default.Add + }, + contentDescription = when { - isFollowed -> MaterialTheme.colorScheme.onPrimary - else -> MaterialTheme.colorScheme.primary - } - ).value, - modifier = Modifier - .shadow( - elevation = animateDpAsState(if (isFollowed) 0.dp else 1.dp).value, - shape = MaterialTheme.shapes.small - ) - .background( - color = animateColorAsState( - when { - isFollowed -> MaterialTheme.colorScheme.primary - else -> MaterialTheme.colorScheme.surfaceContainerHighest - } - ).value, - shape = CircleShape - ) - .padding(4.dp) + isFollowed -> stringResource(R.string.cd_following) + else -> stringResource(R.string.cd_not_following) + }, + tint = + animateColorAsState( + when { + isFollowed -> MaterialTheme.colorScheme.onPrimary + else -> MaterialTheme.colorScheme.primary + }, + ).value, + modifier = + Modifier + .shadow( + elevation = animateDpAsState(if (isFollowed) 0.dp else 1.dp).value, + shape = MaterialTheme.shapes.small, + ).background( + color = + animateColorAsState( + when { + isFollowed -> MaterialTheme.colorScheme.primary + else -> MaterialTheme.colorScheme.surfaceContainerHighest + }, + ).value, + shape = CircleShape, + ).padding(4.dp), ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/GradientScrim.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/GradientScrim.kt index 6713734728..4c0e0bd063 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/GradientScrim.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/GradientScrim.kt @@ -42,17 +42,18 @@ import kotlin.math.pow * center quarter of the element. */ fun Modifier.radialGradientScrim(color: Color): Modifier { - val radialGradient = object : ShaderBrush() { - override fun createShader(size: Size): Shader { - val largerDimension = max(size.height, size.width) - return RadialGradientShader( - center = size.center.copy(y = size.height / 4), - colors = listOf(color, Color.Transparent), - radius = largerDimension / 2, - colorStops = listOf(0f, 0.9f) - ) + val radialGradient = + object : ShaderBrush() { + override fun createShader(size: Size): Shader { + val largerDimension = max(size.height, size.width) + return RadialGradientShader( + center = size.center.copy(y = size.height / 4), + colors = listOf(color, Color.Transparent), + radius = largerDimension / 2, + colorStops = listOf(0f, 0.9f), + ) + } } - } return this.background(radialGradient) } @@ -74,7 +75,7 @@ fun Modifier.verticalGradientScrim( @FloatRange(from = 0.0, to = 1.0) startYPercentage: Float = 0f, @FloatRange(from = 0.0, to = 1.0) endYPercentage: Float = 1f, decay: Float = 1.0f, - numStops: Int = 16 + numStops: Int = 16, ) = this then VerticalGradientElement(color, startYPercentage, endYPercentage, decay, numStops) private data class VerticalGradientElement( @@ -82,22 +83,23 @@ private data class VerticalGradientElement( var startYPercentage: Float = 0f, var endYPercentage: Float = 1f, var decay: Float = 1.0f, - var numStops: Int = 16 + var numStops: Int = 16, ) : ModifierNodeElement() { fun createOnDraw(): DrawScope.() -> Unit { - val colors = if (decay != 1f) { - // If we have a non-linear decay, we need to create the color gradient steps - // manually - val baseAlpha = color.alpha - List(numStops) { i -> - val x = i * 1f / (numStops - 1) - val opacity = x.pow(decay) - color.copy(alpha = baseAlpha * opacity) + val colors = + if (decay != 1f) { + // If we have a non-linear decay, we need to create the color gradient steps + // manually + val baseAlpha = color.alpha + List(numStops) { i -> + val x = i * 1f / (numStops - 1) + val opacity = x.pow(decay) + color.copy(alpha = baseAlpha * opacity) + } + } else { + // If we have a linear decay, we just create a simple list of start + end colors + listOf(color.copy(alpha = 0f), color) } - } else { - // If we have a linear decay, we just create a simple list of start + end colors - listOf(color.copy(alpha = 0f), color) - } val brush = // Reverse the gradient if decaying downwards @@ -113,7 +115,7 @@ private data class VerticalGradientElement( drawRect( topLeft = topLeft, size = Rect(topLeft, bottomRight).size, - brush = brush + brush = brush, ) } } @@ -138,9 +140,9 @@ private data class VerticalGradientElement( } private class VerticalGradientModifier( - var onDraw: DrawScope.() -> Unit -) : Modifier.Node(), DrawModifierNode { - + var onDraw: DrawScope.() -> Unit, +) : Modifier.Node(), + DrawModifierNode { override fun ContentDrawScope.draw() { onDraw() drawContent() diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/LazyVerticalGrid.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/LazyVerticalGrid.kt index 6233653f67..0854155386 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/LazyVerticalGrid.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/LazyVerticalGrid.kt @@ -27,10 +27,10 @@ import androidx.compose.runtime.Composable fun LazyGridScope.fullWidthItem( key: Any? = null, contentType: Any? = null, - content: @Composable LazyGridItemScope.() -> Unit + content: @Composable LazyGridItemScope.() -> Unit, ) = item( span = { GridItemSpan(this.maxLineSpan) }, key = key, contentType = contentType, - content = content + content = content, ) diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/PluralResources.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/PluralResources.kt index 36c0b4ea4a..2f695d136d 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/PluralResources.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/PluralResources.kt @@ -28,7 +28,10 @@ import androidx.compose.ui.platform.LocalContext * @return the string data associated with the resource */ @Composable -fun quantityStringResource(@PluralsRes id: Int, quantity: Int): String { +fun quantityStringResource( + @PluralsRes id: Int, + quantity: Int, +): String { val context = LocalContext.current return context.resources.getQuantityString(id, quantity) } @@ -42,7 +45,11 @@ fun quantityStringResource(@PluralsRes id: Int, quantity: Int): String { * @return the string data associated with the resource */ @Composable -fun quantityStringResource(@PluralsRes id: Int, quantity: Int, vararg formatArgs: Any): String { +fun quantityStringResource( + @PluralsRes id: Int, + quantity: Int, + vararg formatArgs: Any, +): String { val context = LocalContext.current return context.resources.getQuantityString(id, quantity, *formatArgs) } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/ViewModel.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/ViewModel.kt index ff27946656..e862c3c9b3 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/ViewModel.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/ViewModel.kt @@ -28,10 +28,9 @@ import androidx.lifecycle.viewmodel.viewModelFactory * If the created [ViewModel] does not match the requested class, an [IllegalArgumentException] * exception is thrown. */ -inline fun viewModelProviderFactoryOf( - crossinline create: () -> VM -): ViewModelProvider.Factory = viewModelFactory { - initializer { - create() +inline fun viewModelProviderFactoryOf(crossinline create: () -> VM): ViewModelProvider.Factory = + viewModelFactory { + initializer { + create() + } } -} diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/WindowSizeClass.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/WindowSizeClass.kt index b4c90b3729..f9cf4ea700 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/WindowSizeClass.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/WindowSizeClass.kt @@ -24,5 +24,6 @@ import androidx.window.core.layout.WindowWidthSizeClass * Returns true if the width or height size classes are compact. */ val WindowSizeClass.isCompact: Boolean - get() = windowWidthSizeClass == WindowWidthSizeClass.COMPACT || - windowHeightSizeClass == WindowHeightSizeClass.COMPACT + get() = + windowWidthSizeClass == WindowWidthSizeClass.COMPACT || + windowHeightSizeClass == WindowHeightSizeClass.COMPACT diff --git a/Jetcaster/tv/build.gradle.kts b/Jetcaster/tv/build.gradle.kts index f654a9f0a4..cc02f949f1 100644 --- a/Jetcaster/tv/build.gradle.kts +++ b/Jetcaster/tv/build.gradle.kts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,6 +14,7 @@ * limitations under the License. */ + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -24,18 +25,26 @@ plugins { android { namespace = "com.example.jetcaster.tv" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { applicationId = "com.example.jetcaster" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() + targetSdk = + libs.versions.targetSdk + .get() + .toInt() versionCode = 1 versionName = "1.0" vectorDrawables { useSupportLibrary = true } - } signingConfigs { // Important: change the keystore for a production deployment @@ -57,7 +66,7 @@ android { signingConfig = signingConfigs.getByName("release") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/MainActivity.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/MainActivity.kt index 58c27a9423..a5f0f3b3a6 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/MainActivity.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/MainActivity.kt @@ -41,7 +41,7 @@ class MainActivity : ComponentActivity() { JetcasterTheme(isInDarkTheme = true) { Surface( modifier = Modifier.fillMaxSize(), - shape = RectangleShape + shape = RectangleShape, ) { JetcasterApp() } @@ -52,10 +52,13 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalTvMaterial3Api::class) @Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { +fun Greeting( + name: String, + modifier: Modifier = Modifier, +) { Text( text = "Hello $name!", - modifier = modifier + modifier = modifier, ) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategoryInfoList.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategoryInfoList.kt index 95b1d595b1..9e42c3f4af 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategoryInfoList.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategoryInfoList.kt @@ -22,11 +22,10 @@ import com.example.jetcaster.core.model.CategoryInfo import com.example.jetcaster.core.model.asExternalModel @Immutable -data class CategoryInfoList(val member: List) : List by member { - - fun intoCategoryList(): List { - return map(CategoryInfo::intoCategory) - } +data class CategoryInfoList( + val member: List, +) : List by member { + fun intoCategoryList(): List = map(CategoryInfo::intoCategory) companion object { fun from(list: List): CategoryInfoList { @@ -36,6 +35,4 @@ data class CategoryInfoList(val member: List) : List } } -private fun CategoryInfo.intoCategory(): Category { - return Category(id, name) -} +private fun CategoryInfo.intoCategory(): Category = Category(id, name) diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt index c5943815be..752b0e0276 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt @@ -19,9 +19,12 @@ package com.example.jetcaster.tv.model import androidx.compose.runtime.Immutable import com.example.jetcaster.core.model.CategoryInfo -data class CategorySelection(val categoryInfo: CategoryInfo, val isSelected: Boolean = false) +data class CategorySelection( + val categoryInfo: CategoryInfo, + val isSelected: Boolean = false, +) @Immutable data class CategorySelectionList( - val member: List + val member: List, ) : List by member diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/EpisodeList.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/EpisodeList.kt index 44f819252b..6cb5abb933 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/EpisodeList.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/EpisodeList.kt @@ -20,4 +20,6 @@ import androidx.compose.runtime.Immutable import com.example.jetcaster.core.player.model.PlayerEpisode @Immutable -data class EpisodeList(val member: List) : List by member +data class EpisodeList( + val member: List, +) : List by member diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/PodcastList.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/PodcastList.kt index 5ce9c5ace4..5ea246816a 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/PodcastList.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/PodcastList.kt @@ -21,5 +21,5 @@ import com.example.jetcaster.core.model.PodcastInfo @Immutable data class PodcastList( - val member: List + val member: List, ) : List by member diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt index eda8aad1a0..e916c58f91 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt @@ -64,7 +64,7 @@ fun JetcasterApp(jetcasterAppState: JetcasterAppState = rememberJetcasterAppStat private fun GlobalNavigationContainer( jetcasterAppState: JetcasterAppState, modifier: Modifier = Modifier, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val (discover, library) = remember { FocusRequester.createRefs() } val currentRoute @@ -74,18 +74,18 @@ private fun GlobalNavigationContainer( drawerContent = { val isClosed = it == DrawerValue.Closed Column( - modifier = Modifier - .padding(JetcasterAppDefaults.overScanMargin.drawer.intoPaddingValues()) - .onFocusChanged { focusState -> - if (focusState.isFocused) { - when (currentRoute) { - Screen.Discover.route -> discover - Screen.Library.route -> library - else -> FocusRequester.Default - }.requestFocus() - } - } - .focusable() + modifier = + Modifier + .padding(JetcasterAppDefaults.overScanMargin.drawer.intoPaddingValues()) + .onFocusChanged { focusState -> + if (focusState.isFocused) { + when (currentRoute) { + Screen.Discover.route -> discover + Screen.Library.route -> library + else -> FocusRequester.Default + }.requestFocus() + } + }.focusable(), ) { NavigationDrawerItem( selected = isClosed && currentRoute == Screen.Profile.route, @@ -101,7 +101,7 @@ private fun GlobalNavigationContainer( NavigationDrawerItem( selected = isClosed && currentRoute == Screen.Search.route, onClick = jetcasterAppState::navigateToSearch, - leadingContent = { Icon(Icons.Default.Search, contentDescription = null) } + leadingContent = { Icon(Icons.Default.Search, contentDescription = null) }, ) { Text(text = "Search") } @@ -109,7 +109,7 @@ private fun GlobalNavigationContainer( selected = isClosed && currentRoute == Screen.Discover.route, onClick = jetcasterAppState::navigateToDiscover, leadingContent = { Icon(Icons.Default.Home, contentDescription = null) }, - modifier = Modifier.focusRequester(discover) + modifier = Modifier.focusRequester(discover), ) { Text(text = "Discover") } @@ -119,10 +119,10 @@ private fun GlobalNavigationContainer( leadingContent = { Icon( Icons.Default.VideoLibrary, - contentDescription = null + contentDescription = null, ) }, - modifier = Modifier.focusRequester(library) + modifier = Modifier.focusRequester(library), ) { Text(text = "Library") } @@ -130,7 +130,7 @@ private fun GlobalNavigationContainer( NavigationDrawerItem( selected = isClosed && currentRoute == Screen.Settings.route, onClick = jetcasterAppState::navigateToSettings, - leadingContent = { Icon(Icons.Default.Settings, contentDescription = null) } + leadingContent = { Icon(Icons.Default.Settings, contentDescription = null) }, ) { Text(text = "Settings") } @@ -153,7 +153,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) { playEpisode = { jetcasterAppState.playEpisode() }, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } } @@ -168,7 +168,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) { playEpisode = { jetcasterAppState.playEpisode() }, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } } @@ -178,9 +178,10 @@ private fun Route(jetcasterAppState: JetcasterAppState) { onPodcastSelected = { jetcasterAppState.showPodcastDetails(it.uri) }, - modifier = Modifier - .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()) - .fillMaxSize() + modifier = + Modifier + .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()) + .fillMaxSize(), ) } @@ -191,9 +192,10 @@ private fun Route(jetcasterAppState: JetcasterAppState) { jetcasterAppState.playEpisode() }, showEpisodeDetails = { jetcasterAppState.showEpisodeDetails(it.uri) }, - modifier = Modifier - .padding(JetcasterAppDefaults.overScanMargin.podcast.intoPaddingValues()) - .fillMaxSize(), + modifier = + Modifier + .padding(JetcasterAppDefaults.overScanMargin.podcast.intoPaddingValues()) + .fillMaxSize(), ) } @@ -216,17 +218,19 @@ private fun Route(jetcasterAppState: JetcasterAppState) { composable(Screen.Profile.route) { ProfileScreen( - modifier = Modifier - .fillMaxSize() - .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()) + modifier = + Modifier + .fillMaxSize() + .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()), ) } composable(Screen.Settings.route) { SettingsScreen( - modifier = Modifier - .fillMaxSize() - .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()) + modifier = + Modifier + .fillMaxSize() + .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()), ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt index bc714c99a0..394e75a6f5 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt @@ -25,12 +25,12 @@ import com.example.jetcaster.core.player.model.PlayerEpisode import kotlinx.coroutines.flow.map class JetcasterAppState( - val navHostController: NavHostController + val navHostController: NavHostController, ) { - - val currentRouteFlow = navHostController.currentBackStackEntryFlow.map { - it.destination.route - } + val currentRouteFlow = + navHostController.currentBackStackEntryFlow.map { + it.destination.route + } private fun navigate(screen: Screen) { navHostController.navigate(screen.route) @@ -83,9 +83,7 @@ class JetcasterAppState( } @Composable -fun rememberJetcasterAppState( - navHostController: NavHostController = rememberNavController() -) = +fun rememberJetcasterAppState(navHostController: NavHostController = rememberNavController()) = remember(navHostController) { JetcasterAppState(navHostController) } @@ -113,7 +111,9 @@ sealed interface Screen { override val route: String = "settings" } - data class Podcast(private val podcastUri: String) : Screen { + data class Podcast( + private val podcastUri: String, + ) : Screen { override val route = "$ROOT/$podcastUri" companion object : Screen { @@ -123,8 +123,9 @@ sealed interface Screen { } } - data class Episode(private val episodeUri: String) : Screen { - + data class Episode( + private val episodeUri: String, + ) : Screen { override val route: String = "$ROOT/$episodeUri" companion object : Screen { diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt index 4cdd5ccb52..3aa6120155 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt @@ -32,30 +32,28 @@ internal fun BackgroundContainer( playerEpisode: PlayerEpisode, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, - content: @Composable BoxScope.() -> Unit -) = - BackgroundContainer( - imageUrl = playerEpisode.podcastImageUrl, - modifier, - contentAlignment, - content - ) + content: @Composable BoxScope.() -> Unit, +) = BackgroundContainer( + imageUrl = playerEpisode.podcastImageUrl, + modifier, + contentAlignment, + content, +) @Composable internal fun BackgroundContainer( podcastInfo: PodcastInfo, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, - content: @Composable BoxScope.() -> Unit -) = - BackgroundContainer(imageUrl = podcastInfo.imageUrl, modifier, contentAlignment, content) + content: @Composable BoxScope.() -> Unit, +) = BackgroundContainer(imageUrl = podcastInfo.imageUrl, modifier, contentAlignment, content) @Composable internal fun BackgroundContainer( imageUrl: String, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, - content: @Composable BoxScope.() -> Unit + content: @Composable BoxScope.() -> Unit, ) { Box(modifier = modifier, contentAlignment = contentAlignment) { Background(imageUrl = imageUrl, modifier = Modifier.fillMaxSize()) diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Button.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Button.kt index 3b215d422a..9a16f34c62 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Button.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Button.kt @@ -44,20 +44,19 @@ internal fun PlayButton( onClick: () -> Unit, modifier: Modifier = Modifier, scale: ButtonScale = ButtonDefaults.scale(), -) = - ButtonWithIcon( - icon = Icons.Outlined.PlayArrow, - label = stringResource(R.string.label_play), - onClick = onClick, - modifier = modifier, - scale = scale - ) +) = ButtonWithIcon( + icon = Icons.Outlined.PlayArrow, + label = stringResource(R.string.label_play), + onClick = onClick, + modifier = modifier, + scale = scale, +) @OptIn(ExperimentalTvMaterial3Api::class) @Composable internal fun EnqueueButton( onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { IconButton(onClick = onClick, modifier = modifier) { Icon( @@ -71,7 +70,7 @@ internal fun EnqueueButton( @Composable internal fun InfoButton( onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { IconButton(onClick = onClick, modifier = modifier) { Icon( @@ -85,12 +84,12 @@ internal fun InfoButton( @Composable internal fun PreviousButton( onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { IconButton(onClick = onClick, modifier = modifier) { Icon( Icons.Default.SkipPrevious, - contentDescription = stringResource(R.string.label_previous_episode) + contentDescription = stringResource(R.string.label_previous_episode), ) } } @@ -99,12 +98,12 @@ internal fun PreviousButton( @Composable internal fun NextButton( onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { IconButton(onClick = onClick, modifier = modifier) { Icon( Icons.Default.SkipNext, - contentDescription = stringResource(R.string.label_next_episode) + contentDescription = stringResource(R.string.label_next_episode), ) } } @@ -114,13 +113,14 @@ internal fun NextButton( internal fun PlayPauseButton( isPlaying: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val (icon, description) = if (isPlaying) { - Icons.Default.Pause to stringResource(R.string.label_pause) - } else { - Icons.Default.PlayArrow to stringResource(R.string.label_play) - } + val (icon, description) = + if (isPlaying) { + Icons.Default.Pause to stringResource(R.string.label_pause) + } else { + Icons.Default.PlayArrow to stringResource(R.string.label_play) + } IconButton(onClick = onClick, modifier = modifier) { Icon(icon, description, modifier = Modifier.size(48.dp)) } @@ -130,12 +130,12 @@ internal fun PlayPauseButton( @Composable internal fun RewindButton( onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { IconButton(onClick = onClick, modifier = modifier) { Icon( Icons.Default.Replay10, - contentDescription = stringResource(R.string.label_rewind) + contentDescription = stringResource(R.string.label_rewind), ) } } @@ -144,12 +144,12 @@ internal fun RewindButton( @Composable internal fun SkipButton( onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { IconButton(onClick = onClick, modifier = modifier) { Icon( Icons.Default.Forward10, - contentDescription = stringResource(R.string.label_skip) + contentDescription = stringResource(R.string.label_skip), ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt index 9a0bf0a506..2e951f54fd 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt @@ -62,7 +62,7 @@ internal fun Catalog( modifier = modifier, contentPadding = JetcasterAppDefaults.overScanMargin.catalog.intoPaddingValues(), verticalArrangement = - Arrangement.spacedBy(JetcasterAppDefaults.gap.section), + Arrangement.spacedBy(JetcasterAppDefaults.gap.section), state = state, ) { if (header != null) { @@ -72,14 +72,14 @@ internal fun Catalog( PodcastSection( podcastList = podcastList, onPodcastSelected = onPodcastSelected, - title = stringResource(R.string.label_podcast) + title = stringResource(R.string.label_podcast), ) } item { LatestEpisodeSection( episodeList = latestEpisodeList, onEpisodeSelected = onEpisodeSelected, - title = stringResource(R.string.label_latest_episode) + title = stringResource(R.string.label_latest_episode), ) } } @@ -94,7 +94,7 @@ private fun PodcastSection( ) { Section( title = title, - modifier = modifier + modifier = modifier, ) { PodcastRow( podcastList = podcastList, @@ -108,11 +108,11 @@ private fun LatestEpisodeSection( episodeList: EpisodeList, onEpisodeSelected: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - title: String? = null + title: String? = null, ) { Section( modifier = modifier, - title = title + title = title, ) { EpisodeRow( playerEpisodeList = episodeList, @@ -134,7 +134,7 @@ private fun Section( Text( text = title, style = style, - modifier = Modifier.padding(JetcasterAppDefaults.padding.sectionTitle) + modifier = Modifier.padding(JetcasterAppDefaults.padding.sectionTitle), ) } content() @@ -158,33 +158,35 @@ private fun PodcastRow( TvLazyRow( contentPadding = contentPadding, horizontalArrangement = horizontalArrangement, - modifier = modifier - .focusRequester(focusRequester) - .focusProperties { - exit = { - previousPodcastListHash = podcastList.hashCode() - focusRequester.saveFocusedChild() - FocusRequester.Default - } - enter = { - if (isSamePodcastList && focusRequester.restoreFocusedChild()) { - FocusRequester.Cancel - } else { - firstItem + modifier = + modifier + .focusRequester(focusRequester) + .focusProperties { + exit = { + previousPodcastListHash = podcastList.hashCode() + focusRequester.saveFocusedChild() + FocusRequester.Default } - } - }, + enter = { + if (isSamePodcastList && focusRequester.restoreFocusedChild()) { + FocusRequester.Cancel + } else { + firstItem + } + } + }, ) { itemsIndexed(podcastList) { index, podcastInfo -> - val cardModifier = if (index == 0) { - Modifier.focusRequester(firstItem) - } else { - Modifier - } + val cardModifier = + if (index == 0) { + Modifier.focusRequester(firstItem) + } else { + Modifier + } PodcastCard( podcastInfo = podcastInfo, onClick = { onPodcastSelected(podcastInfo) }, - modifier = cardModifier.width(JetcasterAppDefaults.cardWidth.medium) + modifier = cardModifier.width(JetcasterAppDefaults.cardWidth.medium), ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt index e140dec888..f50ceda371 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt @@ -54,12 +54,13 @@ internal fun EpisodeCard( title = { EpisodeMetaData( playerEpisode = playerEpisode, - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 12.dp) - .width(JetcasterAppDefaults.cardWidth.small * 2) + modifier = + Modifier + .padding(horizontal = 16.dp, vertical = 12.dp) + .width(JetcasterAppDefaults.cardWidth.small * 2), ) }, - modifier = modifier + modifier = modifier, ) } @@ -69,7 +70,7 @@ private fun EpisodeThumbnail( playerEpisode: PlayerEpisode, onClick: () -> Unit, modifier: Modifier = Modifier, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { Card( onClick = onClick, @@ -86,7 +87,7 @@ private fun EpisodeThumbnail( @Composable private fun EpisodeMetaData( playerEpisode: PlayerEpisode, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val duration = playerEpisode.duration Column(modifier = modifier) { @@ -94,12 +95,12 @@ private fun EpisodeMetaData( text = playerEpisode.title, style = MaterialTheme.typography.bodyLarge, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) Text(text = playerEpisode.podcastName, style = MaterialTheme.typography.bodySmall) if (duration != null) { Spacer( - modifier = Modifier.height(JetcasterAppDefaults.gap.podcastRow * 0.8f) + modifier = Modifier.height(JetcasterAppDefaults.gap.podcastRow * 0.8f), ) EpisodeDataAndDuration(offsetDateTime = playerEpisode.published, duration = duration) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDateAndDuration.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDateAndDuration.kt index 0ce6dbeaf7..97e6dc145c 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDateAndDuration.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDateAndDuration.kt @@ -40,12 +40,13 @@ internal fun EpisodeDataAndDuration( style: TextStyle = MaterialTheme.typography.bodySmall, ) { Text( - text = stringResource( - R.string.episode_date_duration, - MediumDateFormatter.format(offsetDateTime), - duration.toMinutes().toInt() - ), + text = + stringResource( + R.string.episode_date_duration, + MediumDateFormatter.format(offsetDateTime), + duration.toMinutes().toInt(), + ), style = style, - modifier = modifier + modifier = modifier, ) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt index 01664adb9a..46e108a6fd 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt @@ -33,20 +33,20 @@ internal fun EpisodeDetails( modifier: Modifier = Modifier, controls: (@Composable () -> Unit)? = null, verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(JetcasterAppDefaults.gap.item), - content: @Composable ColumnScope.() -> Unit + content: @Composable ColumnScope.() -> Unit, ) { TwoColumn( modifier = modifier, first = { Thumbnail( playerEpisode, - size = JetcasterAppDefaults.thumbnailSize.episodeDetails + size = JetcasterAppDefaults.thumbnailSize.episodeDetails, ) }, second = { Column( modifier = modifier, - verticalArrangement = verticalArrangement + verticalArrangement = verticalArrangement, ) { EpisodeAuthor(playerEpisode = playerEpisode) EpisodeTitle(playerEpisode = playerEpisode) @@ -55,7 +55,7 @@ internal fun EpisodeDetails( controls() } } - } + }, ) } @@ -63,7 +63,7 @@ internal fun EpisodeDetails( internal fun EpisodeAuthor( playerEpisode: PlayerEpisode, modifier: Modifier = Modifier, - style: TextStyle = MaterialTheme.typography.bodySmall + style: TextStyle = MaterialTheme.typography.bodySmall, ) { Text(text = playerEpisode.author, modifier = modifier, style = style) } @@ -72,7 +72,7 @@ internal fun EpisodeAuthor( internal fun EpisodeTitle( playerEpisode: PlayerEpisode, modifier: Modifier = Modifier, - style: TextStyle = MaterialTheme.typography.headlineLarge + style: TextStyle = MaterialTheme.typography.headlineLarge, ) { Text(text = playerEpisode.title, modifier = modifier, style = style) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeRow.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeRow.kt index 2037c32b2c..3bbd75d5c9 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeRow.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeRow.kt @@ -45,7 +45,7 @@ internal fun EpisodeRow( Arrangement.spacedBy(JetcasterAppDefaults.gap.item), contentPadding: PaddingValues = JetcasterAppDefaults.padding.episodeRowContentPadding, focusRequester: FocusRequester = remember { FocusRequester() }, - lazyListState: TvLazyListState = remember(playerEpisodeList) { TvLazyListState() } + lazyListState: TvLazyListState = remember(playerEpisodeList) { TvLazyListState() }, ) { val firstItem = remember { FocusRequester() } var previousEpisodeListHash by remember { mutableIntStateOf(playerEpisodeList.hashCode()) } @@ -53,36 +53,37 @@ internal fun EpisodeRow( TvLazyRow( state = lazyListState, - modifier = Modifier - .focusRequester(focusRequester) - .focusProperties { - enter = { - when { - lazyListState.layoutInfo.visibleItemsInfo.isEmpty() -> FocusRequester.Cancel - isSameList && focusRequester.restoreFocusedChild() -> FocusRequester.Cancel - else -> firstItem + modifier = + Modifier + .focusRequester(focusRequester) + .focusProperties { + enter = { + when { + lazyListState.layoutInfo.visibleItemsInfo.isEmpty() -> FocusRequester.Cancel + isSameList && focusRequester.restoreFocusedChild() -> FocusRequester.Cancel + else -> firstItem + } } - } - exit = { - previousEpisodeListHash = playerEpisodeList.hashCode() - focusRequester.saveFocusedChild() - FocusRequester.Default - } - } - .then(modifier), + exit = { + previousEpisodeListHash = playerEpisodeList.hashCode() + focusRequester.saveFocusedChild() + FocusRequester.Default + } + }.then(modifier), contentPadding = contentPadding, horizontalArrangement = horizontalArrangement, ) { itemsIndexed(playerEpisodeList) { index, item -> - val cardModifier = if (index == 0) { - Modifier.focusRequester(firstItem) - } else { - Modifier - } + val cardModifier = + if (index == 0) { + Modifier.focusRequester(firstItem) + } else { + Modifier + } EpisodeCard( playerEpisode = item, onClick = { onSelected(item) }, - modifier = cardModifier + modifier = cardModifier, ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/ErrorState.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/ErrorState.kt index be7f99cabd..52e2896f9e 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/ErrorState.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/ErrorState.kt @@ -37,7 +37,7 @@ import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults fun ErrorState( backToHome: () -> Unit, modifier: Modifier = Modifier, - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { LaunchedEffect(Unit) { focusRequester.requestFocus() @@ -46,13 +46,13 @@ fun ErrorState( Column { Text( text = stringResource(R.string.display_error_state), - style = MaterialTheme.typography.displayMedium + style = MaterialTheme.typography.displayMedium, ) Button( onClick = backToHome, modifier .padding(top = JetcasterAppDefaults.gap.podcastRow) - .focusRequester(focusRequester) + .focusRequester(focusRequester), ) { Text(text = stringResource(R.string.label_back_to_home)) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Loading.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Loading.kt index 4603497509..d53eb0b8b1 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Loading.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Loading.kt @@ -62,11 +62,11 @@ fun Loading( ) { Box( modifier = modifier, - contentAlignment = contentAlignment + contentAlignment = contentAlignment, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.default) + verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.default), ) { CircularProgressIndicator() Text(text = message, style = style) @@ -84,64 +84,73 @@ fun CircularProgressIndicator( ) { val transition = rememberInfiniteTransition("loading") - val stroke = with(LocalDensity.current) { - Stroke(width = strokeWidth.toPx(), cap = strokeCap) - } + val stroke = + with(LocalDensity.current) { + Stroke(width = strokeWidth.toPx(), cap = strokeCap) + } - val currentRotation = transition.animateValue( - 0, - RotationsPerCycle, - Int.VectorConverter, - infiniteRepeatable( - animation = tween( - durationMillis = RotationDuration * RotationsPerCycle, - easing = LinearEasing - ) - ), - "loading_current_rotation" - ) + val currentRotation = + transition.animateValue( + 0, + RotationsPerCycle, + Int.VectorConverter, + infiniteRepeatable( + animation = + tween( + durationMillis = RotationDuration * RotationsPerCycle, + easing = LinearEasing, + ), + ), + "loading_current_rotation", + ) // How far forward (degrees) the base point should be from the start point - val baseRotation = transition.animateFloat( - 0f, - BaseRotationAngle, - infiniteRepeatable( - animation = tween( - durationMillis = RotationDuration, - easing = LinearEasing - ) - ), - "loading_base_rotation_angle" - ) + val baseRotation = + transition.animateFloat( + 0f, + BaseRotationAngle, + infiniteRepeatable( + animation = + tween( + durationMillis = RotationDuration, + easing = LinearEasing, + ), + ), + "loading_base_rotation_angle", + ) // How far forward (degrees) both the head and tail should be from the base point - val endAngle = transition.animateFloat( - 0f, - JumpRotationAngle, - infiniteRepeatable( - animation = keyframes { - durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration - 0f at 0 using CircularEasing - JumpRotationAngle at HeadAndTailAnimationDuration - } - ), - "loading_end_rotation_angle" - ) - val startAngle = transition.animateFloat( - 0f, - JumpRotationAngle, - infiniteRepeatable( - animation = keyframes { - durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration - 0f at HeadAndTailDelayDuration using CircularEasing - JumpRotationAngle at durationMillis - } - ), - "loading_start_angle" - ) + val endAngle = + transition.animateFloat( + 0f, + JumpRotationAngle, + infiniteRepeatable( + animation = + keyframes { + durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration + 0f at 0 using CircularEasing + JumpRotationAngle at HeadAndTailAnimationDuration + }, + ), + "loading_end_rotation_angle", + ) + val startAngle = + transition.animateFloat( + 0f, + JumpRotationAngle, + infiniteRepeatable( + animation = + keyframes { + durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration + 0f at HeadAndTailDelayDuration using CircularEasing + JumpRotationAngle at durationMillis + }, + ), + "loading_start_angle", + ) Canvas( modifier .progressSemantics() - .size(CircularIndicatorDiameter) + .size(CircularIndicatorDiameter), ) { drawCircularIndicatorTrack(trackColor, stroke) @@ -157,7 +166,7 @@ fun CircularProgressIndicator( strokeWidth, sweep, color, - stroke + stroke, ) } } @@ -166,7 +175,7 @@ private fun DrawScope.drawCircularIndicator( startAngle: Float, sweep: Float, color: Color, - stroke: Stroke + stroke: Stroke, ) { // To draw this circle we need a rect with edges that line up with the midpoint of the stroke. // To do this we need to remove half the stroke width from the total diameter for both sides. @@ -179,13 +188,13 @@ private fun DrawScope.drawCircularIndicator( useCenter = false, topLeft = Offset(diameterOffset, diameterOffset), size = Size(arcDimen, arcDimen), - style = stroke + style = stroke, ) } private fun DrawScope.drawCircularIndicatorTrack( color: Color, - stroke: Stroke + stroke: Stroke, ) = drawCircularIndicator(0f, 360f, color, stroke) private fun DrawScope.drawIndeterminateCircularIndicator( @@ -193,16 +202,17 @@ private fun DrawScope.drawIndeterminateCircularIndicator( strokeWidth: Dp, sweep: Float, color: Color, - stroke: Stroke + stroke: Stroke, ) { - val strokeCapOffset = if (stroke.cap == StrokeCap.Butt) { - 0f - } else { - // Length of arc is angle * radius - // Angle (radians) is length / radius - // The length should be the same as the stroke width for calculating the min angle - (180.0 / PI).toFloat() * (strokeWidth / (CircularIndicatorDiameter / 2)) / 2f - } + val strokeCapOffset = + if (stroke.cap == StrokeCap.Butt) { + 0f + } else { + // Length of arc is angle * radius + // Angle (radians) is length / radius + // The length should be the same as the stroke width for calculating the min angle + (180.0 / PI).toFloat() * (strokeWidth / (CircularIndicatorDiameter / 2)) / 2f + } // Adding a stroke cap draws half the stroke width behind the start point, so we want to // move it forward by that amount so the arc visually appears in the correct place diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/NotAvailableFeature.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/NotAvailableFeature.kt index 21575e4c25..11b5681f33 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/NotAvailableFeature.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/NotAvailableFeature.kt @@ -25,7 +25,7 @@ import com.example.jetcaster.tv.R @Composable internal fun NotAvailableFeature( modifier: Modifier = Modifier, - message: String = stringResource(id = R.string.message_not_available_feature) + message: String = stringResource(id = R.string.message_not_available_feature), ) { Text(message, modifier = modifier) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt index 3524cae812..749b0e6fda 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt @@ -41,11 +41,11 @@ internal fun PodcastCard( onClick = onClick, interactionSource = it, scale = CardScale.None, - shape = CardDefaults.shape(RoundedCornerShape(12.dp)) + shape = CardDefaults.shape(RoundedCornerShape(12.dp)), ) { Thumbnail( podcastInfo = podcastInfo, - size = JetcasterAppDefaults.thumbnailSize.podcast + size = JetcasterAppDefaults.thumbnailSize.podcast, ) } }, diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Seekbar.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Seekbar.kt index c36c3c7fce..014abf7afa 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Seekbar.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Seekbar.kt @@ -62,11 +62,14 @@ internal fun Seekbar( val start = Offset.Zero.copy(y = knobRadius) val end = start.copy(x = size.width) - val knobCenter = start.copy( - x = timeElapsed.seconds.toFloat() / length.seconds.toFloat() * size.width - ) + val knobCenter = + start.copy( + x = timeElapsed.seconds.toFloat() / length.seconds.toFloat() * size.width, + ) drawLine( - brush, start, end, + brush, + start, + end, ) if (isFocused) { val outlineColor = color.copy(alpha = 0.6f) @@ -74,8 +77,7 @@ internal fun Seekbar( } drawCircle(brush, knobRadius, knobCenter) } - } - .height(outlineSize) + }.height(outlineSize) .focusable(true, interactionSource) .onKeyEvent { when { @@ -91,6 +93,6 @@ internal fun Seekbar( else -> false } - } + }, ) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt index ba3046716b..56035eed86 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt @@ -34,55 +34,56 @@ fun Thumbnail( podcastInfo: PodcastInfo, modifier: Modifier = Modifier, shape: RoundedCornerShape = RoundedCornerShape(12.dp), - size: DpSize = DpSize( - JetcasterAppDefaults.cardWidth.medium, - JetcasterAppDefaults.cardWidth.medium - ), - contentScale: ContentScale = ContentScale.Crop -) = - Thumbnail( - podcastInfo.imageUrl, - modifier, - shape, - size, - contentScale - ) + size: DpSize = + DpSize( + JetcasterAppDefaults.cardWidth.medium, + JetcasterAppDefaults.cardWidth.medium, + ), + contentScale: ContentScale = ContentScale.Crop, +) = Thumbnail( + podcastInfo.imageUrl, + modifier, + shape, + size, + contentScale, +) @Composable fun Thumbnail( episode: PlayerEpisode, modifier: Modifier = Modifier, shape: RoundedCornerShape = RoundedCornerShape(12.dp), - size: DpSize = DpSize( - JetcasterAppDefaults.cardWidth.medium, - JetcasterAppDefaults.cardWidth.medium - ), - contentScale: ContentScale = ContentScale.Crop -) = - Thumbnail( - episode.podcastImageUrl, - modifier, - shape, - size, - contentScale - ) + size: DpSize = + DpSize( + JetcasterAppDefaults.cardWidth.medium, + JetcasterAppDefaults.cardWidth.medium, + ), + contentScale: ContentScale = ContentScale.Crop, +) = Thumbnail( + episode.podcastImageUrl, + modifier, + shape, + size, + contentScale, +) @Composable fun Thumbnail( url: String, modifier: Modifier = Modifier, shape: RoundedCornerShape = RoundedCornerShape(12.dp), - size: DpSize = DpSize( - JetcasterAppDefaults.cardWidth.medium, - JetcasterAppDefaults.cardWidth.medium - ), - contentScale: ContentScale = ContentScale.Crop -) = - PodcastImage( - podcastImageUrl = url, - contentDescription = null, - contentScale = contentScale, - modifier = modifier + size: DpSize = + DpSize( + JetcasterAppDefaults.cardWidth.medium, + JetcasterAppDefaults.cardWidth.medium, + ), + contentScale: ContentScale = ContentScale.Crop, +) = PodcastImage( + podcastImageUrl = url, + contentDescription = null, + contentScale = contentScale, + modifier = + modifier .clip(shape) .size(size), - ) +) diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/TwoColumn.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/TwoColumn.kt index 94658ad170..7f5e4b9190 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/TwoColumn.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/TwoColumn.kt @@ -29,11 +29,11 @@ internal fun TwoColumn( second: (@Composable RowScope.() -> Unit), modifier: Modifier = Modifier, horizontalArrangement: Arrangement.Horizontal = - Arrangement.spacedBy(JetcasterAppDefaults.gap.twoColumn) + Arrangement.spacedBy(JetcasterAppDefaults.gap.twoColumn), ) { Row( horizontalArrangement = horizontalArrangement, - modifier = modifier + modifier = modifier, ) { first() second() diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt index 5bc6b4daea..89df6df12b 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt @@ -49,16 +49,17 @@ fun DiscoverScreen( showPodcastDetails: (PodcastInfo) -> Unit, playEpisode: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - discoverScreenViewModel: DiscoverScreenViewModel = hiltViewModel() + discoverScreenViewModel: DiscoverScreenViewModel = hiltViewModel(), ) { val uiState by discoverScreenViewModel.uiState.collectAsState() when (val s = uiState) { DiscoverScreenUiState.Loading -> { Loading( - modifier = Modifier - .fillMaxSize() - .then(modifier) + modifier = + Modifier + .fillMaxSize() + .then(modifier), ) } @@ -74,9 +75,10 @@ fun DiscoverScreen( discoverScreenViewModel.play(it) playEpisode(it) }, - modifier = Modifier - .fillMaxSize() - .then(modifier) + modifier = + Modifier + .fillMaxSize() + .then(modifier), ) } } @@ -87,7 +89,6 @@ fun DiscoverScreen( private fun CatalogWithCategorySelection( categoryInfoList: CategoryInfoList, podcastList: PodcastList, - selectedCategory: CategoryInfo, latestEpisodeList: EpisodeList, onPodcastSelected: (PodcastInfo) -> Unit, @@ -96,9 +97,10 @@ private fun CatalogWithCategorySelection( modifier: Modifier = Modifier, state: TvLazyListState = rememberTvLazyListState(), ) { - val (focusRequester, selectedTab) = remember { - FocusRequester.createRefs() - } + val (focusRequester, selectedTab) = + remember { + FocusRequester.createRefs() + } LaunchedEffect(Unit) { focusRequester.requestFocus() } @@ -120,18 +122,20 @@ private fun CatalogWithCategorySelection( ) { TabRow( selectedTabIndex = selectedTabIndex, - modifier = Modifier.focusProperties { - enter = { - selectedTab - } - } + modifier = + Modifier.focusProperties { + enter = { + selectedTab + } + }, ) { categoryInfoList.forEachIndexed { index, category -> - val tabModifier = if (selectedTabIndex == index) { - Modifier.focusRequester(selectedTab) - } else { - Modifier - } + val tabModifier = + if (selectedTabIndex == index) { + Modifier.focusRequester(selectedTab) + } else { + Modifier + } Tab( selected = index == selectedTabIndex, @@ -142,7 +146,7 @@ private fun CatalogWithCategorySelection( ) { Text( text = category.name, - modifier = Modifier.padding(JetcasterAppDefaults.padding.tab) + modifier = Modifier.padding(JetcasterAppDefaults.padding.tab), ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt index f3c7de4040..76a01ec924 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt @@ -29,7 +29,6 @@ import com.example.jetcaster.tv.model.CategoryInfoList import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.model.PodcastList import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -39,99 +38,109 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel -class DiscoverScreenViewModel @Inject constructor( - private val podcastsRepository: PodcastsRepository, - private val categoryStore: CategoryStore, - private val episodePlayer: EpisodePlayer, -) : ViewModel() { +class DiscoverScreenViewModel + @Inject + constructor( + private val podcastsRepository: PodcastsRepository, + private val categoryStore: CategoryStore, + private val episodePlayer: EpisodePlayer, + ) : ViewModel() { + private val selectedCategory = MutableStateFlow(null) - private val _selectedCategory = MutableStateFlow(null) + private val categoryListFlow = + categoryStore + .categoriesSortedByPodcastCount() + .map { categoryList -> + categoryList.map { category -> + CategoryInfo( + id = category.id, + name = category.name.filter { !it.isWhitespace() }, + ) + } + } - private val categoryListFlow = categoryStore - .categoriesSortedByPodcastCount() - .map { categoryList -> - categoryList.map { category -> - CategoryInfo( - id = category.id, - name = category.name.filter { !it.isWhitespace() } - ) + private val selectedCategoryFlow = + combine( + categoryListFlow, + selectedCategory, + ) { categoryList, category -> + category ?: categoryList.firstOrNull() } - } - - private val selectedCategoryFlow = combine( - categoryListFlow, - _selectedCategory - ) { categoryList, category -> - category ?: categoryList.firstOrNull() - } - @OptIn(ExperimentalCoroutinesApi::class) - private val podcastInSelectedCategory = selectedCategoryFlow.flatMapLatest { - if (it != null) { - categoryStore.podcastsInCategorySortedByPodcastCount(it.id, limit = 10) - } else { - flowOf(emptyList()) - } - }.map { list -> - PodcastList(list.map { it.asExternalModel() }) - } + @OptIn(ExperimentalCoroutinesApi::class) + private val podcastInSelectedCategory = + selectedCategoryFlow + .flatMapLatest { + if (it != null) { + categoryStore.podcastsInCategorySortedByPodcastCount(it.id, limit = 10) + } else { + flowOf(emptyList()) + } + }.map { list -> + PodcastList(list.map { it.asExternalModel() }) + } - @OptIn(ExperimentalCoroutinesApi::class) - private val latestEpisodeFlow = selectedCategoryFlow.flatMapLatest { - if (it != null) { - categoryStore.episodesFromPodcastsInCategory(it.id, 20) - } else { - flowOf(emptyList()) - } - }.map { list -> - EpisodeList(list.map { it.toPlayerEpisode() }) - } + @OptIn(ExperimentalCoroutinesApi::class) + private val latestEpisodeFlow = + selectedCategoryFlow + .flatMapLatest { + if (it != null) { + categoryStore.episodesFromPodcastsInCategory(it.id, 20) + } else { + flowOf(emptyList()) + } + }.map { list -> + EpisodeList(list.map { it.toPlayerEpisode() }) + } - val uiState = combine( - categoryListFlow, - selectedCategoryFlow, - podcastInSelectedCategory, - latestEpisodeFlow, - ) { categoryList, category, podcastList, latestEpisodes -> - if (category != null) { - DiscoverScreenUiState.Ready( - CategoryInfoList(categoryList), - category, - podcastList, - latestEpisodes + val uiState = + combine( + categoryListFlow, + selectedCategoryFlow, + podcastInSelectedCategory, + latestEpisodeFlow, + ) { categoryList, category, podcastList, latestEpisodes -> + if (category != null) { + DiscoverScreenUiState.Ready( + CategoryInfoList(categoryList), + category, + podcastList, + latestEpisodes, + ) + } else { + DiscoverScreenUiState.Loading + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + DiscoverScreenUiState.Loading, ) - } else { - DiscoverScreenUiState.Loading - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - DiscoverScreenUiState.Loading - ) - init { - refresh() - } + init { + refresh() + } - fun selectCategory(category: CategoryInfo) { - _selectedCategory.value = category - } + fun selectCategory(category: CategoryInfo) { + selectedCategory.value = category + } - fun play(playerEpisode: PlayerEpisode) { - episodePlayer.play(playerEpisode) - } + fun play(playerEpisode: PlayerEpisode) { + episodePlayer.play(playerEpisode) + } - private fun refresh() { - viewModelScope.launch { - podcastsRepository.updatePodcasts(false) + private fun refresh() { + viewModelScope.launch { + podcastsRepository.updatePodcasts(false) + } } } -} sealed interface DiscoverScreenUiState { data object Loading : DiscoverScreenUiState + data class Ready( val categoryInfoList: CategoryInfoList, val selectedCategory: CategoryInfo, diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt index b5b02abfb4..f627cd685f 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt @@ -48,24 +48,24 @@ fun EpisodeScreen( playEpisode: () -> Unit, backToHome: () -> Unit, modifier: Modifier = Modifier, - episodeScreenViewModel: EpisodeScreenViewModel = hiltViewModel() + episodeScreenViewModel: EpisodeScreenViewModel = hiltViewModel(), ) { - val uiState by episodeScreenViewModel.uiStateFlow.collectAsState() val screenModifier = modifier.fillMaxSize() when (val s = uiState) { EpisodeScreenUiState.Loading -> Loading(modifier = screenModifier) EpisodeScreenUiState.Error -> ErrorState(backToHome = backToHome, modifier = screenModifier) - is EpisodeScreenUiState.Ready -> EpisodeDetailsWithBackground( - playerEpisode = s.playerEpisode, - playEpisode = { - episodeScreenViewModel.play(it) - playEpisode() - }, - addPlayList = episodeScreenViewModel::addPlayList, - modifier = screenModifier - ) + is EpisodeScreenUiState.Ready -> + EpisodeDetailsWithBackground( + playerEpisode = s.playerEpisode, + playEpisode = { + episodeScreenViewModel.play(it) + playEpisode() + }, + addPlayList = episodeScreenViewModel::addPlayList, + modifier = screenModifier, + ) } } @@ -79,14 +79,15 @@ private fun EpisodeDetailsWithBackground( BackgroundContainer( playerEpisode = playerEpisode, contentAlignment = Alignment.Center, - modifier = modifier + modifier = modifier, ) { EpisodeDetails( playerEpisode = playerEpisode, playEpisode = playEpisode, addPlayList = addPlayList, - modifier = Modifier - .padding(JetcasterAppDefaults.overScanMargin.episode.intoPaddingValues()) + modifier = + Modifier + .padding(JetcasterAppDefaults.overScanMargin.episode.intoPaddingValues()), ) } } @@ -102,7 +103,7 @@ private fun EpisodeDetails( first = { Thumbnail( episode = playerEpisode, - size = JetcasterAppDefaults.thumbnailSize.episodeDetails + size = JetcasterAppDefaults.thumbnailSize.episodeDetails, ) }, second = { @@ -110,7 +111,7 @@ private fun EpisodeDetails( playerEpisode = playerEpisode, playEpisode = { playEpisode(playerEpisode) }, addPlayList = { addPlayList(playerEpisode) }, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) }, modifier = modifier, @@ -122,7 +123,7 @@ private fun EpisodeInfo( playerEpisode: PlayerEpisode, playEpisode: () -> Unit, addPlayList: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val duration = playerEpisode.duration @@ -137,7 +138,7 @@ private fun EpisodeInfo( text = playerEpisode.summary, softWrap = true, maxLines = 5, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) Spacer(modifier = Modifier.height(JetcasterAppDefaults.gap.paragraph)) Controls(playEpisode = playEpisode, addPlayList = addPlayList) @@ -148,12 +149,12 @@ private fun EpisodeInfo( private fun Controls( playEpisode: () -> Unit, addPlayList: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row( horizontalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.item), verticalAlignment = Alignment.CenterVertically, - modifier = modifier + modifier = modifier, ) { PlayButton(onClick = playEpisode) EnqueueButton(onClick = addPlayList) diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt index 2a5bec06f2..7682ffc1a4 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt @@ -26,7 +26,6 @@ import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.core.player.model.toPlayerEpisode import com.example.jetcaster.tv.ui.Screen import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.flatMapLatest @@ -34,59 +33,69 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel -class EpisodeScreenViewModel @Inject constructor( - handle: SavedStateHandle, - podcastsRepository: PodcastsRepository, - episodeStore: EpisodeStore, - private val episodePlayer: EpisodePlayer, -) : ViewModel() { +class EpisodeScreenViewModel + @Inject + constructor( + handle: SavedStateHandle, + podcastsRepository: PodcastsRepository, + episodeStore: EpisodeStore, + private val episodePlayer: EpisodePlayer, + ) : ViewModel() { + private val episodeUriFlow = handle.getStateFlow(Screen.Episode.PARAMETER_NAME, null) - private val episodeUriFlow = handle.getStateFlow(Screen.Episode.PARAMETER_NAME, null) + @OptIn(ExperimentalCoroutinesApi::class) + private val episodeToPodcastFlow = + episodeUriFlow + .flatMapLatest { + if (it != null) { + episodeStore.episodeAndPodcastWithUri(it) + } else { + flowOf(null) + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + null, + ) - @OptIn(ExperimentalCoroutinesApi::class) - private val episodeToPodcastFlow = episodeUriFlow.flatMapLatest { - if (it != null) { - episodeStore.episodeAndPodcastWithUri(it) - } else { - flowOf(null) - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - null - ) + val uiStateFlow = + episodeToPodcastFlow + .map { + if (it != null) { + EpisodeScreenUiState.Ready(it.toPlayerEpisode()) + } else { + EpisodeScreenUiState.Error + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + EpisodeScreenUiState.Loading, + ) - val uiStateFlow = episodeToPodcastFlow.map { - if (it != null) { - EpisodeScreenUiState.Ready(it.toPlayerEpisode()) - } else { - EpisodeScreenUiState.Error + fun addPlayList(episode: PlayerEpisode) { + episodePlayer.addToQueue(episode) } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - EpisodeScreenUiState.Loading - ) - - fun addPlayList(episode: PlayerEpisode) { - episodePlayer.addToQueue(episode) - } - fun play(playerEpisode: PlayerEpisode) { - episodePlayer.play(playerEpisode) - } + fun play(playerEpisode: PlayerEpisode) { + episodePlayer.play(playerEpisode) + } - init { - viewModelScope.launch { - podcastsRepository.updatePodcasts(false) + init { + viewModelScope.launch { + podcastsRepository.updatePodcasts(false) + } } } -} sealed interface EpisodeScreenUiState { data object Loading : EpisodeScreenUiState + data object Error : EpisodeScreenUiState - data class Ready(val playerEpisode: PlayerEpisode) : EpisodeScreenUiState + + data class Ready( + val playerEpisode: PlayerEpisode, + ) : EpisodeScreenUiState } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt index ed73883b0d..b7dcb2669d 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt @@ -50,7 +50,7 @@ fun LibraryScreen( navigateToDiscover: () -> Unit, showPodcastDetails: (PodcastInfo) -> Unit, playEpisode: (PlayerEpisode) -> Unit, - libraryScreenViewModel: LibraryScreenViewModel = hiltViewModel() + libraryScreenViewModel: LibraryScreenViewModel = hiltViewModel(), ) { val uiState by libraryScreenViewModel.uiState.collectAsState() when (val s = uiState) { @@ -59,16 +59,17 @@ fun LibraryScreen( NavigateToDiscover(onNavigationRequested = navigateToDiscover, modifier = modifier) } - is LibraryScreenUiState.Ready -> Library( - podcastList = s.subscribedPodcastList, - episodeList = s.latestEpisodeList, - showPodcastDetails = showPodcastDetails, - onEpisodeSelected = { - libraryScreenViewModel.playEpisode(it) - playEpisode(it) - }, - modifier = modifier, - ) + is LibraryScreenUiState.Ready -> + Library( + podcastList = s.subscribedPodcastList, + episodeList = s.latestEpisodeList, + showPodcastDetails = showPodcastDetails, + onEpisodeSelected = { + libraryScreenViewModel.playEpisode(it) + playEpisode(it) + }, + modifier = modifier, + ) } } @@ -91,9 +92,10 @@ private fun Library( latestEpisodeList = episodeList, onPodcastSelected = showPodcastDetails, onEpisodeSelected = onEpisodeSelected, - modifier = modifier - .focusRequester(focusRequester) - .focusRestorer() + modifier = + modifier + .focusRequester(focusRequester) + .focusRestorer(), ) } @@ -110,14 +112,15 @@ private fun NavigateToDiscover( Column { Text( text = stringResource(id = R.string.display_no_subscribed_podcast), - style = MaterialTheme.typography.displayMedium + style = MaterialTheme.typography.displayMedium, ) Text(text = stringResource(id = R.string.message_no_subscribed_podcast)) Button( onClick = onNavigationRequested, - modifier = Modifier - .padding(top = JetcasterAppDefaults.gap.podcastRow) - .focusRequester(focusRequester) + modifier = + Modifier + .padding(top = JetcasterAppDefaults.gap.podcastRow) + .focusRequester(focusRequester), ) { Text(text = stringResource(id = R.string.label_navigate_to_discover)) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt index f5b8827ff7..bd9279d332 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt @@ -28,7 +28,6 @@ import com.example.jetcaster.core.player.model.toPlayerEpisode import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.model.PodcastList import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -37,64 +36,69 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel -class LibraryScreenViewModel @Inject constructor( - private val podcastsRepository: PodcastsRepository, - private val episodeStore: EpisodeStore, - podcastStore: PodcastStore, - private val episodePlayer: EpisodePlayer, -) : ViewModel() { - - private val followingPodcastListFlow = - podcastStore.followedPodcastsSortedByLastEpisode().map { list -> - PodcastList(list.map { it.asExternalModel() }) - } +class LibraryScreenViewModel + @Inject + constructor( + private val podcastsRepository: PodcastsRepository, + private val episodeStore: EpisodeStore, + podcastStore: PodcastStore, + private val episodePlayer: EpisodePlayer, + ) : ViewModel() { + private val followingPodcastListFlow = + podcastStore.followedPodcastsSortedByLastEpisode().map { list -> + PodcastList(list.map { it.asExternalModel() }) + } - @OptIn(ExperimentalCoroutinesApi::class) - private val latestEpisodeListFlow = podcastStore - .followedPodcastsSortedByLastEpisode() - .flatMapLatest { podcastList -> - if (podcastList.isNotEmpty()) { - combine(podcastList.map { episodeStore.episodesInPodcast(it.podcast.uri, 1) }) { - it.map { episodes -> - episodes.first() + @OptIn(ExperimentalCoroutinesApi::class) + private val latestEpisodeListFlow = + podcastStore + .followedPodcastsSortedByLastEpisode() + .flatMapLatest { podcastList -> + if (podcastList.isNotEmpty()) { + combine(podcastList.map { episodeStore.episodesInPodcast(it.podcast.uri, 1) }) { + it.map { episodes -> + episodes.first() + } + } + } else { + flowOf(emptyList()) } + }.map { list -> + EpisodeList(list.map { it.toPlayerEpisode() }) } - } else { - flowOf(emptyList()) - } - }.map { list -> - EpisodeList(list.map { it.toPlayerEpisode() }) - } - val uiState = - combine(followingPodcastListFlow, latestEpisodeListFlow) { podcastList, episodeList -> - if (podcastList.isEmpty()) { - LibraryScreenUiState.NoSubscribedPodcast - } else { - LibraryScreenUiState.Ready(podcastList, episodeList) - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - LibraryScreenUiState.Loading - ) + val uiState = + combine(followingPodcastListFlow, latestEpisodeListFlow) { podcastList, episodeList -> + if (podcastList.isEmpty()) { + LibraryScreenUiState.NoSubscribedPodcast + } else { + LibraryScreenUiState.Ready(podcastList, episodeList) + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + LibraryScreenUiState.Loading, + ) - init { - viewModelScope.launch { - podcastsRepository.updatePodcasts(false) + init { + viewModelScope.launch { + podcastsRepository.updatePodcasts(false) + } } - } - fun playEpisode(playerEpisode: PlayerEpisode) { - episodePlayer.play(playerEpisode) + fun playEpisode(playerEpisode: PlayerEpisode) { + episodePlayer.play(playerEpisode) + } } -} sealed interface LibraryScreenUiState { data object Loading : LibraryScreenUiState + data object NoSubscribedPodcast : LibraryScreenUiState + data class Ready( val subscribedPodcastList: PodcastList, val latestEpisodeList: EpisodeList, diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreen.kt index 03a165e1a5..04ca4493d0 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreen.kt @@ -77,16 +77,16 @@ import com.example.jetcaster.tv.ui.component.RewindButton import com.example.jetcaster.tv.ui.component.Seekbar import com.example.jetcaster.tv.ui.component.SkipButton import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults -import java.time.Duration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import java.time.Duration @Composable fun PlayerScreen( backToHome: () -> Unit, showDetails: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - playScreenViewModel: PlayerScreenViewModel = hiltViewModel() + playScreenViewModel: PlayerScreenViewModel = hiltViewModel(), ) { val uiState by playScreenViewModel.uiStateFlow.collectAsStateWithLifecycle() @@ -126,7 +126,7 @@ private fun Player( showDetails: (PlayerEpisode) -> Unit, playEpisode: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - autoStart: Boolean = true + autoStart: Boolean = true, ) { LaunchedEffect(key1 = autoStart) { if (autoStart && !episodePlayerState.isPlaying) { @@ -172,7 +172,7 @@ private fun EpisodePlayerWithBackground( enqueue: (PlayerEpisode) -> Unit, showDetails: (PlayerEpisode) -> Unit, playEpisode: (PlayerEpisode) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val episodePlayer = remember { FocusRequester() } @@ -183,9 +183,8 @@ private fun EpisodePlayerWithBackground( BackgroundContainer( playerEpisode = playerEpisode, modifier = modifier, - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { - EpisodePlayer( playerEpisode = playerEpisode, isPlaying = isPlaying, @@ -199,16 +198,19 @@ private fun EpisodePlayerWithBackground( enqueue = enqueue, showDetails = showDetails, focusRequester = episodePlayer, - modifier = Modifier - .padding(JetcasterAppDefaults.overScanMargin.player.intoPaddingValues()) + modifier = + Modifier + .padding(JetcasterAppDefaults.overScanMargin.player.intoPaddingValues()), ) PlayerQueueOverlay( playerEpisodeList = queue, onSelected = playEpisode, modifier = Modifier.fillMaxSize(), - contentPadding = JetcasterAppDefaults.overScanMargin.player.copy(top = 0.dp) - .intoPaddingValues(), + contentPadding = + JetcasterAppDefaults.overScanMargin.player + .copy(top = 0.dp) + .intoPaddingValues(), offset = DpOffset(0.dp, 136.dp), ) } @@ -231,20 +233,20 @@ private fun EpisodePlayer( modifier: Modifier = Modifier, bringIntoViewRequester: BringIntoViewRequester = remember { BringIntoViewRequester() }, coroutineScope: CoroutineScope = rememberCoroutineScope(), - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { Column( verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.section), - modifier = Modifier - .bringIntoViewRequester(bringIntoViewRequester) - .onFocusChanged { - if (it.hasFocus) { - coroutineScope.launch { - bringIntoViewRequester.bringIntoView() + modifier = + Modifier + .bringIntoViewRequester(bringIntoViewRequester) + .onFocusChanged { + if (it.hasFocus) { + coroutineScope.launch { + bringIntoViewRequester.bringIntoView() + } } - } - } - .then(modifier) + }.then(modifier), ) { EpisodeDetails( playerEpisode = playerEpisode, @@ -252,7 +254,7 @@ private fun EpisodePlayer( controls = { EpisodeControl( showDetails = { showDetails(playerEpisode) }, - enqueue = { enqueue(playerEpisode) } + enqueue = { enqueue(playerEpisode) }, ) }, ) @@ -266,7 +268,7 @@ private fun EpisodePlayer( next = next, skip = skip, rewind = rewind, - focusRequester = focusRequester + focusRequester = focusRequester, ) } } @@ -279,15 +281,15 @@ private fun EpisodeControl( ) { Row( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.item) + horizontalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.item), ) { EnqueueButton( onClick = enqueue, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.default.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.default.intoDpSize()), ) InfoButton( onClick = showDetails, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.default.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.default.intoDpSize()), ) } } @@ -305,7 +307,7 @@ private fun PlayerControl( skip: () -> Unit, rewind: () -> Unit, modifier: Modifier = Modifier, - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { val playPauseButton = remember { FocusRequester() } @@ -314,28 +316,29 @@ private fun PlayerControl( modifier = modifier, ) { Row( - horizontalArrangement = Arrangement.spacedBy( - JetcasterAppDefaults.gap.default, - Alignment.CenterHorizontally - ), + horizontalArrangement = + Arrangement.spacedBy( + JetcasterAppDefaults.gap.default, + Alignment.CenterHorizontally, + ), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .onFocusChanged { - if (it.isFocused) { - playPauseButton.requestFocus() - } - } - .focusable(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + .onFocusChanged { + if (it.isFocused) { + playPauseButton.requestFocus() + } + }.focusable(), ) { PreviousButton( onClick = previous, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()), ) RewindButton( onClick = rewind, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()), ) PlayPauseButton( isPlaying = isPlaying, @@ -346,17 +349,18 @@ private fun PlayerControl( play() } }, - modifier = Modifier - .size(JetcasterAppDefaults.iconButtonSize.large.intoDpSize()) - .focusRequester(playPauseButton) + modifier = + Modifier + .size(JetcasterAppDefaults.iconButtonSize.large.intoDpSize()) + .focusRequester(playPauseButton), ) SkipButton( onClick = skip, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()), ) NextButton( onClick = next, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()), ) } if (length != null) { @@ -372,11 +376,11 @@ private fun ElapsedTimeIndicator( skip: () -> Unit, rewind: () -> Unit, modifier: Modifier = Modifier, - knobSize: Dp = 8.dp + knobSize: Dp = 8.dp, ) { Column( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.tiny) + verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.tiny), ) { ElapsedTime(timeElapsed = timeElapsed, length = length) Seekbar( @@ -385,7 +389,7 @@ private fun ElapsedTimeIndicator( knobSize = knobSize, onMoveLeft = rewind, onMoveRight = skip, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } } @@ -395,20 +399,20 @@ private fun ElapsedTime( timeElapsed: Duration, length: Duration, modifier: Modifier = Modifier, - style: TextStyle = MaterialTheme.typography.bodySmall + style: TextStyle = MaterialTheme.typography.bodySmall, ) { val elapsed = stringResource( R.string.minutes_seconds, timeElapsed.toMinutes(), - timeElapsed.toSeconds() % 60 + timeElapsed.toSeconds() % 60, ) val l = stringResource(R.string.minutes_seconds, length.toMinutes(), length.toSeconds() % 60) Text( text = stringResource(R.string.elapsed_time, elapsed, l), style = style, - modifier = modifier + modifier = modifier, ) } @@ -416,7 +420,7 @@ private fun ElapsedTime( private fun NoEpisodeInQueue( backToHome: () -> Unit, modifier: Modifier = Modifier, - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { LaunchedEffect(Unit) { focusRequester.requestFocus() @@ -425,7 +429,7 @@ private fun NoEpisodeInQueue( Column { Text( text = stringResource(R.string.display_nothing_in_queue), - style = MaterialTheme.typography.displayMedium + style = MaterialTheme.typography.displayMedium, ) Spacer(modifier = Modifier.height(JetcasterAppDefaults.gap.paragraph)) Text(text = stringResource(R.string.message_nothing_in_queue)) @@ -447,27 +451,30 @@ private fun PlayerQueueOverlay( contentPadding: PaddingValues = PaddingValues(), contentAlignment: Alignment = Alignment.BottomStart, scrim: DrawScope.() -> Unit = { - val brush = Brush.verticalGradient( - listOf(Color.Transparent, Color.Black), - ) + val brush = + Brush.verticalGradient( + listOf(Color.Transparent, Color.Black), + ) drawRect(brush, blendMode = BlendMode.Multiply) }, offset: DpOffset = DpOffset.Zero, ) { var hasFocus by remember { mutableStateOf(false) } - val actualOffset = if (hasFocus) { - DpOffset.Zero - } else { - offset - } + val actualOffset = + if (hasFocus) { + DpOffset.Zero + } else { + offset + } Box( - modifier = modifier.drawWithCache { - onDrawBehind { - if (hasFocus) { - scrim() + modifier = + modifier.drawWithCache { + onDrawBehind { + if (hasFocus) { + scrim() + } } - } - }, + }, contentAlignment = contentAlignment, ) { EpisodeRow( @@ -475,9 +482,10 @@ private fun PlayerQueueOverlay( onSelected = onSelected, horizontalArrangement = horizontalArrangement, contentPadding = contentPadding, - modifier = Modifier - .offset(actualOffset.x, actualOffset.y) - .onFocusChanged { hasFocus = it.hasFocus } + modifier = + Modifier + .offset(actualOffset.x, actualOffset.y) + .onFocusChanged { hasFocus = it.hasFocus }, ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreenViewModel.kt index 9b66a9359d..9f8a618fd7 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreenViewModel.kt @@ -22,61 +22,69 @@ import com.example.jetcaster.core.player.EpisodePlayer import com.example.jetcaster.core.player.EpisodePlayerState import com.example.jetcaster.core.player.model.PlayerEpisode import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.Duration -import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import java.time.Duration +import javax.inject.Inject @HiltViewModel -class PlayerScreenViewModel @Inject constructor( - private val episodePlayer: EpisodePlayer, -) : ViewModel() { +class PlayerScreenViewModel + @Inject + constructor( + private val episodePlayer: EpisodePlayer, + ) : ViewModel() { + val uiStateFlow = + episodePlayer.playerState + .map { + if (it.currentEpisode == null && it.queue.isEmpty()) { + PlayerScreenUiState.NoEpisodeInQueue + } else { + PlayerScreenUiState.Ready(it) + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + PlayerScreenUiState.Loading, + ) - val uiStateFlow = episodePlayer.playerState.map { - if (it.currentEpisode == null && it.queue.isEmpty()) { - PlayerScreenUiState.NoEpisodeInQueue - } else { - PlayerScreenUiState.Ready(it) - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - PlayerScreenUiState.Loading - ) + private val skipAmount = Duration.ofSeconds(10L) - private val skipAmount = Duration.ofSeconds(10L) + fun play() { + if (episodePlayer.playerState.value.currentEpisode == null) { + episodePlayer.next() + } + episodePlayer.play() + } - fun play() { - if (episodePlayer.playerState.value.currentEpisode == null) { - episodePlayer.next() + fun play(playerEpisode: PlayerEpisode) { + episodePlayer.play(playerEpisode) } - episodePlayer.play() - } - fun play(playerEpisode: PlayerEpisode) { - episodePlayer.play(playerEpisode) - } - fun pause() = episodePlayer.pause() - fun next() = episodePlayer.next() - fun previous() = episodePlayer.previous() - fun skip() { - episodePlayer.advanceBy(skipAmount) - } + fun pause() = episodePlayer.pause() - fun rewind() { - episodePlayer.rewindBy(skipAmount) - } + fun next() = episodePlayer.next() + + fun previous() = episodePlayer.previous() - fun enqueue(playerEpisode: PlayerEpisode) { - episodePlayer.addToQueue(playerEpisode) + fun skip() { + episodePlayer.advanceBy(skipAmount) + } + + fun rewind() { + episodePlayer.rewindBy(skipAmount) + } + + fun enqueue(playerEpisode: PlayerEpisode) { + episodePlayer.addToQueue(playerEpisode) + } } -} sealed interface PlayerScreenUiState { data object Loading : PlayerScreenUiState + data class Ready( - val playerState: EpisodePlayerState + val playerState: EpisodePlayerState, ) : PlayerScreenUiState data object NoEpisodeInQueue : PlayerScreenUiState diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreen.kt index 42fbc4ec6d..5d056d6095 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreen.kt @@ -85,19 +85,20 @@ fun PodcastDetailsScreen( when (val s = uiState) { PodcastScreenUiState.Loading -> Loading(modifier = modifier) PodcastScreenUiState.Error -> ErrorState(backToHome = backToHomeScreen, modifier = modifier) - is PodcastScreenUiState.Ready -> PodcastDetailsWithBackground( - podcastInfo = s.podcastInfo, - episodeList = s.episodeList, - isSubscribed = s.isSubscribed, - subscribe = podcastDetailsScreenViewModel::subscribe, - unsubscribe = podcastDetailsScreenViewModel::unsubscribe, - playEpisode = { - podcastDetailsScreenViewModel.play(it) - playEpisode(it) - }, - enqueue = podcastDetailsScreenViewModel::enqueue, - showEpisodeDetails = showEpisodeDetails, - ) + is PodcastScreenUiState.Ready -> + PodcastDetailsWithBackground( + podcastInfo = s.podcastInfo, + episodeList = s.episodeList, + isSubscribed = s.isSubscribed, + subscribe = podcastDetailsScreenViewModel::subscribe, + unsubscribe = podcastDetailsScreenViewModel::unsubscribe, + playEpisode = { + podcastDetailsScreenViewModel.play(it) + playEpisode(it) + }, + enqueue = podcastDetailsScreenViewModel::enqueue, + showEpisodeDetails = showEpisodeDetails, + ) } } @@ -112,9 +113,8 @@ private fun PodcastDetailsWithBackground( showEpisodeDetails: (PlayerEpisode) -> Unit, enqueue: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { - BackgroundContainer(podcastInfo = podcastInfo, modifier = modifier) { PodcastDetails( podcastInfo = podcastInfo, @@ -126,8 +126,9 @@ private fun PodcastDetailsWithBackground( focusRequester = focusRequester, showEpisodeDetails = showEpisodeDetails, enqueue = enqueue, - modifier = Modifier - .fillMaxSize() + modifier = + Modifier + .fillMaxSize(), ) } } @@ -144,24 +145,26 @@ private fun PodcastDetails( showEpisodeDetails: (PlayerEpisode) -> Unit, enqueue: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { TwoColumn( modifier = modifier, horizontalArrangement = - Arrangement.spacedBy(JetcasterAppDefaults.gap.twoColumn), + Arrangement.spacedBy(JetcasterAppDefaults.gap.twoColumn), first = { PodcastInfo( podcastInfo = podcastInfo, isSubscribed = isSubscribed, subscribe = subscribe, unsubscribe = unsubscribe, - modifier = Modifier - .weight(0.3f) - .padding( - JetcasterAppDefaults.overScanMargin.podcast.copy(end = 0.dp) - .intoPaddingValues() - ), + modifier = + Modifier + .weight(0.3f) + .padding( + JetcasterAppDefaults.overScanMargin.podcast + .copy(end = 0.dp) + .intoPaddingValues(), + ), ) }, second = { @@ -170,12 +173,13 @@ private fun PodcastDetails( playEpisode = { playEpisode(it) }, showDetails = showEpisodeDetails, enqueue = enqueue, - modifier = Modifier - .focusRequester(focusRequester) - .focusRestorer() - .weight(0.7f) + modifier = + Modifier + .focusRequester(focusRequester) + .focusRestorer() + .weight(0.7f), ) - } + }, ) LaunchedEffect(Unit) { @@ -197,7 +201,7 @@ private fun PodcastInfo( Text( text = podcastInfo.author, - style = MaterialTheme.typography.bodySmall + style = MaterialTheme.typography.bodySmall, ) Text( text = podcastInfo.title, @@ -207,15 +211,16 @@ private fun PodcastInfo( text = podcastInfo.description, maxLines = 2, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) ToggleSubscriptionButton( podcastInfo, isSubscribed, subscribe, unsubscribe, - modifier = Modifier - .padding(top = JetcasterAppDefaults.gap.podcastRow) + modifier = + Modifier + .padding(top = JetcasterAppDefaults.gap.podcastRow), ) } } @@ -226,29 +231,32 @@ private fun ToggleSubscriptionButton( isSubscribed: Boolean, subscribe: (PodcastInfo, Boolean) -> Unit, unsubscribe: (PodcastInfo, Boolean) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val icon = if (isSubscribed) { - Icons.Default.Remove - } else { - Icons.Default.Add - } - val label = if (isSubscribed) { - stringResource(R.string.label_unsubscribe) - } else { - stringResource(R.string.label_subscribe) - } - val action = if (isSubscribed) { - unsubscribe - } else { - subscribe - } + val icon = + if (isSubscribed) { + Icons.Default.Remove + } else { + Icons.Default.Add + } + val label = + if (isSubscribed) { + stringResource(R.string.label_unsubscribe) + } else { + stringResource(R.string.label_subscribe) + } + val action = + if (isSubscribed) { + unsubscribe + } else { + subscribe + } ButtonWithIcon( label = label, icon = icon, onClick = { action(podcastInfo, isSubscribed) }, scale = ButtonDefaults.scale(scale = 1f), - modifier = modifier + modifier = modifier, ) } @@ -258,12 +266,12 @@ private fun PodcastEpisodeList( playEpisode: (PlayerEpisode) -> Unit, showDetails: (PlayerEpisode) -> Unit, enqueue: (PlayerEpisode) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { TvLazyColumn( verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.podcastRow), modifier = modifier, - contentPadding = JetcasterAppDefaults.overScanMargin.podcast.intoPaddingValues() + contentPadding = JetcasterAppDefaults.overScanMargin.podcast.intoPaddingValues(), ) { items(episodeList) { EpisodeListItem( @@ -291,37 +299,40 @@ private fun EpisodeListItem( } val shape = RoundedCornerShape(cornerRadius) - val backgroundColor = if (hasFocus) { - MaterialTheme.colorScheme.surface - } else { - Color.Transparent - } + val backgroundColor = + if (hasFocus) { + MaterialTheme.colorScheme.surface + } else { + Color.Transparent + } - val borderColor = if (hasFocus) { - MaterialTheme.colorScheme.border - } else { - Color.Transparent - } - val elevation = if (hasFocus) { - 10.dp - } else { - 0.dp - } + val borderColor = + if (hasFocus) { + MaterialTheme.colorScheme.border + } else { + Color.Transparent + } + val elevation = + if (hasFocus) { + 10.dp + } else { + 0.dp + } EpisodeListItemContentLayer( playerEpisode = playerEpisode, onEpisodeSelected = onEpisodeSelected, onInfoClicked = onInfoClicked, onEnqueueClicked = onEnqueueClicked, - modifier = modifier - .clip(shape) - .onFocusChanged { - hasFocus = it.hasFocus - } - .border(borderWidth, borderColor, shape) - .background(backgroundColor) - .shadow(elevation, shape) - .padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 16.dp) + modifier = + modifier + .clip(shape) + .onFocusChanged { + hasFocus = it.hasFocus + }.border(borderWidth, borderColor, shape) + .background(backgroundColor) + .shadow(elevation, shape) + .padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 16.dp), ) } @@ -337,9 +348,8 @@ private fun EpisodeListItemContentLayer( val playButton = remember { FocusRequester() } Box( contentAlignment = Alignment.CenterStart, - modifier = modifier + modifier = modifier, ) { - Column( verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.tiny), ) { @@ -347,12 +357,13 @@ private fun EpisodeListItemContentLayer( Row( horizontalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.default), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(top = JetcasterAppDefaults.gap.paragraph) + modifier = + Modifier + .padding(top = JetcasterAppDefaults.gap.paragraph), ) { PlayButton( onClick = onEpisodeSelected, - modifier = Modifier.focusRequester(playButton) + modifier = Modifier.focusRequester(playButton), ) if (duration != null) { EpisodeDataAndDuration(playerEpisode.published, duration) @@ -366,10 +377,13 @@ private fun EpisodeListItemContentLayer( } @Composable -private fun EpisodeTitle(playerEpisode: PlayerEpisode, modifier: Modifier = Modifier) { +private fun EpisodeTitle( + playerEpisode: PlayerEpisode, + modifier: Modifier = Modifier, +) { Text( text = playerEpisode.title, style = MaterialTheme.typography.titleLarge, - modifier = modifier + modifier = modifier, ) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreenViewModel.kt index c68033c656..1844fe9287 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreenViewModel.kt @@ -29,7 +29,6 @@ import com.example.jetcaster.core.player.model.toPlayerEpisode import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.ui.Screen import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -38,89 +37,102 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel -class PodcastDetailsScreenViewModel @Inject constructor( - handle: SavedStateHandle, - private val podcastStore: PodcastStore, - episodeStore: EpisodeStore, - private val episodePlayer: EpisodePlayer, -) : ViewModel() { +class PodcastDetailsScreenViewModel + @Inject + constructor( + handle: SavedStateHandle, + private val podcastStore: PodcastStore, + episodeStore: EpisodeStore, + private val episodePlayer: EpisodePlayer, + ) : ViewModel() { + private val podcastUri = handle.get(Screen.Podcast.PARAMETER_NAME) - private val podcastUri = handle.get(Screen.Podcast.PARAMETER_NAME) - - @OptIn(ExperimentalCoroutinesApi::class) - private val podcastFlow = - handle.getStateFlow(Screen.Podcast.PARAMETER_NAME, null).flatMapLatest { - if (it != null) { - podcastStore.podcastWithUri(it) - } else { - flowOf(null) + @OptIn(ExperimentalCoroutinesApi::class) + private val podcastFlow = + handle.getStateFlow(Screen.Podcast.PARAMETER_NAME, null).flatMapLatest { + if (it != null) { + podcastStore.podcastWithUri(it) + } else { + flowOf(null) + } } - } - @OptIn(ExperimentalCoroutinesApi::class) - private val episodeListFlow = podcastFlow.flatMapLatest { - if (it != null) { - episodeStore.episodesInPodcast(it.uri) - } else { - flowOf(emptyList()) - } - }.map { list -> - EpisodeList(list.map { it.toPlayerEpisode() }) - } + @OptIn(ExperimentalCoroutinesApi::class) + private val episodeListFlow = + podcastFlow + .flatMapLatest { + if (it != null) { + episodeStore.episodesInPodcast(it.uri) + } else { + flowOf(emptyList()) + } + }.map { list -> + EpisodeList(list.map { it.toPlayerEpisode() }) + } - private val subscribedPodcastListFlow = - podcastStore.followedPodcastsSortedByLastEpisode() + private val subscribedPodcastListFlow = + podcastStore.followedPodcastsSortedByLastEpisode() - val uiStateFlow = combine( - podcastFlow, - episodeListFlow, - subscribedPodcastListFlow - ) { podcast, episodeList, subscribedPodcastList -> - if (podcast != null) { - val isSubscribed = subscribedPodcastList.any { it.podcast.uri == podcastUri } - PodcastScreenUiState.Ready(podcast.asExternalModel(), episodeList, isSubscribed) - } else { - PodcastScreenUiState.Error - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - PodcastScreenUiState.Loading - ) + val uiStateFlow = + combine( + podcastFlow, + episodeListFlow, + subscribedPodcastListFlow, + ) { podcast, episodeList, subscribedPodcastList -> + if (podcast != null) { + val isSubscribed = subscribedPodcastList.any { it.podcast.uri == podcastUri } + PodcastScreenUiState.Ready(podcast.asExternalModel(), episodeList, isSubscribed) + } else { + PodcastScreenUiState.Error + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + PodcastScreenUiState.Loading, + ) - fun subscribe(podcastInfo: PodcastInfo, isSubscribed: Boolean) { - if (!isSubscribed) { - viewModelScope.launch { - podcastStore.togglePodcastFollowed(podcastInfo.uri) + fun subscribe( + podcastInfo: PodcastInfo, + isSubscribed: Boolean, + ) { + if (!isSubscribed) { + viewModelScope.launch { + podcastStore.togglePodcastFollowed(podcastInfo.uri) + } } } - } - fun unsubscribe(podcastInfo: PodcastInfo, isSubscribed: Boolean) { - if (isSubscribed) { - viewModelScope.launch { - podcastStore.togglePodcastFollowed(podcastInfo.uri) + fun unsubscribe( + podcastInfo: PodcastInfo, + isSubscribed: Boolean, + ) { + if (isSubscribed) { + viewModelScope.launch { + podcastStore.togglePodcastFollowed(podcastInfo.uri) + } } } - } - fun play(playerEpisode: PlayerEpisode) { - episodePlayer.play(playerEpisode) - } + fun play(playerEpisode: PlayerEpisode) { + episodePlayer.play(playerEpisode) + } - fun enqueue(playerEpisode: PlayerEpisode) { - episodePlayer.addToQueue(playerEpisode) + fun enqueue(playerEpisode: PlayerEpisode) { + episodePlayer.addToQueue(playerEpisode) + } } -} sealed interface PodcastScreenUiState { data object Loading : PodcastScreenUiState + data object Error : PodcastScreenUiState + data class Ready( val podcastInfo: PodcastInfo, val episodeList: EpisodeList, - val isSubscribed: Boolean + val isSubscribed: Boolean, ) : PodcastScreenUiState } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt index 7df5f96a77..c716f857c1 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt @@ -68,31 +68,33 @@ import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults fun SearchScreen( onPodcastSelected: (PodcastInfo) -> Unit, modifier: Modifier = Modifier, - searchScreenViewModel: SearchScreenViewModel = hiltViewModel() + searchScreenViewModel: SearchScreenViewModel = hiltViewModel(), ) { val uiState by searchScreenViewModel.uiStateFlow.collectAsState() when (val s = uiState) { SearchScreenUiState.Loading -> Loading(modifier = modifier) - is SearchScreenUiState.Ready -> Ready( - keyword = s.keyword, - categorySelectionList = s.categorySelectionList, - onKeywordInput = searchScreenViewModel::setKeyword, - onCategorySelected = searchScreenViewModel::addCategoryToSelectedCategoryList, - onCategoryUnselected = searchScreenViewModel::removeCategoryFromSelectedCategoryList, - modifier = modifier - ) + is SearchScreenUiState.Ready -> + Ready( + keyword = s.keyword, + categorySelectionList = s.categorySelectionList, + onKeywordInput = searchScreenViewModel::setKeyword, + onCategorySelected = searchScreenViewModel::addCategoryToSelectedCategoryList, + onCategoryUnselected = searchScreenViewModel::removeCategoryFromSelectedCategoryList, + modifier = modifier, + ) - is SearchScreenUiState.HasResult -> HasResult( - keyword = s.keyword, - categorySelectionList = s.categorySelectionList, - podcastList = s.result, - onKeywordInput = searchScreenViewModel::setKeyword, - onCategorySelected = searchScreenViewModel::addCategoryToSelectedCategoryList, - onCategoryUnselected = searchScreenViewModel::removeCategoryFromSelectedCategoryList, - onPodcastSelected = onPodcastSelected, - modifier = modifier, - ) + is SearchScreenUiState.HasResult -> + HasResult( + keyword = s.keyword, + categorySelectionList = s.categorySelectionList, + podcastList = s.result, + onKeywordInput = searchScreenViewModel::setKeyword, + onCategorySelected = searchScreenViewModel::addCategoryToSelectedCategoryList, + onCategoryUnselected = searchScreenViewModel::removeCategoryFromSelectedCategoryList, + onPodcastSelected = onPodcastSelected, + modifier = modifier, + ) } } @@ -103,7 +105,7 @@ private fun Ready( onKeywordInput: (String) -> Unit, onCategorySelected: (CategoryInfo) -> Unit, onCategoryUnselected: (CategoryInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Controls( keyword = keyword, @@ -112,7 +114,7 @@ private fun Ready( onCategorySelected = onCategorySelected, onCategoryUnselected = onCategoryUnselected, modifier = modifier, - toRequestFocus = true + toRequestFocus = true, ) } @@ -125,7 +127,7 @@ private fun HasResult( onCategorySelected: (CategoryInfo) -> Unit, onCategoryUnselected: (CategoryInfo) -> Unit, onPodcastSelected: (PodcastInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { SearchResult( podcastList = podcastList, @@ -139,7 +141,7 @@ private fun HasResult( onCategoryUnselected = onCategoryUnselected, ) }, - modifier = modifier + modifier = modifier, ) } @@ -153,7 +155,7 @@ private fun Controls( onCategoryUnselected: (CategoryInfo) -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester = remember { FocusRequester() }, - toRequestFocus: Boolean = false + toRequestFocus: Boolean = false, ) { LaunchedEffect(toRequestFocus) { if (toRequestFocus) { @@ -163,7 +165,7 @@ private fun Controls( Column( verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.item), - modifier = modifier + modifier = modifier, ) { KeywordInput( keyword = keyword, @@ -173,9 +175,10 @@ private fun Controls( categorySelectionList = categorySelectionList, onCategorySelected = onCategorySelected, onCategoryUnselected = onCategoryUnselected, - modifier = Modifier - .focusRestorer() - .focusRequester(focusRequester) + modifier = + Modifier + .focusRestorer() + .focusRequester(focusRequester), ) } } @@ -186,9 +189,10 @@ private fun KeywordInput( onKeywordInput: (String) -> Unit, modifier: Modifier = Modifier, ) { - val textStyle = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + val textStyle = + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) val cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) BasicTextField( value = keyword, @@ -199,26 +203,27 @@ private fun KeywordInput( keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), decorationBox = { innerTextField -> Box( - modifier = Modifier - .fillMaxWidth() - .background( - MaterialTheme.colorScheme.surfaceVariant, - RoundedCornerShape(percent = 50) - ) + modifier = + Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceVariant, + RoundedCornerShape(percent = 50), + ), ) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Icon( Icons.Default.Search, contentDescription = stringResource(R.string.label_search), - modifier = Modifier.padding(end = 12.dp) + modifier = Modifier.padding(end = 12.dp), ) innerTextField() } } - } + }, ) } @@ -228,7 +233,7 @@ private fun CategorySelection( categorySelectionList: CategorySelectionList, onCategorySelected: (CategoryInfo) -> Unit, onCategoryUnselected: (CategoryInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { FlowRow( modifier = modifier, @@ -244,7 +249,7 @@ private fun CategorySelection( } else { onCategorySelected(it.categoryInfo) } - } + }, ) { Text(text = it.categoryInfo.name) } @@ -262,7 +267,7 @@ private fun SearchResult( TvLazyVerticalGrid( columns = TvGridCells.Fixed(4), horizontalArrangement = - Arrangement.spacedBy(JetcasterAppDefaults.gap.podcastRow), + Arrangement.spacedBy(JetcasterAppDefaults.gap.podcastRow), verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.podcastRow), modifier = modifier, ) { diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt index 94243ab0aa..6a6f0e379d 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt @@ -28,7 +28,6 @@ import com.example.jetcaster.tv.model.CategorySelection import com.example.jetcaster.tv.model.CategorySelectionList import com.example.jetcaster.tv.model.PodcastList import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -37,116 +36,126 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel -class SearchScreenViewModel @Inject constructor( - private val podcastsRepository: PodcastsRepository, - private val podcastStore: PodcastStore, - categoryStore: CategoryStore, -) : ViewModel() { - - private val keywordFlow = MutableStateFlow("") - private val selectedCategoryListFlow = MutableStateFlow>(emptyList()) - - private val categoryInfoListFlow = - categoryStore.categoriesSortedByPodcastCount().map(CategoryInfoList::from) - - private val searchConditionFlow = - combine( - keywordFlow, - selectedCategoryListFlow, - categoryInfoListFlow - ) { keyword, selectedCategories, categories -> - val selected = selectedCategories.ifEmpty { - categories +class SearchScreenViewModel + @Inject + constructor( + private val podcastsRepository: PodcastsRepository, + private val podcastStore: PodcastStore, + categoryStore: CategoryStore, + ) : ViewModel() { + private val keywordFlow = MutableStateFlow("") + private val selectedCategoryListFlow = MutableStateFlow>(emptyList()) + + private val categoryInfoListFlow = + categoryStore.categoriesSortedByPodcastCount().map(CategoryInfoList::from) + + private val searchConditionFlow = + combine( + keywordFlow, + selectedCategoryListFlow, + categoryInfoListFlow, + ) { keyword, selectedCategories, categories -> + val selected = + selectedCategories.ifEmpty { + categories + } + SearchCondition(keyword, selected) } - SearchCondition(keyword, selected) - } - - @OptIn(ExperimentalCoroutinesApi::class) - private val searchResultFlow = searchConditionFlow.flatMapLatest { - podcastStore.searchPodcastByTitleAndCategories( - it.keyword, - it.selectedCategories.intoCategoryList() - ) - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - emptyList() - ) - private val categorySelectionFlow = - combine( - categoryInfoListFlow, - selectedCategoryListFlow - ) { categoryList, selectedCategories -> - val list = categoryList.map { - CategorySelection(it, selectedCategories.contains(it)) + @OptIn(ExperimentalCoroutinesApi::class) + private val searchResultFlow = + searchConditionFlow + .flatMapLatest { + podcastStore.searchPodcastByTitleAndCategories( + it.keyword, + it.selectedCategories.intoCategoryList(), + ) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + emptyList(), + ) + + private val categorySelectionFlow = + combine( + categoryInfoListFlow, + selectedCategoryListFlow, + ) { categoryList, selectedCategories -> + val list = + categoryList.map { + CategorySelection(it, selectedCategories.contains(it)) + } + CategorySelectionList(list) } - CategorySelectionList(list) + + val uiStateFlow = + combine( + keywordFlow, + categorySelectionFlow, + searchResultFlow, + ) { keyword, categorySelection, result -> + val podcastList = PodcastList(result.map { it.asExternalModel() }) + when { + result.isEmpty() -> SearchScreenUiState.Ready(keyword, categorySelection) + else -> SearchScreenUiState.HasResult(keyword, categorySelection, podcastList) + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + SearchScreenUiState.Loading, + ) + + fun setKeyword(keyword: String) { + keywordFlow.value = keyword } - val uiStateFlow = - combine( - keywordFlow, - categorySelectionFlow, - searchResultFlow - ) { keyword, categorySelection, result -> - val podcastList = PodcastList(result.map { it.asExternalModel() }) - when { - result.isEmpty() -> SearchScreenUiState.Ready(keyword, categorySelection) - else -> SearchScreenUiState.HasResult(keyword, categorySelection, podcastList) + fun addCategoryToSelectedCategoryList(category: CategoryInfo) { + val list = selectedCategoryListFlow.value + if (!list.contains(category)) { + selectedCategoryListFlow.value = list + listOf(category) } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - SearchScreenUiState.Loading, - ) - - fun setKeyword(keyword: String) { - keywordFlow.value = keyword - } - - fun addCategoryToSelectedCategoryList(category: CategoryInfo) { - val list = selectedCategoryListFlow.value - if (!list.contains(category)) { - selectedCategoryListFlow.value = list + listOf(category) } - } - fun removeCategoryFromSelectedCategoryList(category: CategoryInfo) { - val list = selectedCategoryListFlow.value - if (list.contains(category)) { - val mutable = list.toMutableList() - mutable.remove(category) - selectedCategoryListFlow.value = mutable.toList() + fun removeCategoryFromSelectedCategoryList(category: CategoryInfo) { + val list = selectedCategoryListFlow.value + if (list.contains(category)) { + val mutable = list.toMutableList() + mutable.remove(category) + selectedCategoryListFlow.value = mutable.toList() + } } - } - init { - viewModelScope.launch { - podcastsRepository.updatePodcasts(false) + init { + viewModelScope.launch { + podcastsRepository.updatePodcasts(false) + } } } -} -private data class SearchCondition(val keyword: String, val selectedCategories: CategoryInfoList) { +private data class SearchCondition( + val keyword: String, + val selectedCategories: CategoryInfoList, +) { constructor(keyword: String, categoryInfoList: List) : this( keyword, - CategoryInfoList(categoryInfoList) + CategoryInfoList(categoryInfoList), ) } sealed interface SearchScreenUiState { data object Loading : SearchScreenUiState + data class Ready( val keyword: String, - val categorySelectionList: CategorySelectionList + val categorySelectionList: CategorySelectionList, ) : SearchScreenUiState data class HasResult( val keyword: String, val categorySelectionList: CategorySelectionList, - val result: PodcastList + val result: PodcastList, ) : SearchScreenUiState } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/settings/SettingsScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/settings/SettingsScreen.kt index 53bf32f50c..c7a54f16d4 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/settings/SettingsScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/settings/SettingsScreen.kt @@ -21,8 +21,6 @@ import androidx.compose.ui.Modifier import com.example.jetcaster.tv.ui.component.NotAvailableFeature @Composable -fun SettingsScreen( - modifier: Modifier = Modifier -) { +fun SettingsScreen(modifier: Modifier = Modifier) { NotAvailableFeature(modifier = modifier) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Color.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Color.kt index e01c77c91b..abbd73386a 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Color.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Color.kt @@ -75,65 +75,67 @@ import com.example.jetcaster.designsystem.theme.tertiaryContainerLight import com.example.jetcaster.designsystem.theme.tertiaryDark import com.example.jetcaster.designsystem.theme.tertiaryLight -val colorSchemeForDarkMode = darkColorScheme( - primary = primaryDark, - onPrimary = onPrimaryDark, - primaryContainer = primaryContainerDark, - onPrimaryContainer = onPrimaryContainerDark, - secondary = secondaryDark, - onSecondary = onSecondaryDark, - secondaryContainer = secondaryContainerDark, - onSecondaryContainer = onSecondaryContainerDark, - tertiary = tertiaryDark, - onTertiary = onTertiaryDark, - tertiaryContainer = tertiaryContainerDark, - onTertiaryContainer = onTertiaryContainerDark, - error = errorDark, - onError = onErrorDark, - background = backgroundDark, - onBackground = onBackgroundDark, - surface = surfaceDark, - onSurface = onSurfaceDark, - surfaceVariant = surfaceVariantDark, - onSurfaceVariant = onSurfaceVariantDark, - border = outlineDark, - borderVariant = outlineVariantDark, - scrim = scrimDark, - inverseSurface = inverseSurfaceDark, - inverseOnSurface = inverseOnSurfaceDark, - inversePrimary = inversePrimaryDark, - errorContainer = errorContainerDark, - onErrorContainer = onErrorContainerDark, -) +val colorSchemeForDarkMode = + darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + border = outlineDark, + borderVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + ) // Todo: specify surfaceTint -val colorSchemeForLightMode = lightColorScheme( - primary = primaryLight, - onPrimary = onPrimaryLight, - primaryContainer = primaryContainerLight, - onPrimaryContainer = onPrimaryContainerLight, - secondary = secondaryLight, - onSecondary = onSecondaryLight, - secondaryContainer = secondaryContainerLight, - onSecondaryContainer = onSecondaryContainerLight, - tertiary = tertiaryLight, - onTertiary = onTertiaryLight, - tertiaryContainer = tertiaryContainerLight, - onTertiaryContainer = onTertiaryContainerLight, - error = errorLight, - onError = onErrorLight, - background = backgroundLight, - onBackground = onBackgroundLight, - surface = surfaceLight, - onSurface = onSurfaceLight, - surfaceVariant = surfaceVariantLight, - onSurfaceVariant = onSurfaceVariantLight, - border = outlineLight, - borderVariant = outlineVariantLight, - scrim = scrimLight, - inverseSurface = inverseSurfaceLight, - inverseOnSurface = inverseOnSurfaceLight, - inversePrimary = inversePrimaryLight, - errorContainer = errorContainerLight, - onErrorContainer = onErrorContainerLight, -) +val colorSchemeForLightMode = + lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + border = outlineLight, + borderVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + ) diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt index def4a37865..2f8e0478be 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt @@ -35,18 +35,20 @@ internal data class OverScanMarginSettings( val catalog: OverScanMargin = OverScanMargin(end = 0.dp), val episode: OverScanMargin = OverScanMargin(start = 80.dp, end = 80.dp), val drawer: OverScanMargin = OverScanMargin(start = 16.dp, end = 16.dp), - val podcast: OverScanMargin = OverScanMargin( - top = 40.dp, - bottom = 40.dp, - start = 80.dp, - end = 80.dp - ), - val player: OverScanMargin = OverScanMargin( - top = 40.dp, - bottom = 40.dp, - start = 80.dp, - end = 80.dp - ), + val podcast: OverScanMargin = + OverScanMargin( + top = 40.dp, + bottom = 40.dp, + start = 80.dp, + end = 80.dp, + ), + val player: OverScanMargin = + OverScanMargin( + top = 40.dp, + bottom = 40.dp, + start = 80.dp, + end = 80.dp, + ), ) internal data class OverScanMargin( @@ -55,21 +57,19 @@ internal data class OverScanMargin( val start: Dp = 48.dp, val end: Dp = 48.dp, ) { - fun intoPaddingValues(): PaddingValues { - return PaddingValues(start, top, end, bottom) - } + fun intoPaddingValues(): PaddingValues = PaddingValues(start, top, end, bottom) } internal data class CardWidth( val large: Dp = 268.dp, val medium: Dp = 196.dp, - val small: Dp = 124.dp + val small: Dp = 124.dp, ) internal data class ThumbnailSize( val episodeDetails: DpSize = DpSize(266.dp, 266.dp), val podcast: DpSize = DpSize(196.dp, 196.dp), - val episode: DpSize = DpSize(124.dp, 124.dp) + val episode: DpSize = DpSize(124.dp, 124.dp), ) internal data class PaddingSettings( @@ -85,7 +85,6 @@ internal data class GapSettings( val default: Dp = small * 2, val medium: Dp = default + tiny, val large: Dp = medium * 2, - val chip: Dp = small, val episodeRow: Dp = medium, val item: Dp = default, @@ -98,13 +97,14 @@ internal data class GapSettings( internal data class IconButtonSize( val default: Radius = Radius(14.dp), val medium: Radius = Radius(20.dp), - val large: Radius = Radius(28.dp) + val large: Radius = Radius(28.dp), ) -internal data class Radius(private val value: Dp) { - private fun diameter(): Dp { - return value * 2 - } +internal data class Radius( + private val value: Dp, +) { + private fun diameter(): Dp = value * 2 + fun intoDpSize(): DpSize { val d = diameter() return DpSize(d, d) diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Theme.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Theme.kt index f895300f78..56d598eefd 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Theme.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Theme.kt @@ -25,14 +25,15 @@ fun JetcasterTheme( isInDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit, ) { - val colorScheme = if (isInDarkTheme) { - colorSchemeForDarkMode - } else { - colorSchemeForLightMode - } + val colorScheme = + if (isInDarkTheme) { + colorSchemeForDarkMode + } else { + colorSchemeForLightMode + } MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Type.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Type.kt index 1be9cc97c1..7e18d940d6 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Type.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Type.kt @@ -23,95 +23,111 @@ import androidx.tv.material3.Typography import com.example.jetcaster.designsystem.theme.Montserrat // Set of Material typography styles to start with -val Typography = Typography( - displayLarge = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 57.sp, - lineHeight = 64.sp, - ), - displayMedium = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 42.sp, - lineHeight = 52.sp, - ), - displaySmall = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 36.sp, - lineHeight = 44.sp, - ), - headlineLarge = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 32.sp, - lineHeight = 40.sp, - ), - headlineMedium = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 28.sp, - lineHeight = 36.sp, - ), - headlineSmall = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 24.sp, - lineHeight = 32.sp, - ), - titleLarge = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - ), - titleMedium = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - ), - titleSmall = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - ), - labelLarge = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - ), - labelMedium = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, - ), - labelSmall = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 11.sp, - lineHeight = 16.sp, - ), - bodyLarge = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - ), - bodyMedium = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - ), - bodySmall = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, +val Typography = + Typography( + displayLarge = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 64.sp, + ), + displayMedium = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 42.sp, + lineHeight = 52.sp, + ), + displaySmall = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + ), + headlineLarge = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 32.sp, + lineHeight = 40.sp, + ), + headlineMedium = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 36.sp, + ), + headlineSmall = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp, + ), + titleLarge = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + ), + titleMedium = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + ), + titleSmall = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + ), + labelLarge = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + ), + labelMedium = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + ), + labelSmall = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 11.sp, + lineHeight = 16.sp, + ), + bodyLarge = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + ), + bodyMedium = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + ), + bodySmall = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + ), ) -) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/JetcasterWearApplication.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/JetcasterWearApplication.kt index a633b967a8..2c629268ea 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/JetcasterWearApplication.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/JetcasterWearApplication.kt @@ -24,8 +24,9 @@ import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject @HiltAndroidApp -class JetcasterWearApplication : Application(), ImageLoaderFactory { - +class JetcasterWearApplication : + Application(), + ImageLoaderFactory { @Inject lateinit var imageLoader: ImageLoader override fun onCreate() { @@ -35,7 +36,8 @@ class JetcasterWearApplication : Application(), ImageLoaderFactory { private fun setStrictMode() { StrictMode.setThreadPolicy( - StrictMode.ThreadPolicy.Builder() + StrictMode.ThreadPolicy + .Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() @@ -44,6 +46,5 @@ class JetcasterWearApplication : Application(), ImageLoaderFactory { ) } - override fun newImageLoader(): ImageLoader = - imageLoader + override fun newImageLoader(): ImageLoader = imageLoader } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt index c44f333f16..352bd5e7e6 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt @@ -59,7 +59,6 @@ import com.google.android.horologist.media.ui.screens.playerlibrarypager.PlayerL @Composable fun WearApp() { - val navController = rememberSwipeDismissableNavController() val navHostState = rememberSwipeDismissableNavHostState() val volumeViewModel: VolumeViewModel = viewModel(factory = VolumeViewModel.Factory) @@ -92,7 +91,7 @@ fun WearApp() { volumeViewModel = volumeViewModel, onVolumeClick = { navController.navigateToVolume() - } + }, ) }, libraryScreen = { @@ -123,13 +122,13 @@ fun WearApp() { onPlayButtonClick = { navController.navigateToPlayer() }, - onDismiss = { navController.popBackStack() } + onDismiss = { navController.popBackStack() }, ) } composable(route = YourPodcasts.navRoute) { PodcastsScreen( onPodcastsItemClick = { navController.navigateToPodcastDetails(it.uri) }, - onDismiss = { navController.popBackStack() } + onDismiss = { navController.popBackStack() }, ) } composable(route = PodcastDetails.navRoute) { @@ -138,7 +137,7 @@ fun WearApp() { navController.navigateToPlayer() }, onEpisodeItemClick = { navController.navigateToEpisode(it.uri) }, - onDismiss = { navController.popBackStack() } + onDismiss = { navController.popBackStack() }, ) } composable(route = UpNext.navRoute) { @@ -150,7 +149,7 @@ fun WearApp() { onDismiss = { navController.popBackStack() navController.navigateToYourPodcast() - } + }, ) } composable(route = Episode.navRoute) { @@ -161,7 +160,7 @@ fun WearApp() { onDismiss = { navController.popBackStack() navController.navigateToYourPodcast() - } + }, ) } } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Color.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Color.kt index 2bb7c18ffa..c75a5bb3c1 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Color.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Color.kt @@ -26,13 +26,14 @@ import com.example.jetcaster.designsystem.theme.primaryDark import com.example.jetcaster.designsystem.theme.secondaryContainerDarkMediumContrast import com.example.jetcaster.designsystem.theme.secondaryDark -internal val wearColorPalette: Colors = Colors( - primary = primaryDark, - primaryVariant = primaryContainerDarkMediumContrast, - secondary = secondaryDark, - secondaryVariant = secondaryContainerDarkMediumContrast, - error = errorDark, - onPrimary = onPrimaryDark, - onSecondary = onSecondaryDark, - onError = onErrorDark -) +internal val wearColorPalette: Colors = + Colors( + primary = primaryDark, + primaryVariant = primaryContainerDarkMediumContrast, + secondary = secondaryDark, + secondaryVariant = secondaryContainerDarkMediumContrast, + error = errorDark, + onPrimary = onPrimaryDark, + onSecondary = onSecondaryDark, + onError = onErrorDark, + ) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Type.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Type.kt index 2ce188903a..c92c659aea 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Type.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Type.kt @@ -23,12 +23,14 @@ import androidx.wear.compose.material.Typography import com.example.jetcaster.designsystem.theme.Montserrat // Set of Material typography styles to start with -val Typography = Typography( - body1 = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Normal, - fontSize = 16.sp - ) +val Typography = + Typography( + body1 = + TextStyle( + fontFamily = Montserrat, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + ), /* Other default text styles to override button = TextStyle( fontFamily = FontFamily.Default, @@ -41,4 +43,4 @@ val Typography = Typography( fontSize = 12.sp ) */ -) + ) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/WearAppTheme.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/WearAppTheme.kt index c797a8c041..b8411cdd27 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/WearAppTheme.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/WearAppTheme.kt @@ -20,14 +20,12 @@ import androidx.compose.runtime.Composable import androidx.wear.compose.material.MaterialTheme @Composable -fun WearAppTheme( - content: @Composable () -> Unit -) { +fun WearAppTheme(content: @Composable () -> Unit) { MaterialTheme( colors = wearColorPalette, typography = Typography, // For shapes, we generally recommend using the default Material Wear shapes which are // optimized for round and non-round devices. - content = content + content = content, ) } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/JetcasterNavController.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/JetcasterNavController.kt index c0246787aa..3bac9a8a9a 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/JetcasterNavController.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/JetcasterNavController.kt @@ -27,7 +27,6 @@ import com.google.android.horologist.media.ui.navigation.NavigationScreens * NavController extensions that links to the screens of the Jetcaster app. */ public object JetcasterNavController { - public fun NavController.navigateToYourPodcast() { navigate(YourPodcasts.destination()) } @@ -63,32 +62,36 @@ public object LatestEpisodes : NavigationScreens("latestEpisodes") { public object PodcastDetails : NavigationScreens("podcast?podcastUri={podcastUri}") { public const val PODCAST_URI: String = "podcastUri" + public fun destination(podcastUri: String): String { val encodedUri = Uri.encode(podcastUri) return "podcast?$PODCAST_URI=$encodedUri" } override val arguments: List - get() = listOf( - navArgument(PODCAST_URI) { - type = NavType.StringType - }, - ) + get() = + listOf( + navArgument(PODCAST_URI) { + type = NavType.StringType + }, + ) } public object Episode : NavigationScreens("episode?episodeUri={episodeUri}") { public const val EPISODE_URI: String = "episodeUri" + public fun destination(episodeUri: String): String { val encodedUri = Uri.encode(episodeUri) return "episode?$EPISODE_URI=$encodedUri" } override val arguments: List - get() = listOf( - navArgument(EPISODE_URI) { - type = NavType.StringType - }, - ) + get() = + listOf( + navArgument(EPISODE_URI) { + type = NavType.StringType + }, + ) } public object UpNext : NavigationScreens("upNext") { diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt index fe8b33c1d7..ae4520a403 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt @@ -33,24 +33,25 @@ fun MediaContent( episode: PlayerEpisode, episodeArtworkPlaceholder: Painter?, onItemClick: (PlayerEpisode) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val mediaTitle = episode.title val duration = episode.duration - val secondaryLabel = when { - duration != null -> { - // If we have the duration, we combine the date/duration via a - // formatted string - stringResource( - R.string.episode_date_duration, - MediumDateFormatter.format(episode.published), - duration.toMinutes().toInt() - ) + val secondaryLabel = + when { + duration != null -> { + // If we have the duration, we combine the date/duration via a + // formatted string + stringResource( + R.string.episode_date_duration, + MediumDateFormatter.format(episode.published), + duration.toMinutes().toInt(), + ) + } + // Otherwise we just use the date + else -> MediumDateFormatter.format(episode.published) } - // Otherwise we just use the date - else -> MediumDateFormatter.format(episode.published) - } Chip( label = mediaTitle, @@ -59,7 +60,7 @@ fun MediaContent( icon = CoilPaintable(episode.podcastImageUrl, episodeArtworkPlaceholder), largeIcon = true, colors = ChipDefaults.secondaryChipColors(), - modifier = modifier + modifier = modifier, ) } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/SettingsButtons.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/SettingsButtons.kt index 91f32debc5..18924bf7ef 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/SettingsButtons.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/SettingsButtons.kt @@ -52,10 +52,13 @@ fun SettingsButtons( horizontalArrangement = Arrangement.SpaceEvenly, ) { PlaybackSpeedButton( - currentPlayerSpeed = playerUiState.episodePlayerState - .playbackSpeed.toMillis().toFloat() / 1000, + currentPlayerSpeed = + playerUiState.episodePlayerState + .playbackSpeed + .toMillis() + .toFloat() / 1000, onPlaybackSpeedChange = onPlaybackSpeedChange, - enabled = enabled + enabled = enabled, ) SettingsButtonsDefaults.BrandIcon( @@ -66,7 +69,7 @@ fun SettingsButtons( SetVolumeButton( onVolumeClick = onVolumeClick, volumeUiState = volumeUiState, - enabled = enabled + enabled = enabled, ) } } @@ -83,11 +86,13 @@ fun PlaybackSpeedButton( onClick = onPlaybackSpeedChange, enabled = enabled, imageVector = - when (currentPlayerSpeed) { - 1f -> ImageVector.vectorResource(R.drawable.speed_1x) - 1.5f -> ImageVector.vectorResource(R.drawable.speed_15x) - else -> { ImageVector.vectorResource(R.drawable.speed_2x) } - }, + when (currentPlayerSpeed) { + 1f -> ImageVector.vectorResource(R.drawable.speed_1x) + 1.5f -> ImageVector.vectorResource(R.drawable.speed_15x) + else -> { + ImageVector.vectorResource(R.drawable.speed_2x) + } + }, iconRtlMode = IconRtlMode.Mirrored, contentDescription = stringResource(R.string.change_playback_speed_content_description), ) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt index 4bb9a901dc..106313f39e 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt @@ -63,7 +63,7 @@ fun EpisodeScreen( onPlayButtonClick: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, - episodeViewModel: EpisodeViewModel = hiltViewModel() + episodeViewModel: EpisodeViewModel = hiltViewModel(), ) { val uiState by episodeViewModel.uiState.collectAsStateWithLifecycle() @@ -86,15 +86,17 @@ fun EpisodeScreen( onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - val columnState = rememberResponsiveColumnState( - contentPadding = padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip + val columnState = + rememberResponsiveColumnState( + contentPadding = + padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) - ) ScreenScaffold( scrollState = columnState, - modifier = modifier + modifier = modifier, ) { when (uiState) { is EpisodeScreenState.Loaded -> { @@ -104,12 +106,12 @@ fun EpisodeScreen( columnState = columnState, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text( text = title, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) } }, @@ -118,13 +120,12 @@ fun EpisodeScreen( episode = uiState.episode, onPlayButtonClick = onPlayButtonClick, onPlayEpisode = onPlayEpisode, - onAddToQueue = onAddToQueue + onAddToQueue = onAddToQueue, ) }, content = { episodeInfoContent(episode = uiState.episode) - } - + }, ) } @@ -132,7 +133,7 @@ fun EpisodeScreen( AlertDialog( showDialog = true, onDismiss = { onDismiss }, - message = stringResource(R.string.episode_info_not_available) + message = stringResource(R.string.episode_info_not_available), ) } EpisodeScreenState.Loading -> { @@ -149,17 +150,16 @@ fun LoadedButtonsContent( onPlayButtonClick: () -> Unit, onPlayEpisode: (PlayerEpisode) -> Unit, onAddToQueue: (PlayerEpisode) -> Unit, - enabled: Boolean = true + enabled: Boolean = true, ) { - Row( - modifier = Modifier - .padding(bottom = 16.dp) - .height(52.dp), + modifier = + Modifier + .padding(bottom = 16.dp) + .height(52.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), ) { - Button( imageVector = Icons.Outlined.PlayArrow, contentDescription = stringResource(id = R.string.button_play_content_description), @@ -168,8 +168,9 @@ fun LoadedButtonsContent( onPlayEpisode(episode.toPlayerEpisode()) }, enabled = enabled, - modifier = Modifier - .weight(weight = 0.3F, fill = false), + modifier = + Modifier + .weight(weight = 0.3F, fill = false), ) Button( @@ -177,18 +178,20 @@ fun LoadedButtonsContent( contentDescription = stringResource(id = R.string.add_to_queue_content_description), onClick = { onAddToQueue(episode.toPlayerEpisode()) }, enabled = enabled, - modifier = Modifier - .weight(weight = 0.3F, fill = false), + modifier = + Modifier + .weight(weight = 0.3F, fill = false), ) } } + @Composable fun LoadingScreen(columnState: ScalingLazyColumnState) { EntityScreen( columnState = columnState, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = stringResource(R.string.loading)) } @@ -200,7 +203,7 @@ fun LoadingScreen(columnState: ScalingLazyColumnState) { items(count = 2) { PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) } - } + }, ) } @@ -208,20 +211,21 @@ fun LoadingScreen(columnState: ScalingLazyColumnState) { @Composable fun LoadingButtonsContent() { Row( - modifier = Modifier - .padding(bottom = 16.dp) - .height(52.dp), + modifier = + Modifier + .padding(bottom = 16.dp) + .height(52.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), ) { - Button( imageVector = Icons.Outlined.PlayArrow, contentDescription = stringResource(id = R.string.button_play_content_description), onClick = {}, enabled = false, - modifier = Modifier - .weight(weight = 0.3F, fill = false), + modifier = + Modifier + .weight(weight = 0.3F, fill = false), ) Button( @@ -229,8 +233,9 @@ fun LoadingButtonsContent() { contentDescription = stringResource(id = R.string.add_to_queue_content_description), onClick = {}, enabled = false, - modifier = Modifier - .weight(weight = 0.3F, fill = false), + modifier = + Modifier + .weight(weight = 0.3F, fill = false), ) } } @@ -247,31 +252,33 @@ private fun ScalingLazyListScope.episodeInfoContent(episode: EpisodeToPodcast) { text = author, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.body2 + style = MaterialTheme.typography.body2, ) } } item { Text( - text = when { - duration != null -> { - // If we have the duration, we combine the date/duration via a - // formatted string - stringResource( - R.string.episode_date_duration, - MediumDateFormatter.format(published), - duration.toMinutes().toInt() - ) - } - // Otherwise we just use the date - else -> MediumDateFormatter.format(published) - }, + text = + when { + duration != null -> { + // If we have the duration, we combine the date/duration via a + // formatted string + stringResource( + R.string.episode_date_duration, + MediumDateFormatter.format(published), + duration.toMinutes().toInt(), + ) + } + // Otherwise we just use the date + else -> MediumDateFormatter.format(published) + }, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.body2, - modifier = Modifier - .padding(horizontal = 8.dp) + modifier = + Modifier + .padding(horizontal = 8.dp), ) } if (summary != null) { @@ -282,7 +289,7 @@ private fun ScalingLazyListScope.episodeInfoContent(episode: EpisodeToPodcast) { text = it, style = MaterialTheme.typography.body2, color = LocalContentColor.current, - modifier = Modifier.listTextPadding() + modifier = Modifier.listTextPadding(), ) } } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeViewModel.kt index 1381462913..1e08f906e0 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeViewModel.kt @@ -43,67 +43,70 @@ import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.ui.Episode import com.google.android.horologist.annotations.ExperimentalHorologistApi import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject /** * ViewModel that handles the business logic and screen state of the Episode screen. */ @HiltViewModel -class EpisodeViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - episodeStore: EpisodeStore, - private val episodePlayer: EpisodePlayer, -) : ViewModel() { +class EpisodeViewModel + @Inject + constructor( + savedStateHandle: SavedStateHandle, + episodeStore: EpisodeStore, + private val episodePlayer: EpisodePlayer, + ) : ViewModel() { + private val episodeUri: String = + savedStateHandle.get(Episode.EPISODE_URI).let { + Uri.decode(it) + } - private val episodeUri: String = - savedStateHandle.get(Episode.EPISODE_URI).let { - Uri.decode(it) - } + private val episodeFlow = + if (episodeUri != null) { + episodeStore.episodeAndPodcastWithUri(episodeUri) + } else { + flowOf(null) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + null, + ) - private val episodeFlow = if (episodeUri != null) { - episodeStore.episodeAndPodcastWithUri(episodeUri) - } else { - flowOf(null) - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - null - ) + val uiState: StateFlow = + episodeFlow + .map { + if (it != null) { + EpisodeScreenState.Loaded(it) + } else { + EpisodeScreenState.Empty + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + EpisodeScreenState.Loading, + ) - val uiState: StateFlow = - episodeFlow.map { - if (it != null) { - EpisodeScreenState.Loaded(it) - } else { - EpisodeScreenState.Empty - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5000), - EpisodeScreenState.Loading, - ) + fun onPlayEpisode(episode: PlayerEpisode) { + episodePlayer.currentEpisode = episode + episodePlayer.play() + } - fun onPlayEpisode(episode: PlayerEpisode) { - episodePlayer.currentEpisode = episode - episodePlayer.play() - } - fun addToQueue(episode: PlayerEpisode) { - episodePlayer.addToQueue(episode) + fun addToQueue(episode: PlayerEpisode) { + episodePlayer.addToQueue(episode) + } } -} @ExperimentalHorologistApi sealed interface EpisodeScreenState { - data object Loading : EpisodeScreenState data class Loaded( - val episode: EpisodeToPodcast + val episode: EpisodeToPodcast, ) : EpisodeScreenState data object Empty : EpisodeScreenState diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodeViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latestepisodes/LatestEpisodeViewModel.kt similarity index 52% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodeViewModel.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latestepisodes/LatestEpisodeViewModel.kt index cc78891709..785ed255f2 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodeViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latestepisodes/LatestEpisodeViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.latest_episodes +package com.example.jetcaster.ui.latestepisodes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -23,52 +23,54 @@ import com.example.jetcaster.core.player.EpisodePlayer import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.core.player.model.toPlayerEpisode import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject @HiltViewModel -class LatestEpisodeViewModel @Inject constructor( - episodesFromFavouritePodcasts: GetLatestFollowedEpisodesUseCase, - private val episodePlayer: EpisodePlayer, -) : ViewModel() { - - val uiState: StateFlow = - episodesFromFavouritePodcasts.invoke().map { episodeToPodcastList -> - if (episodeToPodcastList.isNotEmpty()) { - LatestEpisodeScreenState.Loaded( - episodeToPodcastList.map { - it.toPlayerEpisode() +class LatestEpisodeViewModel + @Inject + constructor( + episodesFromFavouritePodcasts: GetLatestFollowedEpisodesUseCase, + private val episodePlayer: EpisodePlayer, + ) : ViewModel() { + val uiState: StateFlow = + episodesFromFavouritePodcasts + .invoke() + .map { episodeToPodcastList -> + if (episodeToPodcastList.isNotEmpty()) { + LatestEpisodeScreenState.Loaded( + episodeToPodcastList.map { + it.toPlayerEpisode() + }, + ) + } else { + LatestEpisodeScreenState.Empty } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + LatestEpisodeScreenState.Loading, ) - } else { - LatestEpisodeScreenState.Empty - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5000), - LatestEpisodeScreenState.Loading, - ) - fun onPlayEpisodes(episodes: List) { - episodePlayer.currentEpisode = episodes[0] - episodePlayer.play(episodes) - } + fun onPlayEpisodes(episodes: List) { + episodePlayer.currentEpisode = episodes[0] + episodePlayer.play(episodes) + } - fun onPlayEpisode(episode: PlayerEpisode) { - episodePlayer.currentEpisode = episode - episodePlayer.play() + fun onPlayEpisode(episode: PlayerEpisode) { + episodePlayer.currentEpisode = episode + episodePlayer.play() + } } -} sealed interface LatestEpisodeScreenState { - data object Loading : LatestEpisodeScreenState data class Loaded( - val episodeList: List + val episodeList: List, ) : LatestEpisodeScreenState data object Empty : LatestEpisodeScreenState diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latestepisodes/LatestEpisodesScreen.kt similarity index 82% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latestepisodes/LatestEpisodesScreen.kt index 11c0387c5d..1fc062dbf4 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latestepisodes/LatestEpisodesScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.latest_episodes +package com.example.jetcaster.ui.latestepisodes import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -56,7 +56,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen onPlayButtonClick: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, - latestEpisodeViewModel: LatestEpisodeViewModel = hiltViewModel() + latestEpisodeViewModel: LatestEpisodeViewModel = hiltViewModel(), ) { val uiState by latestEpisodeViewModel.uiState.collectAsStateWithLifecycle() LatestEpisodeScreen( @@ -65,7 +65,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen onPlayButtonClick = onPlayButtonClick, onDismiss = onDismiss, onPlayEpisodes = latestEpisodeViewModel::onPlayEpisodes, - onPlayEpisode = latestEpisodeViewModel::onPlayEpisode + onPlayEpisode = latestEpisodeViewModel::onPlayEpisode, ) } @@ -78,15 +78,17 @@ fun LatestEpisodeScreen( onPlayEpisode: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, ) { - val columnState = rememberResponsiveColumnState( - contentPadding = padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip + val columnState = + rememberResponsiveColumnState( + contentPadding = + padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) - ) ScreenScaffold( scrollState = columnState, - modifier = modifier + modifier = modifier, ) { when (uiState) { is LatestEpisodeScreenState.Loaded -> { @@ -96,7 +98,7 @@ fun LatestEpisodeScreen( onPlayButtonClick = onPlayButtonClick, onPlayEpisode = onPlayEpisode, onPlayEpisodes = onPlayEpisodes, - modifier = modifier + modifier = modifier, ) } @@ -104,14 +106,14 @@ fun LatestEpisodeScreen( AlertDialog( showDialog = true, onDismiss = onDismiss, - message = stringResource(R.string.podcasts_no_episode_podcasts) + message = stringResource(R.string.podcasts_no_episode_podcasts), ) } is LatestEpisodeScreenState.Loading -> { LatestEpisodesScreenLoading( columnState = columnState, - modifier = modifier + modifier = modifier, ) } } @@ -124,7 +126,7 @@ fun ButtonsContent( episodes: List, onPlayButtonClick: () -> Unit, onPlayEpisodes: (List) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Chip( label = stringResource(id = R.string.button_play_content_description), @@ -144,30 +146,31 @@ fun LatestEpisodesScreen( onPlayButtonClick: () -> Unit, onPlayEpisode: (PlayerEpisode) -> Unit, onPlayEpisodes: (List) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { EntityScreen( modifier = modifier, columnState = columnState, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { - Text(text = stringResource(id = R.string.latest_episodes),) + Text(text = stringResource(id = R.string.latest_episodes)) } }, content = { items(count = episodeList.size) { index -> MediaContent( episode = episodeList[index], - episodeArtworkPlaceholder = rememberVectorPainter( - image = Icons.Default.MusicNote, - tintColor = Color.Blue, - ), + episodeArtworkPlaceholder = + rememberVectorPainter( + image = Icons.Default.MusicNote, + tintColor = Color.Blue, + ), onItemClick = { onPlayButtonClick() onPlayEpisode(episodeList[index]) - } + }, ) } }, @@ -175,7 +178,7 @@ fun LatestEpisodesScreen( ButtonsContent( episodes = episodeList, onPlayButtonClick = onPlayButtonClick, - onPlayEpisodes = onPlayEpisodes + onPlayEpisodes = onPlayEpisodes, ) }, ) @@ -184,16 +187,16 @@ fun LatestEpisodesScreen( @Composable fun LatestEpisodesScreenLoading( columnState: ScalingLazyColumnState, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { EntityScreen( modifier = modifier, columnState = columnState, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { - Text(text = stringResource(id = R.string.latest_episodes),) + Text(text = stringResource(id = R.string.latest_episodes)) } }, content = { @@ -216,20 +219,22 @@ fun LatestEpisodesScreenLoading( @Composable fun LatestEpisodeScreenLoadedPreview( @PreviewParameter(WearPreviewEpisodes::class) - episode: PlayerEpisode + episode: PlayerEpisode, ) { - val columnState = rememberResponsiveColumnState( - contentPadding = padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip + val columnState = + rememberResponsiveColumnState( + contentPadding = + padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) - ) LatestEpisodesScreen( columnState = columnState, episodeList = listOf(episode), onPlayButtonClick = { }, onPlayEpisode = { }, - onPlayEpisodes = { } + onPlayEpisodes = { }, ) } @@ -237,12 +242,14 @@ fun LatestEpisodeScreenLoadedPreview( @WearPreviewFontScales @Composable fun LatestEpisodeScreenLoadingPreview() { - val columnState = rememberResponsiveColumnState( - contentPadding = padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip + val columnState = + rememberResponsiveColumnState( + contentPadding = + padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) - ) LatestEpisodesScreenLoading( columnState = columnState, ) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt index 4ee8eab90b..f636d0e875 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt @@ -57,29 +57,31 @@ fun LibraryScreen( onYourPodcastClick: () -> Unit, onUpNextClick: () -> Unit, modifier: Modifier = Modifier, - libraryScreenViewModel: LibraryViewModel = hiltViewModel() + libraryScreenViewModel: LibraryViewModel = hiltViewModel(), ) { val uiState by libraryScreenViewModel.uiState.collectAsState() - val columnState = rememberResponsiveColumnState( - contentPadding = ScalingLazyColumnDefaults.padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip, - ), - ) + val columnState = + rememberResponsiveColumnState( + contentPadding = + ScalingLazyColumnDefaults.padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), + ) when (val s = uiState) { is LibraryScreenUiState.Loading -> LoadingScreen( columnState = columnState, - modifier = modifier + modifier = modifier, ) is LibraryScreenUiState.NoSubscribedPodcast -> NoSubscribedPodcastScreen( columnState = columnState, modifier = modifier, topPodcasts = s.topPodcasts, - onTogglePodcastFollowed = libraryScreenViewModel::onTogglePodcastFollowed + onTogglePodcastFollowed = libraryScreenViewModel::onTogglePodcastFollowed, ) is LibraryScreenUiState.Ready -> @@ -89,7 +91,7 @@ fun LibraryScreen( onLatestEpisodeClick = onLatestEpisodeClick, onYourPodcastClick = onYourPodcastClick, onUpNextClick = onUpNextClick, - queue = s.queue + queue = s.queue, ) } } @@ -103,7 +105,7 @@ fun LoadingScreen( columnState = columnState, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = stringResource(R.string.loading)) } @@ -113,7 +115,7 @@ fun LoadingScreen( items(count = 2) { PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) } - } + }, ) } @@ -122,14 +124,14 @@ fun NoSubscribedPodcastScreen( columnState: ScalingLazyColumnState, modifier: Modifier, topPodcasts: List, - onTogglePodcastFollowed: (uri: String) -> Unit + onTogglePodcastFollowed: (uri: String) -> Unit, ) { ScreenScaffold(scrollState = columnState, modifier = modifier) { ScalingLazyColumn(columnState = columnState) { item { ResponsiveListHeader( modifier = modifier.listTextPadding(), - contentColor = MaterialTheme.colors.onSurface + contentColor = MaterialTheme.colors.onSurface, ) { Text(stringResource(R.string.entity_no_featured_podcasts)) } @@ -138,10 +140,11 @@ fun NoSubscribedPodcastScreen( items(topPodcasts.take(3)) { podcast -> PodcastContent( podcast = podcast, - downloadItemArtworkPlaceholder = rememberVectorPainter( - image = Icons.Default.MusicNote, - tintColor = Color.Blue, - ), + downloadItemArtworkPlaceholder = + rememberVectorPainter( + image = Icons.Default.MusicNote, + tintColor = Color.Blue, + ), onClick = { onTogglePodcastFollowed(podcast.uri) }, @@ -151,7 +154,7 @@ fun NoSubscribedPodcastScreen( item { PlaceholderChip( contentDescription = "", - colors = ChipDefaults.secondaryChipColors() + colors = ChipDefaults.secondaryChipColors(), ) } } @@ -185,7 +188,7 @@ fun LibraryScreen( onLatestEpisodeClick: () -> Unit, onYourPodcastClick: () -> Unit, onUpNextClick: () -> Unit, - queue: List + queue: List, ) { ScreenScaffold(scrollState = columnState, modifier = modifier) { ScalingLazyColumn(columnState = columnState) { @@ -199,7 +202,7 @@ fun LibraryScreen( label = stringResource(R.string.latest_episodes), onClick = onLatestEpisodeClick, icon = DrawableResPaintable(R.drawable.new_releases), - colors = ChipDefaults.secondaryChipColors() + colors = ChipDefaults.secondaryChipColors(), ) } item { @@ -207,7 +210,7 @@ fun LibraryScreen( label = stringResource(R.string.podcasts), onClick = onYourPodcastClick, icon = DrawableResPaintable(R.drawable.podcast), - colors = ChipDefaults.secondaryChipColors() + colors = ChipDefaults.secondaryChipColors(), ) } item { @@ -223,7 +226,7 @@ fun LibraryScreen( label = stringResource(R.string.up_next), onClick = onUpNextClick, icon = DrawableResPaintable(R.drawable.up_next), - colors = ChipDefaults.secondaryChipColors() + colors = ChipDefaults.secondaryChipColors(), ) } } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt index 27ae2e85e1..dee2e70987 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt @@ -47,7 +47,6 @@ import com.example.jetcaster.core.player.EpisodePlayer import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.core.player.model.toPlayerEpisode import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -56,88 +55,95 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel -class LibraryViewModel @Inject constructor( - private val podcastsRepository: PodcastsRepository, - private val episodeStore: EpisodeStore, - private val podcastStore: PodcastStore, - private val episodePlayer: EpisodePlayer, - private val categoryStore: CategoryStore, - private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase -) : ViewModel() { - - private val defaultCategory = categoryStore.getCategory(CategoryTechnology) - private val topPodcastsFlow = defaultCategory.flatMapLatest { - podcastCategoryFilterUseCase(it?.asExternalModel()) - } +class LibraryViewModel + @Inject + constructor( + private val podcastsRepository: PodcastsRepository, + private val episodeStore: EpisodeStore, + private val podcastStore: PodcastStore, + private val episodePlayer: EpisodePlayer, + private val categoryStore: CategoryStore, + private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase, + ) : ViewModel() { + private val defaultCategory = categoryStore.getCategory(CategoryTechnology) + private val topPodcastsFlow = + defaultCategory.flatMapLatest { + podcastCategoryFilterUseCase(it?.asExternalModel()) + } - private val followingPodcastListFlow = podcastStore.followedPodcastsSortedByLastEpisode() + private val followingPodcastListFlow = podcastStore.followedPodcastsSortedByLastEpisode() - private val queue = episodePlayer.playerState.map { - it.queue - } + private val queue = + episodePlayer.playerState.map { + it.queue + } - @OptIn(ExperimentalCoroutinesApi::class) - private val latestEpisodeListFlow = podcastStore - .followedPodcastsSortedByLastEpisode() - .flatMapLatest { podcastList -> - if (podcastList.isNotEmpty()) { - combine(podcastList.map { episodeStore.episodesInPodcast(it.podcast.uri, 1) }) { - it.map { episodes -> - episodes.first() + @OptIn(ExperimentalCoroutinesApi::class) + private val latestEpisodeListFlow = + podcastStore + .followedPodcastsSortedByLastEpisode() + .flatMapLatest { podcastList -> + if (podcastList.isNotEmpty()) { + combine(podcastList.map { episodeStore.episodesInPodcast(it.podcast.uri, 1) }) { + it.map { episodes -> + episodes.first() + } + } + } else { + flowOf(emptyList()) } + }.map { list -> + (list.map { it.toPlayerEpisode() }) } - } else { - flowOf(emptyList()) - } - }.map { list -> - (list.map { it.toPlayerEpisode() }) - } - val uiState = - combine( - topPodcastsFlow, - followingPodcastListFlow, - latestEpisodeListFlow, - queue - ) { topPodcasts, podcastList, episodeList, queue -> - if (podcastList.isEmpty()) { - LibraryScreenUiState.NoSubscribedPodcast(topPodcasts.topPodcasts) - } else { - LibraryScreenUiState.Ready(podcastList, episodeList, queue) - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - LibraryScreenUiState.Loading - ) + val uiState = + combine( + topPodcastsFlow, + followingPodcastListFlow, + latestEpisodeListFlow, + queue, + ) { topPodcasts, podcastList, episodeList, queue -> + if (podcastList.isEmpty()) { + LibraryScreenUiState.NoSubscribedPodcast(topPodcasts.topPodcasts) + } else { + LibraryScreenUiState.Ready(podcastList, episodeList, queue) + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + LibraryScreenUiState.Loading, + ) - init { - viewModelScope.launch { - podcastsRepository.updatePodcasts(false) + init { + viewModelScope.launch { + podcastsRepository.updatePodcasts(false) + } } - } - fun playEpisode(playerEpisode: PlayerEpisode) { - episodePlayer.play(playerEpisode) - } + fun playEpisode(playerEpisode: PlayerEpisode) { + episodePlayer.play(playerEpisode) + } - fun onTogglePodcastFollowed(podcastUri: String) { - viewModelScope.launch { - podcastStore.togglePodcastFollowed(podcastUri) + fun onTogglePodcastFollowed(podcastUri: String) { + viewModelScope.launch { + podcastStore.togglePodcastFollowed(podcastUri) + } } } -} sealed interface LibraryScreenUiState { data object Loading : LibraryScreenUiState + data class NoSubscribedPodcast( - val topPodcasts: List + val topPodcasts: List, ) : LibraryScreenUiState + data class Ready( val subscribedPodcastList: List, val latestEpisodeList: List, - val queue: List + val queue: List, ) : LibraryScreenUiState } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt index dbcd235840..743e251299 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt @@ -70,7 +70,7 @@ fun PlayerScreen( volumeUiState = volumeUiState, onVolumeClick = onVolumeClick, onUpdateVolume = { newVolume -> volumeViewModel.setVolume(newVolume) }, - modifier = modifier + modifier = modifier, ) } @@ -92,7 +92,7 @@ private fun PlayerScreen( mediaDisplay = { TextMediaDisplay( title = stringResource(R.string.nothing_playing), - subtitle = "" + subtitle = "", ) }, controlButtons = { @@ -104,7 +104,7 @@ private fun PlayerScreen( onSeekBackButtonClick = playerScreenViewModel::onRewindBy, seekBackButtonEnabled = false, onSeekForwardButtonClick = playerScreenViewModel::onAdvanceBy, - seekForwardButtonEnabled = false + seekForwardButtonEnabled = false, ) }, buttons = { @@ -129,16 +129,15 @@ private fun PlayerScreen( if (episode != null && episode.title.isNotEmpty()) { TextMediaDisplay( title = episode.podcastName, - subtitle = episode.title + subtitle = episode.title, ) } else { TextMediaDisplay( title = stringResource(R.string.nothing_playing), - subtitle = "" + subtitle = "", ) } }, - controlButtons = { PodcastControlButtons( onPlayButtonClick = playerScreenViewModel::onPlay, @@ -151,7 +150,7 @@ private fun PlayerScreen( seekForwardButtonEnabled = true, seekBackButtonIncrement = SeekButtonIncrement.Ten, seekForwardButtonIncrement = SeekButtonIncrement.Ten, - trackPositionUiModel = state.playerState.trackPositionUiModel + trackPositionUiModel = state.playerState.trackPositionUiModel, ) }, buttons = { @@ -163,21 +162,22 @@ private fun PlayerScreen( enabled = true, ) }, - modifier = modifier - .rotary( - volumeRotaryBehavior( - volumeUiStateProvider = { volumeUiState }, - onRotaryVolumeInput = { onUpdateVolume }, + modifier = + modifier + .rotary( + volumeRotaryBehavior( + volumeUiStateProvider = { volumeUiState }, + onRotaryVolumeInput = { onUpdateVolume }, + ), + focusRequester = rememberActiveFocusRequester(), ), - focusRequester = rememberActiveFocusRequester(), - ), background = { ArtworkColorBackground( paintable = episode?.let { CoilPaintable(episode.podcastImageUrl) }, defaultColor = MaterialTheme.colors.primary, modifier = Modifier.fillMaxSize(), ) - } + }, ) } } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt index c640b92021..c675e560cf 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt @@ -23,17 +23,17 @@ import com.example.jetcaster.core.player.EpisodePlayerState import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.media.ui.state.model.TrackPositionUiModel import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.Duration -import javax.inject.Inject -import kotlin.time.toKotlinDuration import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import java.time.Duration +import javax.inject.Inject +import kotlin.time.toKotlinDuration @OptIn(ExperimentalHorologistApi::class) data class PlayerUiState( val episodePlayerState: EpisodePlayerState = EpisodePlayerState(), - var trackPositionUiModel: TrackPositionUiModel = TrackPositionUiModel.Actual.ZERO + var trackPositionUiModel: TrackPositionUiModel = TrackPositionUiModel.Actual.ZERO, ) /** @@ -41,67 +41,75 @@ data class PlayerUiState( */ @HiltViewModel @OptIn(ExperimentalHorologistApi::class) -class PlayerViewModel @Inject constructor( - private val episodePlayer: EpisodePlayer, -) : ViewModel() { +class PlayerViewModel + @Inject + constructor( + private val episodePlayer: EpisodePlayer, + ) : ViewModel() { + val uiState = + episodePlayer.playerState + .map { + if (it.currentEpisode == null && it.queue.isEmpty()) { + PlayerScreenUiState.Empty + } else { + PlayerScreenUiState.Ready(PlayerUiState(it, buildPositionModel(it))) + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + PlayerScreenUiState.Loading, + ) - val uiState = episodePlayer.playerState.map { - if (it.currentEpisode == null && it.queue.isEmpty()) { - PlayerScreenUiState.Empty - } else { - PlayerScreenUiState.Ready(PlayerUiState(it, buildPositionModel(it))) - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - PlayerScreenUiState.Loading - ) + private fun buildPositionModel(it: EpisodePlayerState) = + if (it.currentEpisode != null) { + TrackPositionUiModel.Actual( + percent = + it.timeElapsed.toMillis().toFloat() / + ( + it.currentEpisode + ?.duration + ?.toMillis() + ?.toFloat() ?: 0f + ), + duration = + it.currentEpisode?.duration?.toKotlinDuration() + ?: Duration.ZERO.toKotlinDuration(), + position = it.timeElapsed.toKotlinDuration(), + ) + } else { + TrackPositionUiModel.Actual.ZERO + } - private fun buildPositionModel(it: EpisodePlayerState) = - if (it.currentEpisode != null) { - TrackPositionUiModel.Actual( - percent = it.timeElapsed.toMillis().toFloat() / - ( - it.currentEpisode?.duration?.toMillis() - ?.toFloat() ?: 0f - ), - duration = it.currentEpisode?.duration?.toKotlinDuration() - ?: Duration.ZERO.toKotlinDuration(), - position = it.timeElapsed.toKotlinDuration() - ) - } else { - TrackPositionUiModel.Actual.ZERO + fun onPlay() { + episodePlayer.play() } - fun onPlay() { - episodePlayer.play() - } - - fun onPause() { - episodePlayer.pause() - } + fun onPause() { + episodePlayer.pause() + } - fun onAdvanceBy() { - episodePlayer.advanceBy(Duration.ofSeconds(10)) - } + fun onAdvanceBy() { + episodePlayer.advanceBy(Duration.ofSeconds(10)) + } - fun onRewindBy() { - episodePlayer.rewindBy(Duration.ofSeconds(10)) - } + fun onRewindBy() { + episodePlayer.rewindBy(Duration.ofSeconds(10)) + } - fun onPlaybackSpeedChange() { - if (episodePlayer.playerState.value.playbackSpeed == Duration.ofSeconds(2)) { - episodePlayer.decreaseSpeed(speed = Duration.ofMillis(1000)) - } else { - episodePlayer.increaseSpeed() + fun onPlaybackSpeedChange() { + if (episodePlayer.playerState.value.playbackSpeed == Duration.ofSeconds(2)) { + episodePlayer.decreaseSpeed(speed = Duration.ofMillis(1000)) + } else { + episodePlayer.increaseSpeed() + } } } -} sealed class PlayerScreenUiState { data object Loading : PlayerScreenUiState() + data class Ready( - val playerState: PlayerUiState + val playerState: PlayerUiState, ) : PlayerScreenUiState() data object Empty : PlayerScreenUiState() diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt index 53afd31f67..667e5ed21a 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt @@ -59,7 +59,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen onEpisodeItemClick: (PlayerEpisode) -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, - podcastDetailsViewModel: PodcastDetailsViewModel = hiltViewModel() + podcastDetailsViewModel: PodcastDetailsViewModel = hiltViewModel(), ) { val uiState by podcastDetailsViewModel.uiState.collectAsStateWithLifecycle() @@ -80,17 +80,19 @@ fun PodcastDetailsScreen( modifier: Modifier = Modifier, onEpisodeItemClick: (PlayerEpisode) -> Unit, onPlayEpisode: (List) -> Unit, - onDismiss: () -> Unit + onDismiss: () -> Unit, ) { - val columnState = rememberResponsiveColumnState( - contentPadding = padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip + val columnState = + rememberResponsiveColumnState( + contentPadding = + padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) - ) ScreenScaffold( scrollState = columnState, - modifier = modifier + modifier = modifier, ) { when (uiState) { is PodcastDetailsScreenState.Loaded -> { @@ -98,7 +100,7 @@ fun PodcastDetailsScreen( columnState = columnState, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = uiState.podcast.title) } @@ -107,21 +109,22 @@ fun PodcastDetailsScreen( ButtonsContent( episodes = uiState.episodeList, onPlayButtonClick = onPlayButtonClick, - onPlayEpisode = onPlayEpisode + onPlayEpisode = onPlayEpisode, ) }, content = { items(uiState.episodeList) { episode -> MediaContent( episode = episode, - episodeArtworkPlaceholder = rememberVectorPainter( - image = Icons.Default.MusicNote, - tintColor = Color.Blue, - ), - onEpisodeItemClick + episodeArtworkPlaceholder = + rememberVectorPainter( + image = Icons.Default.MusicNote, + tintColor = Color.Blue, + ), + onEpisodeItemClick, ) } - } + }, ) } @@ -129,7 +132,7 @@ fun PodcastDetailsScreen( AlertDialog( showDialog = true, onDismiss = { onDismiss }, - message = stringResource(R.string.podcasts_no_episode_podcasts) + message = stringResource(R.string.podcasts_no_episode_podcasts), ) } PodcastDetailsScreenState.Loading -> { @@ -137,7 +140,7 @@ fun PodcastDetailsScreen( columnState = columnState, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = stringResource(id = R.string.loading)) } @@ -146,14 +149,14 @@ fun PodcastDetailsScreen( ButtonsContent( episodes = emptyList(), onPlayButtonClick = { }, - onPlayEpisode = { } + onPlayEpisode = { }, ) }, content = { items(count = 2) { PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) } - } + }, ) } } @@ -167,7 +170,6 @@ fun ButtonsContent( onPlayButtonClick: () -> Unit, onPlayEpisode: (List) -> Unit, ) { - Chip( label = stringResource(id = R.string.button_play_content_description), onClick = { @@ -181,7 +183,6 @@ fun ButtonsContent( @ExperimentalHorologistApi sealed class PodcastDetailsScreenState { - data object Loading : PodcastDetailsScreenState() data class Loaded( @@ -197,17 +198,18 @@ sealed class PodcastDetailsScreenState { @Composable fun PodcastDetailsScreenLoadedPreview( @PreviewParameter(WearPreviewEpisodes::class) - episode: PlayerEpisode + episode: PlayerEpisode, ) { PodcastDetailsScreen( - uiState = PodcastDetailsScreenState.Loaded( - episodeList = listOf(episode), - podcast = PreviewPodcastEpisodes.first().podcast - ), + uiState = + PodcastDetailsScreenState.Loaded( + episodeList = listOf(episode), + podcast = PreviewPodcastEpisodes.first().podcast, + ), onPlayButtonClick = { }, onEpisodeItemClick = {}, onPlayEpisode = {}, - onDismiss = {} + onDismiss = {}, ) } @@ -216,13 +218,13 @@ fun PodcastDetailsScreenLoadedPreview( @Composable fun PodcastDetailsScreenLoadingPreview( @PreviewParameter(WearPreviewEpisodes::class) - episode: PlayerEpisode + episode: PlayerEpisode, ) { PodcastDetailsScreen( uiState = PodcastDetailsScreenState.Loading, onPlayButtonClick = { }, onEpisodeItemClick = {}, onPlayEpisode = {}, - onDismiss = {} + onDismiss = {}, ) } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt index b8c778044a..4a41985844 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt @@ -44,7 +44,6 @@ import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.core.player.model.toPlayerEpisode import com.example.jetcaster.ui.PodcastDetails import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -52,65 +51,72 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject /** * ViewModel that handles the business logic and screen state of the Podcast details screen. */ @HiltViewModel -class PodcastDetailsViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - episodeStore: EpisodeStore, - private val episodePlayer: EpisodePlayer, - podcastStore: PodcastStore -) : ViewModel() { +class PodcastDetailsViewModel + @Inject + constructor( + savedStateHandle: SavedStateHandle, + episodeStore: EpisodeStore, + private val episodePlayer: EpisodePlayer, + podcastStore: PodcastStore, + ) : ViewModel() { + private val podcastUri: String = + savedStateHandle.get(PodcastDetails.PODCAST_URI).let { + Uri.decode(it) + } - private val podcastUri: String = - savedStateHandle.get(PodcastDetails.PODCAST_URI).let { - Uri.decode(it) - } + private val podcastFlow = + if (podcastUri != null) { + podcastStore.podcastWithExtraInfo(podcastUri) + } else { + flowOf(null) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + null, + ) - private val podcastFlow = if (podcastUri != null) { - podcastStore.podcastWithExtraInfo(podcastUri) - } else { - flowOf(null) - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - null - ) + private val episodeListFlow = + podcastFlow + .flatMapLatest { + if (it != null) { + episodeStore.episodesInPodcast(it.podcast.uri) + } else { + flowOf(emptyList()) + } + }.map { list -> + list.map { it.toPlayerEpisode() } + } - private val episodeListFlow = podcastFlow.flatMapLatest { - if (it != null) { - episodeStore.episodesInPodcast(it.podcast.uri) - } else { - flowOf(emptyList()) - } - }.map { list -> - list.map { it.toPlayerEpisode() } - } + val uiState: StateFlow = + combine( + podcastFlow, + episodeListFlow, + ) { podcast, episodes -> + if (podcast != null) { + PodcastDetailsScreenState.Loaded( + podcast = + podcast.podcast + .asExternalModel() + .copy(isSubscribed = podcast.isFollowed), + episodeList = episodes, + ) + } else { + PodcastDetailsScreenState.Empty + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + PodcastDetailsScreenState.Loading, + ) - val uiState: StateFlow = - combine( - podcastFlow, - episodeListFlow - ) { podcast, episodes -> - if (podcast != null) { - PodcastDetailsScreenState.Loaded( - podcast = podcast.podcast.asExternalModel() - .copy(isSubscribed = podcast.isFollowed), - episodeList = episodes, - ) - } else { - PodcastDetailsScreenState.Empty - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5000), - PodcastDetailsScreenState.Loading, - ) - - fun onPlayEpisodes(episodes: List) { - episodePlayer.currentEpisode = episodes[0] - episodePlayer.play(episodes) + fun onPlayEpisodes(episodes: List) { + episodePlayer.currentEpisode = episodes[0] + episodePlayer.play(episodes) + } } -} diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt index f278424b92..72d58e878b 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt @@ -57,25 +57,27 @@ fun PodcastsScreen( ) { val uiState by podcastsViewModel.uiState.collectAsStateWithLifecycle() - val modifiedState = when (uiState) { - is PodcastsScreenState.Loaded -> { - val modifiedPodcast = (uiState as PodcastsScreenState.Loaded).podcastList.map { - it.takeIf { it.title.isNotEmpty() } - ?: it.copy(title = stringResource(id = R.string.no_title)) + val modifiedState = + when (uiState) { + is PodcastsScreenState.Loaded -> { + val modifiedPodcast = + (uiState as PodcastsScreenState.Loaded).podcastList.map { + it.takeIf { it.title.isNotEmpty() } + ?: it.copy(title = stringResource(id = R.string.no_title)) + } + + PodcastsScreenState.Loaded(modifiedPodcast) } - PodcastsScreenState.Loaded(modifiedPodcast) + PodcastsScreenState.Empty, + PodcastsScreenState.Loading, + -> uiState } - PodcastsScreenState.Empty, - PodcastsScreenState.Loading, - -> uiState - } - PodcastsScreen( podcastsScreenState = modifiedState, onPodcastsItemClick = onPodcastsItemClick, - onDismiss = onDismiss + onDismiss = onDismiss, ) } @@ -87,18 +89,18 @@ fun PodcastsScreen( onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - val columnState = rememberResponsiveColumnState() ScreenScaffold( scrollState = columnState, - modifier = modifier + modifier = modifier, ) { when (podcastsScreenState) { - is PodcastsScreenState.Loaded -> PodcastScreenLoaded( - columnState = columnState, - podcastList = podcastsScreenState.podcastList, - onPodcastsItemClick = onPodcastsItemClick - ) + is PodcastsScreenState.Loaded -> + PodcastScreenLoaded( + columnState = columnState, + podcastList = podcastsScreenState.podcastList, + onPodcastsItemClick = onPodcastsItemClick, + ) PodcastsScreenState.Empty -> PodcastScreenEmpty(onDismiss) PodcastsScreenState.Loading -> @@ -112,66 +114,65 @@ fun PodcastScreenLoaded( columnState: ScalingLazyColumnState, podcastList: List, onPodcastsItemClick: (PodcastInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { EntityScreen( columnState = columnState, modifier = modifier, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = stringResource(id = R.string.podcasts)) } }, content = { - items(count = podcastList.size) { - index -> + items(count = podcastList.size) { index -> MediaContent( podcast = podcastList[index], - downloadItemArtworkPlaceholder = rememberVectorPainter( - image = Icons.Default.MusicNote, - tintColor = Color.Blue, - ), - onPodcastsItemClick = onPodcastsItemClick - + downloadItemArtworkPlaceholder = + rememberVectorPainter( + image = Icons.Default.MusicNote, + tintColor = Color.Blue, + ), + onPodcastsItemClick = onPodcastsItemClick, ) } - } + }, ) } @Composable fun PodcastScreenEmpty( onDismiss: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { AlertDialog( showDialog = true, message = stringResource(R.string.podcasts_no_podcasts), onDismiss = onDismiss, - modifier = modifier + modifier = modifier, ) } @Composable fun PodcastScreenLoading( columnState: ScalingLazyColumnState, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { EntityScreen( columnState = columnState, modifier = modifier, headerContent = { DefaultEntityScreenHeader( - title = stringResource(R.string.podcasts) + title = stringResource(R.string.podcasts), ) }, content = { items(count = 2) { PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) } - } + }, ) } @@ -179,18 +180,20 @@ fun PodcastScreenLoading( @WearPreviewFontScales @Composable fun PodcastScreenLoadedPreview( - @PreviewParameter(WearPreviewPodcasts::class) podcasts: PodcastInfo + @PreviewParameter(WearPreviewPodcasts::class) podcasts: PodcastInfo, ) { - val columnState = rememberResponsiveColumnState( - contentPadding = ScalingLazyColumnDefaults.padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip + val columnState = + rememberResponsiveColumnState( + contentPadding = + ScalingLazyColumnDefaults.padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) - ) PodcastScreenLoaded( columnState = columnState, podcastList = listOf(podcasts), - onPodcastsItemClick = {} + onPodcastsItemClick = {}, ) } @@ -198,12 +201,14 @@ fun PodcastScreenLoadedPreview( @WearPreviewFontScales @Composable fun PodcastScreenLoadingPreview() { - val columnState = rememberResponsiveColumnState( - contentPadding = ScalingLazyColumnDefaults.padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip + val columnState = + rememberResponsiveColumnState( + contentPadding = + ScalingLazyColumnDefaults.padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) - ) PodcastScreenLoading(columnState) } @@ -218,7 +223,7 @@ fun PodcastScreenEmptyPreview() { fun MediaContent( podcast: PodcastInfo, downloadItemArtworkPlaceholder: Painter?, - onPodcastsItemClick: (PodcastInfo) -> Unit + onPodcastsItemClick: (PodcastInfo) -> Unit, ) { val mediaTitle = podcast.title diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsViewModel.kt index 65d7f0666b..e909c77898 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsViewModel.kt @@ -24,48 +24,46 @@ import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.core.model.asExternalModel import com.google.android.horologist.annotations.ExperimentalHorologistApi import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject @HiltViewModel -class PodcastsViewModel @Inject constructor( - podcastStore: PodcastStore, -) : ViewModel() { - - val uiState: StateFlow = - podcastStore.followedPodcastsSortedByLastEpisode(limit = 10).map { - if (it.isNotEmpty()) { - PodcastsScreenState.Loaded(it.map(PodcastMapper::map)) - } else { - PodcastsScreenState.Empty - } - }.catch { - emit(PodcastsScreenState.Empty) - }.stateIn( - viewModelScope, - started = SharingStarted.Eagerly, - initialValue = PodcastsScreenState.Loading, - ) -} +class PodcastsViewModel + @Inject + constructor( + podcastStore: PodcastStore, + ) : ViewModel() { + val uiState: StateFlow = + podcastStore + .followedPodcastsSortedByLastEpisode(limit = 10) + .map { + if (it.isNotEmpty()) { + PodcastsScreenState.Loaded(it.map(PodcastMapper::map)) + } else { + PodcastsScreenState.Empty + } + }.catch { + emit(PodcastsScreenState.Empty) + }.stateIn( + viewModelScope, + started = SharingStarted.Eagerly, + initialValue = PodcastsScreenState.Loading, + ) + } object PodcastMapper { - /** * Maps from [Podcast]. */ - fun map( - podcastWithExtraInfo: PodcastWithExtraInfo, - ): PodcastInfo = - podcastWithExtraInfo.asExternalModel() + fun map(podcastWithExtraInfo: PodcastWithExtraInfo): PodcastInfo = podcastWithExtraInfo.asExternalModel() } @ExperimentalHorologistApi sealed interface PodcastsScreenState { - data object Loading : PodcastsScreenState data class Loaded( diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt index 88d9aa32f0..4c94bb858a 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt @@ -63,7 +63,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen onEpisodeItemClick: (PlayerEpisode) -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, - queueViewModel: QueueViewModel = hiltViewModel() + queueViewModel: QueueViewModel = hiltViewModel(), ) { val uiState by queueViewModel.uiState.collectAsStateWithLifecycle() @@ -74,7 +74,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen modifier = modifier, onEpisodeItemClick = onEpisodeItemClick, onDeleteQueueEpisodes = queueViewModel::onDeleteQueueEpisodes, - onDismiss = onDismiss + onDismiss = onDismiss, ) } @@ -86,27 +86,30 @@ fun QueueScreen( modifier: Modifier = Modifier, onEpisodeItemClick: (PlayerEpisode) -> Unit, onDeleteQueueEpisodes: () -> Unit, - onDismiss: () -> Unit + onDismiss: () -> Unit, ) { - val columnState = rememberResponsiveColumnState( - contentPadding = padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip + val columnState = + rememberResponsiveColumnState( + contentPadding = + padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) - ) ScreenScaffold( scrollState = columnState, - modifier = modifier + modifier = modifier, ) { when (uiState) { - is QueueScreenState.Loaded -> QueueScreenLoaded( - columnState = columnState, - episodeList = uiState.episodeList, - onDeleteQueueEpisodes = onDeleteQueueEpisodes, - onPlayEpisodes = onPlayEpisodes, - onPlayButtonClick = onPlayButtonClick, - onEpisodeItemClick = onEpisodeItemClick - ) + is QueueScreenState.Loaded -> + QueueScreenLoaded( + columnState = columnState, + episodeList = uiState.episodeList, + onDeleteQueueEpisodes = onDeleteQueueEpisodes, + onPlayEpisodes = onPlayEpisodes, + onPlayButtonClick = onPlayButtonClick, + onEpisodeItemClick = onEpisodeItemClick, + ) QueueScreenState.Loading -> QueueScreenLoading(columnState) QueueScreenState.Empty -> QueueScreenEmpty(onDismiss) } @@ -121,14 +124,14 @@ fun QueueScreenLoaded( onPlayEpisodes: (List) -> Unit, onDeleteQueueEpisodes: () -> Unit, onEpisodeItemClick: (PlayerEpisode) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { EntityScreen( columnState = columnState, modifier = modifier, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = stringResource(R.string.queue)) } @@ -138,35 +141,36 @@ fun QueueScreenLoaded( episodes = episodeList, onPlayButtonClick = onPlayButtonClick, onPlayEpisodes = onPlayEpisodes, - onDeleteQueueEpisodes = onDeleteQueueEpisodes + onDeleteQueueEpisodes = onDeleteQueueEpisodes, ) }, content = { items(episodeList) { episode -> MediaContent( episode = episode, - episodeArtworkPlaceholder = rememberVectorPainter( - image = Icons.Default.MusicNote, - tintColor = Color.Blue, - ), - onItemClick = onEpisodeItemClick + episodeArtworkPlaceholder = + rememberVectorPainter( + image = Icons.Default.MusicNote, + tintColor = Color.Blue, + ), + onItemClick = onEpisodeItemClick, ) } - } + }, ) } @Composable fun QueueScreenLoading( columnState: ScalingLazyColumnState, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { EntityScreen( columnState = columnState, modifier = modifier, headerContent = { DefaultEntityScreenHeader( - title = stringResource(R.string.queue) + title = stringResource(R.string.queue), ) }, buttonsContent = { @@ -175,28 +179,28 @@ fun QueueScreenLoading( onPlayButtonClick = {}, onPlayEpisodes = {}, onDeleteQueueEpisodes = { }, - enabled = false + enabled = false, ) }, content = { items(count = 2) { PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) } - } + }, ) } @Composable fun QueueScreenEmpty( onDismiss: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { AlertDialog( showDialog = true, onDismiss = onDismiss, title = stringResource(R.string.display_nothing_in_queue), message = stringResource(R.string.no_episodes_from_queue), - modifier = modifier + modifier = modifier, ) } @@ -208,13 +212,13 @@ fun ButtonsContent( onPlayEpisodes: (List) -> Unit, onDeleteQueueEpisodes: () -> Unit, enabled: Boolean = true, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - Row( - modifier = modifier - .padding(bottom = 16.dp) - .height(52.dp), + modifier = + modifier + .padding(bottom = 16.dp) + .height(52.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), ) { @@ -225,18 +229,20 @@ fun ButtonsContent( onPlayButtonClick() onPlayEpisodes(episodes) }, - modifier = Modifier - .weight(weight = 0.3F, fill = false), - enabled = enabled + modifier = + Modifier + .weight(weight = 0.3F, fill = false), + enabled = enabled, ) Button( imageVector = Icons.Outlined.Delete, contentDescription = - stringResource(id = R.string.button_delete_queue_content_description), + stringResource(id = R.string.button_delete_queue_content_description), onClick = onDeleteQueueEpisodes, - modifier = Modifier - .weight(weight = 0.3F, fill = false), - enabled = enabled + modifier = + Modifier + .weight(weight = 0.3F, fill = false), + enabled = enabled, ) } } @@ -246,21 +252,23 @@ fun ButtonsContent( @Composable fun QueueScreenLoadedPreview( @PreviewParameter(WearPreviewEpisodes::class) - episode: PlayerEpisode + episode: PlayerEpisode, ) { - val columnState = rememberResponsiveColumnState( - contentPadding = padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip + val columnState = + rememberResponsiveColumnState( + contentPadding = + padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) - ) QueueScreenLoaded( columnState = columnState, episodeList = listOf(episode), onPlayButtonClick = { }, onPlayEpisodes = { }, onEpisodeItemClick = { }, - onDeleteQueueEpisodes = { } + onDeleteQueueEpisodes = { }, ) } @@ -268,12 +276,14 @@ fun QueueScreenLoadedPreview( @WearPreviewFontScales @Composable fun QueueScreenLoadingPreview() { - val columnState = rememberResponsiveColumnState( - contentPadding = padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip + val columnState = + rememberResponsiveColumnState( + contentPadding = + padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) - ) QueueScreenLoading( columnState = columnState, ) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueViewModel.kt index bdd38f694d..2756a72a92 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueViewModel.kt @@ -22,55 +22,56 @@ import com.example.jetcaster.core.player.EpisodePlayer import com.example.jetcaster.core.player.model.PlayerEpisode import com.google.android.horologist.annotations.ExperimentalHorologistApi import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject /** * ViewModel that handles the business logic and screen state of the Queue screen. */ @HiltViewModel -class QueueViewModel @Inject constructor( - private val episodePlayer: EpisodePlayer, - -) : ViewModel() { +class QueueViewModel + @Inject + constructor( + private val episodePlayer: EpisodePlayer, + ) : ViewModel() { + val uiState: StateFlow = + episodePlayer.playerState + .map { + if (it.queue.isNotEmpty()) { + QueueScreenState.Loaded(it.queue) + } else { + QueueScreenState.Empty + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + QueueScreenState.Loading, + ) - val uiState: StateFlow = episodePlayer.playerState.map { - if (it.queue.isNotEmpty()) { - QueueScreenState.Loaded(it.queue) - } else { - QueueScreenState.Empty + fun onPlayEpisode(episode: PlayerEpisode) { + episodePlayer.currentEpisode = episode + episodePlayer.play() } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5000), - QueueScreenState.Loading, - ) - - fun onPlayEpisode(episode: PlayerEpisode) { - episodePlayer.currentEpisode = episode - episodePlayer.play() - } - fun onPlayEpisodes(episodes: List) { - episodePlayer.currentEpisode = episodes[0] - episodePlayer.play(episodes) - } + fun onPlayEpisodes(episodes: List) { + episodePlayer.currentEpisode = episodes[0] + episodePlayer.play(episodes) + } - fun onDeleteQueueEpisodes() { - episodePlayer.removeAllFromQueue() + fun onDeleteQueueEpisodes() { + episodePlayer.removeAllFromQueue() + } } -} @ExperimentalHorologistApi sealed interface QueueScreenState { - data object Loading : QueueScreenState data class Loaded( - val episodeList: List + val episodeList: List, ) : QueueScreenState data object Empty : QueueScreenState diff --git a/Jetchat/.editorconfig b/Jetchat/.editorconfig new file mode 100644 index 0000000000..43f0af8237 --- /dev/null +++ b/Jetchat/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{kt,kts}] +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL + +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ktlint_standard_property-naming = disabled +ktlint_function_naming_ignore_when_annotated_with = Composable diff --git a/Jetchat/app/build.gradle.kts b/Jetchat/app/build.gradle.kts index da4276b718..97828375eb 100644 --- a/Jetchat/app/build.gradle.kts +++ b/Jetchat/app/build.gradle.kts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,6 +14,7 @@ * limitations under the License. */ + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -21,13 +22,22 @@ plugins { } android { - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() namespace = "com.example.compose.jetchat" defaultConfig { applicationId = "com.example.compose.jetchat" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() + targetSdk = + libs.versions.targetSdk + .get() + .toInt() versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -50,14 +60,15 @@ android { buildTypes { getByName("debug") { - } getByName("release") { isMinifyEnabled = true signingConfig = signingConfigs.getByName("release") - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) } } diff --git a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/ConversationTest.kt b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/ConversationTest.kt index 3d6baeee74..2ec13e49f5 100644 --- a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/ConversationTest.kt +++ b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/ConversationTest.kt @@ -41,7 +41,6 @@ import org.junit.Test * Checks that the features in the Conversation screen work as expected. */ class ConversationTest { - @get:Rule val composeTestRule = createAndroidComposeRule() @@ -55,7 +54,7 @@ class ConversationTest { ConversationContent( uiState = conversationTestUiState, navigateToProfile = { }, - onNavIconPressed = { } + onNavIconPressed = { }, ) } } @@ -75,7 +74,7 @@ class ConversationTest { this.swipe( start = this.center, end = Offset(this.center.x, this.center.y + 500), - durationMillis = 200 + durationMillis = 200, ) } // Check that the jump to bottom button is shown @@ -89,7 +88,7 @@ class ConversationTest { this.swipe( start = this.center, end = Offset(this.center.x, this.center.y + 500), - durationMillis = 200 + durationMillis = 200, ) } // Snap scroll to the bottom @@ -102,16 +101,17 @@ class ConversationTest { @Test fun jumpToBottom_snapsToBottomAfterUserInteracted() { // First swipe - composeTestRule.onNodeWithTag( - testTag = ConversationTestTag, - useUnmergedTree = true // https://issuetracker.google.com/issues/184825850 - ).performTouchInput { - this.swipe( - start = this.center, - end = Offset(this.center.x, this.center.y + 500), - durationMillis = 200 - ) - } + composeTestRule + .onNodeWithTag( + testTag = ConversationTestTag, + useUnmergedTree = true, // https://issuetracker.google.com/issues/184825850 + ).performTouchInput { + this.swipe( + start = this.center, + end = Offset(this.center.x, this.center.y + 500), + durationMillis = 200, + ) + } // Second, snap to bottom findJumpToBottom().performClick() @@ -129,7 +129,7 @@ class ConversationTest { this.swipe( start = this.center, end = Offset(this.center.x, this.center.y + 500), - durationMillis = 200 + durationMillis = 200, ) } @@ -146,23 +146,23 @@ class ConversationTest { private fun findJumpToBottom() = composeTestRule.onNodeWithText( composeTestRule.activity.getString(R.string.jumpBottom), - useUnmergedTree = true + useUnmergedTree = true, ) private fun openEmojiSelector() = composeTestRule .onNodeWithContentDescription( label = composeTestRule.activity.getString(R.string.emoji_selector_bt_desc), - useUnmergedTree = true // https://issuetracker.google.com/issues/184825850 - ) - .performClick() + useUnmergedTree = true, // https://issuetracker.google.com/issues/184825850 + ).performClick() } /** * Make the list of messages longer so the test makes sense on tablets. */ -private val conversationTestUiState = ConversationUiState( - initialMessages = (exampleUiState.messages.plus(exampleUiState.messages)), - channelName = "#composers", - channelMembers = 42 -) +private val conversationTestUiState = + ConversationUiState( + initialMessages = (exampleUiState.messages.plus(exampleUiState.messages)), + channelName = "#composers", + channelMembers = 42, + ) diff --git a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/NavigationTest.kt b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/NavigationTest.kt index 63fb05b784..6b188ae550 100644 --- a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/NavigationTest.kt +++ b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/NavigationTest.kt @@ -36,7 +36,6 @@ import org.junit.Test * Checks that the navigation flows in the app are correct. */ class NavigationTest { - @get:Rule val composeTestRule = createAndroidComposeRule() @@ -80,29 +79,30 @@ class NavigationTest { } private fun navigateToProfile(name: String) { - composeTestRule.onNodeWithContentDescription( - composeTestRule.activity.getString(R.string.navigation_drawer_open) - ).performClick() + composeTestRule + .onNodeWithContentDescription( + composeTestRule.activity.getString(R.string.navigation_drawer_open), + ).performClick() composeTestRule.onNode(hasText(name) and isInDrawer()).performClick() } private fun isInDrawer() = hasAnyAncestor(isDrawer()) - private fun isDrawer() = SemanticsMatcher.expectValue( - SemanticsProperties.PaneTitle, - composeTestRule.activity.getString(androidx.compose.ui.R.string.navigation_menu) - ) + private fun isDrawer() = + SemanticsMatcher.expectValue( + SemanticsProperties.PaneTitle, + composeTestRule.activity.getString(androidx.compose.ui.R.string.navigation_menu), + ) private fun navigateToHome() { - composeTestRule.onNodeWithContentDescription( - composeTestRule.activity.getString(R.string.navigation_drawer_open) - ).performClick() + composeTestRule + .onNodeWithContentDescription( + composeTestRule.activity.getString(R.string.navigation_drawer_open), + ).performClick() composeTestRule.onNode(hasText("composers") and isInDrawer()).performClick() } - private fun getNavController(): NavController { - return composeTestRule.activity.findNavController(R.id.nav_host_fragment) - } + private fun getNavController(): NavController = composeTestRule.activity.findNavController(R.id.nav_host_fragment) } diff --git a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/UserInputTest.kt b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/UserInputTest.kt index cd601860bb..ad6777d6dd 100644 --- a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/UserInputTest.kt +++ b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/UserInputTest.kt @@ -45,7 +45,6 @@ import org.junit.Test * Checks that the user input composable, including extended controls, behave as expected. */ class UserInputTest { - @get:Rule val composeTestRule = createAndroidComposeRule() @@ -59,7 +58,7 @@ class UserInputTest { ConversationContent( uiState = exampleUiState, navigateToProfile = { }, - onNavIconPressed = { } + onNavIconPressed = { }, ) } } @@ -73,7 +72,8 @@ class UserInputTest { // Check emoji selector is displayed assertEmojiSelectorIsDisplayed() - composeTestRule.onNode(SemanticsMatcher.expectValue(KeyboardShownKey, false)) + composeTestRule + .onNode(SemanticsMatcher.expectValue(KeyboardShownKey, false)) .assertExists() // Press back button Espresso.pressBack() @@ -83,7 +83,8 @@ class UserInputTest { composeTestRule.waitUntil(timeoutMillis = 10_000) { composeTestRule .onAllNodesWithContentDescription(activity.getString(R.string.emoji_selector_desc)) - .fetchSemanticsNodes().isEmpty() + .fetchSemanticsNodes() + .isEmpty() } // Check the emoji selector is not displayed @@ -142,9 +143,8 @@ class UserInputTest { composeTestRule .onNodeWithContentDescription( label = activity.getString(R.string.emoji_selector_bt_desc), - useUnmergedTree = true // https://issuetracker.google.com/issues/184825850 - ) - .performClick() + useUnmergedTree = true, // https://issuetracker.google.com/issues/184825850 + ).performClick() private fun assertEmojiSelectorIsDisplayed() = composeTestRule @@ -158,10 +158,9 @@ class UserInputTest { private fun findSendButton() = composeTestRule.onNodeWithText(activity.getString(R.string.send)) - private fun findTextInputField(): SemanticsNodeInteraction { - return composeTestRule.onNode( + private fun findTextInputField(): SemanticsNodeInteraction = + composeTestRule.onNode( hasSetTextAction() and - hasAnyAncestor(hasContentDescription(activity.getString(R.string.textfield_desc))) + hasAnyAncestor(hasContentDescription(activity.getString(R.string.textfield_desc))), ) - } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/MainViewModel.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/MainViewModel.kt index 51bb6d040a..a344b437a9 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/MainViewModel.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/MainViewModel.kt @@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.asStateFlow * Used to communicate between screens. */ class MainViewModel : ViewModel() { - private val _drawerShouldBeOpened = MutableStateFlow(false) val drawerShouldBeOpened = _drawerShouldBeOpened.asStateFlow() diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/NavActivity.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/NavActivity.kt index 644d0855e8..8690e8eb78 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/NavActivity.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/NavActivity.kt @@ -95,18 +95,16 @@ class NavActivity : AppCompatActivity() { scope.launch { drawerState.close() } - } + }, ) { AndroidViewBinding(ContentMainBinding::inflate) } } - } + }, ) } - override fun onSupportNavigateUp(): Boolean { - return findNavController().navigateUp() || super.onSupportNavigateUp() - } + override fun onSupportNavigateUp(): Boolean = findNavController().navigateUp() || super.onSupportNavigateUp() /** * See https://issuetracker.google.com/142847973 diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/UiExtras.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/UiExtras.kt index a374c4107b..3d9f0c9064 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/UiExtras.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/UiExtras.kt @@ -29,13 +29,13 @@ fun FunctionalityNotAvailablePopup(onDismiss: () -> Unit) { text = { Text( text = "Functionality not available \uD83D\uDE48", - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) }, confirmButton = { TextButton(onClick = onDismiss) { Text(text = "CLOSE") } - } + }, ) } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/AnimatingFabContent.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/AnimatingFabContent.kt index d4617b6f85..a624834067 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/AnimatingFabContent.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/AnimatingFabContent.kt @@ -40,7 +40,7 @@ fun AnimatingFabContent( icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, - extended: Boolean = true + extended: Boolean = true, ) { val currentState = if (extended) ExpandableFabStates.Extended else ExpandableFabStates.Collapsed val transition = updateTransition(currentState, "fab_transition") @@ -50,17 +50,17 @@ fun AnimatingFabContent( if (targetState == ExpandableFabStates.Collapsed) { tween( easing = LinearEasing, - durationMillis = (transitionDuration / 12f * 5).roundToInt() // 5 / 12 frames + durationMillis = (transitionDuration / 12f * 5).roundToInt(), // 5 / 12 frames ) } else { tween( easing = LinearEasing, delayMillis = (transitionDuration / 3f).roundToInt(), // 4 / 12 frames - durationMillis = (transitionDuration / 12f * 5).roundToInt() // 5 / 12 frames + durationMillis = (transitionDuration / 12f * 5).roundToInt(), // 5 / 12 frames ) } }, - label = "fab_text_opacity" + label = "fab_text_opacity", ) { state -> if (state == ExpandableFabStates.Collapsed) { 0f @@ -73,16 +73,16 @@ fun AnimatingFabContent( if (targetState == ExpandableFabStates.Collapsed) { tween( easing = FastOutSlowInEasing, - durationMillis = transitionDuration + durationMillis = transitionDuration, ) } else { tween( easing = FastOutSlowInEasing, - durationMillis = transitionDuration + durationMillis = transitionDuration, ) } }, - label = "fab_width_factor" + label = "fab_width_factor", ) { state -> if (state == ExpandableFabStates.Collapsed) { 0f @@ -97,7 +97,7 @@ fun AnimatingFabContent( text, { textOpacity }, { fabWidthFactor }, - modifier = modifier + modifier = modifier, ) } @@ -107,7 +107,7 @@ private fun IconAndTextRow( text: @Composable () -> Unit, opacityProgress: () -> Float, // Lambdas instead of Floats, to defer read widthProgress: () -> Float, - modifier: Modifier + modifier: Modifier, ) { Layout( modifier = modifier, @@ -116,7 +116,7 @@ private fun IconAndTextRow( Box(modifier = Modifier.graphicsLayer { alpha = opacityProgress() }) { text() } - } + }, ) { measurables, constraints -> val iconPlaceable = measurables[0].measure(constraints) @@ -139,11 +139,11 @@ private fun IconAndTextRow( layout(width.roundToInt(), height) { iconPlaceable.place( iconPadding.roundToInt(), - constraints.maxHeight / 2 - iconPlaceable.height / 2 + constraints.maxHeight / 2 - iconPlaceable.height / 2, ) textPlaceable.place( (iconPlaceable.width + iconPadding * 2).roundToInt(), - constraints.maxHeight / 2 - textPlaceable.height / 2 + constraints.maxHeight / 2 - textPlaceable.height / 2, ) } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/BaseLineHeightModifier.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/BaseLineHeightModifier.kt index c0f731aa0d..9d6dcb002d 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/BaseLineHeightModifier.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/BaseLineHeightModifier.kt @@ -40,14 +40,12 @@ import androidx.compose.ui.unit.Dp * baselines. */ data class BaselineHeightModifier( - val heightFromBaseline: Dp + val heightFromBaseline: Dp, ) : LayoutModifier { - override fun MeasureScope.measure( measurable: Measurable, - constraints: Constraints + constraints: Constraints, ): MeasureResult { - val textPlaceable = measurable.measure(constraints) val firstBaseline = textPlaceable[FirstBaseline] val lastBaseline = textPlaceable[LastBaseline] @@ -60,5 +58,4 @@ data class BaselineHeightModifier( } } -fun Modifier.baselineHeight(heightFromBaseline: Dp): Modifier = - this.then(BaselineHeightModifier(heightFromBaseline)) +fun Modifier.baselineHeight(heightFromBaseline: Dp): Modifier = this.then(BaselineHeightModifier(heightFromBaseline)) diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt index 9d221bd6fa..6a19abd75c 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt @@ -41,7 +41,7 @@ fun JetchatAppBar( scrollBehavior: TopAppBarScrollBehavior? = null, onNavIconPressed: () -> Unit = { }, title: @Composable () -> Unit, - actions: @Composable RowScope.() -> Unit = {} + actions: @Composable RowScope.() -> Unit = {}, ) { CenterAlignedTopAppBar( modifier = modifier, @@ -51,12 +51,13 @@ fun JetchatAppBar( navigationIcon = { JetchatIcon( contentDescription = stringResource(id = R.string.navigation_drawer_open), - modifier = Modifier - .size(64.dp) - .clickable(onClick = onNavIconPressed) - .padding(16.dp) + modifier = + Modifier + .size(64.dp) + .clickable(onClick = onNavIconPressed) + .padding(16.dp), ) - } + }, ) } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatDrawer.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatDrawer.kt index fde46076f6..b8b8005c93 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatDrawer.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatDrawer.kt @@ -57,7 +57,7 @@ import com.example.compose.jetchat.theme.JetchatTheme fun JetchatDrawerContent( onProfileClicked: (String) -> Unit, onChatClicked: (String) -> - Unit + Unit, ) { // Use windowInsetsTopHeight() to add a spacer which pushes the drawer content // below the status bar (y-axis) @@ -82,92 +82,108 @@ private fun DrawerHeader() { Row(modifier = Modifier.padding(16.dp), verticalAlignment = CenterVertically) { JetchatIcon( contentDescription = null, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(24.dp), ) Image( painter = painterResource(id = R.drawable.jetchat_logo), contentDescription = null, - modifier = Modifier.padding(start = 8.dp) + modifier = Modifier.padding(start = 8.dp), ) } } + @Composable private fun DrawerItemHeader(text: String) { Box( - modifier = Modifier - .heightIn(min = 52.dp) - .padding(horizontal = 28.dp), - contentAlignment = CenterStart + modifier = + Modifier + .heightIn(min = 52.dp) + .padding(horizontal = 28.dp), + contentAlignment = CenterStart, ) { Text( text, style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @Composable -private fun ChatItem(text: String, selected: Boolean, onChatClicked: () -> Unit) { - val background = if (selected) { - Modifier.background(MaterialTheme.colorScheme.primaryContainer) - } else { - Modifier - } - Row( - modifier = Modifier - .height(56.dp) - .fillMaxWidth() - .padding(horizontal = 12.dp) - .clip(CircleShape) - .then(background) - .clickable(onClick = onChatClicked), - verticalAlignment = CenterVertically - ) { - val iconTint = if (selected) { - MaterialTheme.colorScheme.primary +private fun ChatItem( + text: String, + selected: Boolean, + onChatClicked: () -> Unit, +) { + val background = + if (selected) { + Modifier.background(MaterialTheme.colorScheme.primaryContainer) } else { - MaterialTheme.colorScheme.onSurfaceVariant + Modifier } + Row( + modifier = + Modifier + .height(56.dp) + .fillMaxWidth() + .padding(horizontal = 12.dp) + .clip(CircleShape) + .then(background) + .clickable(onClick = onChatClicked), + verticalAlignment = CenterVertically, + ) { + val iconTint = + if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } Icon( painter = painterResource(id = R.drawable.ic_jetchat), tint = iconTint, modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp), - contentDescription = null + contentDescription = null, ) Text( text, style = MaterialTheme.typography.bodyMedium, - color = if (selected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - }, - modifier = Modifier.padding(start = 12.dp) + color = + if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + }, + modifier = Modifier.padding(start = 12.dp), ) } } @Composable -private fun ProfileItem(text: String, @DrawableRes profilePic: Int?, onProfileClicked: () -> Unit) { +private fun ProfileItem( + text: String, + @DrawableRes profilePic: Int?, + onProfileClicked: () -> Unit, +) { Row( - modifier = Modifier - .height(56.dp) - .fillMaxWidth() - .padding(horizontal = 12.dp) - .clip(CircleShape) - .clickable(onClick = onProfileClicked), - verticalAlignment = CenterVertically + modifier = + Modifier + .height(56.dp) + .fillMaxWidth() + .padding(horizontal = 12.dp) + .clip(CircleShape) + .clickable(onClick = onProfileClicked), + verticalAlignment = CenterVertically, ) { - val paddingSizeModifier = Modifier - .padding(start = 16.dp, top = 16.dp, bottom = 16.dp) - .size(24.dp) + val paddingSizeModifier = + Modifier + .padding(start = 16.dp, top = 16.dp, bottom = 16.dp) + .size(24.dp) if (profilePic != null) { Image( painter = painterResource(id = profilePic), modifier = paddingSizeModifier.then(Modifier.clip(CircleShape)), contentScale = ContentScale.Crop, - contentDescription = null + contentDescription = null, ) } else { Spacer(modifier = paddingSizeModifier) @@ -176,7 +192,7 @@ private fun ProfileItem(text: String, @DrawableRes profilePic: Int?, onProfileCl text, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(start = 12.dp) + modifier = Modifier.padding(start = 12.dp), ) } } @@ -185,7 +201,7 @@ private fun ProfileItem(text: String, @DrawableRes profilePic: Int?, onProfileCl fun DividerItem(modifier: Modifier = Modifier) { Divider( modifier = modifier, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), ) } @@ -200,6 +216,7 @@ fun DrawerPreview() { } } } + @Composable @Preview fun DrawerPreviewDark() { diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatIcon.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatIcon.kt index fad1045539..29df8a4b5b 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatIcon.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatIcon.kt @@ -31,26 +31,27 @@ import com.example.compose.jetchat.R @Composable fun JetchatIcon( contentDescription: String?, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val semantics = if (contentDescription != null) { - Modifier.semantics { - this.contentDescription = contentDescription - this.role = Role.Image + val semantics = + if (contentDescription != null) { + Modifier.semantics { + this.contentDescription = contentDescription + this.role = Role.Image + } + } else { + Modifier } - } else { - Modifier - } Box(modifier = modifier.then(semantics)) { Icon( painter = painterResource(id = R.drawable.ic_jetchat_back), contentDescription = null, - tint = MaterialTheme.colorScheme.primaryContainer + tint = MaterialTheme.colorScheme.primaryContainer, ) Icon( painter = painterResource(id = R.drawable.ic_jetchat_front), contentDescription = null, - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatScaffold.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatScaffold.kt index 579074718b..e1a38f76e8 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatScaffold.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatScaffold.kt @@ -31,7 +31,7 @@ fun JetchatDrawer( drawerState: DrawerState = rememberDrawerState(initialValue = Closed), onProfileClicked: (String) -> Unit, onChatClicked: (String) -> Unit, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { JetchatTheme { ModalNavigationDrawer( @@ -40,11 +40,11 @@ fun JetchatDrawer( ModalDrawerSheet { JetchatDrawerContent( onProfileClicked = onProfileClicked, - onChatClicked = onChatClicked + onChatClicked = onChatClicked, ) } }, - content = content + content = content, ) } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt index e31857fb34..25c7918980 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt @@ -101,7 +101,7 @@ fun ConversationContent( uiState: ConversationUiState, navigateToProfile: (String) -> Unit, modifier: Modifier = Modifier, - onNavIconPressed: () -> Unit = { } + onNavIconPressed: () -> Unit = { }, ) { val authorMe = stringResource(R.string.author_me) val timeNow = stringResource(id = R.string.now) @@ -121,23 +121,24 @@ fun ConversationContent( ) }, // Exclude ime and navigation bar padding so this can be added by the UserInput composable - contentWindowInsets = ScaffoldDefaults - .contentWindowInsets - .exclude(WindowInsets.navigationBars) - .exclude(WindowInsets.ime), - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + contentWindowInsets = + ScaffoldDefaults + .contentWindowInsets + .exclude(WindowInsets.navigationBars) + .exclude(WindowInsets.ime), + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { paddingValues -> Column(Modifier.fillMaxSize().padding(paddingValues)) { Messages( messages = uiState.messages, navigateToProfile = navigateToProfile, modifier = Modifier.weight(1f), - scrollState = scrollState + scrollState = scrollState, ) UserInput( onMessageSent = { content -> uiState.addMessage( - Message(authorMe, content, timeNow) + Message(authorMe, content, timeNow), ) }, resetScroll = { @@ -147,7 +148,7 @@ fun ConversationContent( }, // let this element handle the padding so that the elevation is shown behind the // navigation bar - modifier = Modifier.navigationBarsPadding().imePadding() + modifier = Modifier.navigationBarsPadding().imePadding(), ) } } @@ -160,7 +161,7 @@ fun ChannelNameBar( channelMembers: Int, modifier: Modifier = Modifier, scrollBehavior: TopAppBarScrollBehavior? = null, - onNavIconPressed: () -> Unit = { } + onNavIconPressed: () -> Unit = { }, ) { var functionalityNotAvailablePopupShown by remember { mutableStateOf(false) } if (functionalityNotAvailablePopupShown) { @@ -175,13 +176,13 @@ fun ChannelNameBar( // Channel name Text( text = channelName, - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) // Number of members Text( text = stringResource(R.string.members, channelMembers), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } }, @@ -190,23 +191,25 @@ fun ChannelNameBar( Icon( imageVector = Icons.Outlined.Search, tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .clickable(onClick = { functionalityNotAvailablePopupShown = true }) - .padding(horizontal = 12.dp, vertical = 16.dp) - .height(24.dp), - contentDescription = stringResource(id = R.string.search) + modifier = + Modifier + .clickable(onClick = { functionalityNotAvailablePopupShown = true }) + .padding(horizontal = 12.dp, vertical = 16.dp) + .height(24.dp), + contentDescription = stringResource(id = R.string.search), ) // Info icon Icon( imageVector = Icons.Outlined.Info, tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .clickable(onClick = { functionalityNotAvailablePopupShown = true }) - .padding(horizontal = 12.dp, vertical = 16.dp) - .height(24.dp), - contentDescription = stringResource(id = R.string.info) + modifier = + Modifier + .clickable(onClick = { functionalityNotAvailablePopupShown = true }) + .padding(horizontal = 12.dp, vertical = 16.dp) + .height(24.dp), + contentDescription = stringResource(id = R.string.info), ) - } + }, ) } @@ -217,18 +220,18 @@ fun Messages( messages: List, navigateToProfile: (String) -> Unit, scrollState: LazyListState, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() Box(modifier = modifier) { - val authorMe = stringResource(id = R.string.author_me) LazyColumn( reverseLayout = true, state = scrollState, - modifier = Modifier - .testTag(ConversationTestTag) - .fillMaxSize() + modifier = + Modifier + .testTag(ConversationTestTag) + .fillMaxSize(), ) { for (index in messages.indices) { val prevAuthor = messages.getOrNull(index - 1)?.author @@ -254,16 +257,17 @@ fun Messages( msg = content, isUserMe = content.author == authorMe, isFirstMessageByAuthor = isFirstMessageByAuthor, - isLastMessageByAuthor = isLastMessageByAuthor + isLastMessageByAuthor = isLastMessageByAuthor, ) } } } // Jump to bottom button shows up when user scrolls past a threshold. // Convert to pixels: - val jumpThreshold = with(LocalDensity.current) { - JumpToBottomThreshold.toPx() - } + val jumpThreshold = + with(LocalDensity.current) { + JumpToBottomThreshold.toPx() + } // Show the button if the first visible item is not the first one or if the offset is // greater than the threshold. @@ -282,7 +286,7 @@ fun Messages( scrollState.animateScrollToItem(0) } }, - modifier = Modifier.align(Alignment.BottomCenter) + modifier = Modifier.align(Alignment.BottomCenter), ) } } @@ -293,27 +297,29 @@ fun Message( msg: Message, isUserMe: Boolean, isFirstMessageByAuthor: Boolean, - isLastMessageByAuthor: Boolean + isLastMessageByAuthor: Boolean, ) { - val borderColor = if (isUserMe) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.tertiary - } + val borderColor = + if (isUserMe) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.tertiary + } val spaceBetweenAuthors = if (isLastMessageByAuthor) Modifier.padding(top = 8.dp) else Modifier Row(modifier = spaceBetweenAuthors) { if (isLastMessageByAuthor) { // Avatar Image( - modifier = Modifier - .clickable(onClick = { onAuthorClick(msg.author) }) - .padding(horizontal = 16.dp) - .size(42.dp) - .border(1.5.dp, borderColor, CircleShape) - .border(3.dp, MaterialTheme.colorScheme.surface, CircleShape) - .clip(CircleShape) - .align(Alignment.Top), + modifier = + Modifier + .clickable(onClick = { onAuthorClick(msg.author) }) + .padding(horizontal = 16.dp) + .size(42.dp) + .border(1.5.dp, borderColor, CircleShape) + .border(3.dp, MaterialTheme.colorScheme.surface, CircleShape) + .clip(CircleShape) + .align(Alignment.Top), painter = painterResource(id = msg.authorImage), contentScale = ContentScale.Crop, contentDescription = null, @@ -328,9 +334,10 @@ fun Message( isFirstMessageByAuthor = isFirstMessageByAuthor, isLastMessageByAuthor = isLastMessageByAuthor, authorClicked = onAuthorClick, - modifier = Modifier - .padding(end = 16.dp) - .weight(1f) + modifier = + Modifier + .padding(end = 16.dp) + .weight(1f), ) } } @@ -342,7 +349,7 @@ fun AuthorAndTextMessage( isFirstMessageByAuthor: Boolean, isLastMessageByAuthor: Boolean, authorClicked: (String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Column(modifier = modifier) { if (isLastMessageByAuthor) { @@ -366,16 +373,17 @@ private fun AuthorNameTimestamp(msg: Message) { Text( text = msg.author, style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .alignBy(LastBaseline) - .paddingFrom(LastBaseline, after = 8.dp) // Space to 1st bubble + modifier = + Modifier + .alignBy(LastBaseline) + .paddingFrom(LastBaseline, after = 8.dp), // Space to 1st bubble ) Spacer(modifier = Modifier.width(8.dp)) Text( text = msg.timestamp, style = MaterialTheme.typography.bodySmall, modifier = Modifier.alignBy(LastBaseline), - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -385,16 +393,17 @@ private val ChatBubbleShape = RoundedCornerShape(4.dp, 20.dp, 20.dp, 20.dp) @Composable fun DayHeader(dayString: String) { Row( - modifier = Modifier - .padding(vertical = 8.dp, horizontal = 16.dp) - .height(16.dp) + modifier = + Modifier + .padding(vertical = 8.dp, horizontal = 16.dp) + .height(16.dp), ) { DayHeaderLine() Text( text = dayString, modifier = Modifier.padding(horizontal = 16.dp), style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) DayHeaderLine() } @@ -403,10 +412,11 @@ fun DayHeader(dayString: String) { @Composable private fun RowScope.DayHeaderLine() { Divider( - modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) + modifier = + Modifier + .weight(1f) + .align(Alignment.CenterVertically), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), ) } @@ -414,24 +424,24 @@ private fun RowScope.DayHeaderLine() { fun ChatItemBubble( message: Message, isUserMe: Boolean, - authorClicked: (String) -> Unit + authorClicked: (String) -> Unit, ) { - - val backgroundBubbleColor = if (isUserMe) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.surfaceVariant - } + val backgroundBubbleColor = + if (isUserMe) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceVariant + } Column { Surface( color = backgroundBubbleColor, - shape = ChatBubbleShape + shape = ChatBubbleShape, ) { ClickableMessage( message = message, isUserMe = isUserMe, - authorClicked = authorClicked + authorClicked = authorClicked, ) } @@ -439,13 +449,13 @@ fun ChatItemBubble( Spacer(modifier = Modifier.height(4.dp)) Surface( color = backgroundBubbleColor, - shape = ChatBubbleShape + shape = ChatBubbleShape, ) { Image( painter = painterResource(it), contentScale = ContentScale.Fit, modifier = Modifier.size(160.dp), - contentDescription = stringResource(id = R.string.attached_image) + contentDescription = stringResource(id = R.string.attached_image), ) } } @@ -456,14 +466,15 @@ fun ChatItemBubble( fun ClickableMessage( message: Message, isUserMe: Boolean, - authorClicked: (String) -> Unit + authorClicked: (String) -> Unit, ) { val uriHandler = LocalUriHandler.current - val styledMessage = messageFormatter( - text = message.content, - primary = isUserMe - ) + val styledMessage = + messageFormatter( + text = message.content, + primary = isUserMe, + ) ClickableText( text = styledMessage, @@ -480,7 +491,7 @@ fun ClickableMessage( else -> Unit } } - } + }, ) } @@ -490,7 +501,7 @@ fun ConversationPreview() { JetchatTheme { ConversationContent( uiState = exampleUiState, - navigateToProfile = { } + navigateToProfile = { }, ) } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationFragment.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationFragment.kt index efdc12401f..c4e2a91930 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationFragment.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationFragment.kt @@ -33,33 +33,33 @@ import com.example.compose.jetchat.data.exampleUiState import com.example.compose.jetchat.theme.JetchatTheme class ConversationFragment : Fragment() { - private val activityViewModel: MainViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View = ComposeView(inflater.context).apply { - layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) + savedInstanceState: Bundle?, + ): View = + ComposeView(inflater.context).apply { + layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) - setContent { - JetchatTheme { - ConversationContent( - uiState = exampleUiState, - navigateToProfile = { user -> - // Click callback - val bundle = bundleOf("userId" to user) - findNavController().navigate( - R.id.nav_profile, - bundle - ) - }, - onNavIconPressed = { - activityViewModel.openDrawer() - } - ) + setContent { + JetchatTheme { + ConversationContent( + uiState = exampleUiState, + navigateToProfile = { user -> + // Click callback + val bundle = bundleOf("userId" to user) + findNavController().navigate( + R.id.nav_profile, + bundle, + ) + }, + onNavIconPressed = { + activityViewModel.openDrawer() + }, + ) + } } } - } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationUiState.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationUiState.kt index 452e4eeb41..154754fad1 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationUiState.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationUiState.kt @@ -23,7 +23,7 @@ import com.example.compose.jetchat.R class ConversationUiState( val channelName: String, val channelMembers: Int, - initialMessages: List + initialMessages: List, ) { private val _messages: MutableList = initialMessages.toMutableStateList() val messages: List = _messages @@ -39,5 +39,5 @@ data class Message( val content: String, val timestamp: String, val image: Int? = null, - val authorImage: Int = if (author == "me") R.drawable.ali else R.drawable.someone_else + val authorImage: Int = if (author == "me") R.drawable.ali else R.drawable.someone_else, ) diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt index dfe517e03e..76f0b504ce 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt @@ -36,7 +36,7 @@ import com.example.compose.jetchat.R private enum class Visibility { VISIBLE, - GONE + GONE, } /** @@ -46,13 +46,14 @@ private enum class Visibility { fun JumpToBottom( enabled: Boolean, onClicked: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { // Show Jump to Bottom button - val transition = updateTransition( - if (enabled) Visibility.VISIBLE else Visibility.GONE, - label = "JumpToBottom visibility animation" - ) + val transition = + updateTransition( + if (enabled) Visibility.VISIBLE else Visibility.GONE, + label = "JumpToBottom visibility animation", + ) val bottomOffset by transition.animateDp(label = "JumpToBottom offset animation") { if (it == Visibility.GONE) { (-32).dp @@ -66,7 +67,7 @@ fun JumpToBottom( Icon( imageVector = Icons.Filled.ArrowDownward, modifier = Modifier.height(18.dp), - contentDescription = null + contentDescription = null, ) }, text = { @@ -75,9 +76,10 @@ fun JumpToBottom( onClick = onClicked, containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.primary, - modifier = modifier - .offset(x = 0.dp, y = -bottomOffset) - .height(36.dp) + modifier = + modifier + .offset(x = 0.dp, y = -bottomOffset) + .height(36.dp), ) } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt index 46cf46b301..a1fbf2ee37 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt @@ -37,9 +37,11 @@ val symbolPattern by lazy { // Accepted annotations for the ClickableTextWrapper enum class SymbolAnnotationType { - PERSON, LINK + PERSON, + LINK, } typealias StringAnnotation = AnnotatedString.Range + // Pair returning styled content and annotation for ClickableText when matching syntax token typealias SymbolAnnotation = Pair @@ -58,12 +60,11 @@ typealias SymbolAnnotation = Pair @Composable fun messageFormatter( text: String, - primary: Boolean + primary: Boolean, ): AnnotatedString { val tokens = symbolPattern.findAll(text) return buildAnnotatedString { - var cursorPosition = 0 val codeSnippetBackground = @@ -76,12 +77,13 @@ fun messageFormatter( for (token in tokens) { append(text.slice(cursorPosition until token.range.first)) - val (annotatedString, stringAnnotation) = getSymbolAnnotation( - matchResult = token, - colorScheme = MaterialTheme.colorScheme, - primary = primary, - codeSnippetBackground = codeSnippetBackground - ) + val (annotatedString, stringAnnotation) = + getSymbolAnnotation( + matchResult = token, + colorScheme = MaterialTheme.colorScheme, + primary = primary, + codeSnippetBackground = codeSnippetBackground, + ) append(annotatedString) if (stringAnnotation != null) { @@ -110,71 +112,79 @@ private fun getSymbolAnnotation( matchResult: MatchResult, colorScheme: ColorScheme, primary: Boolean, - codeSnippetBackground: Color -): SymbolAnnotation { - return when (matchResult.value.first()) { - '@' -> SymbolAnnotation( - AnnotatedString( - text = matchResult.value, - spanStyle = SpanStyle( - color = if (primary) colorScheme.inversePrimary else colorScheme.primary, - fontWeight = FontWeight.Bold - ) - ), - StringAnnotation( - item = matchResult.value.substring(1), - start = matchResult.range.first, - end = matchResult.range.last, - tag = SymbolAnnotationType.PERSON.name + codeSnippetBackground: Color, +): SymbolAnnotation = + when (matchResult.value.first()) { + '@' -> + SymbolAnnotation( + AnnotatedString( + text = matchResult.value, + spanStyle = + SpanStyle( + color = if (primary) colorScheme.inversePrimary else colorScheme.primary, + fontWeight = FontWeight.Bold, + ), + ), + StringAnnotation( + item = matchResult.value.substring(1), + start = matchResult.range.first, + end = matchResult.range.last, + tag = SymbolAnnotationType.PERSON.name, + ), ) - ) - '*' -> SymbolAnnotation( - AnnotatedString( - text = matchResult.value.trim('*'), - spanStyle = SpanStyle(fontWeight = FontWeight.Bold) - ), - null - ) - '_' -> SymbolAnnotation( - AnnotatedString( - text = matchResult.value.trim('_'), - spanStyle = SpanStyle(fontStyle = FontStyle.Italic) - ), - null - ) - '~' -> SymbolAnnotation( - AnnotatedString( - text = matchResult.value.trim('~'), - spanStyle = SpanStyle(textDecoration = TextDecoration.LineThrough) - ), - null - ) - '`' -> SymbolAnnotation( - AnnotatedString( - text = matchResult.value.trim('`'), - spanStyle = SpanStyle( - fontFamily = FontFamily.Monospace, - fontSize = 12.sp, - background = codeSnippetBackground, - baselineShift = BaselineShift(0.2f) - ) - ), - null - ) - 'h' -> SymbolAnnotation( - AnnotatedString( - text = matchResult.value, - spanStyle = SpanStyle( - color = if (primary) colorScheme.inversePrimary else colorScheme.primary - ) - ), - StringAnnotation( - item = matchResult.value, - start = matchResult.range.first, - end = matchResult.range.last, - tag = SymbolAnnotationType.LINK.name + '*' -> + SymbolAnnotation( + AnnotatedString( + text = matchResult.value.trim('*'), + spanStyle = SpanStyle(fontWeight = FontWeight.Bold), + ), + null, + ) + '_' -> + SymbolAnnotation( + AnnotatedString( + text = matchResult.value.trim('_'), + spanStyle = SpanStyle(fontStyle = FontStyle.Italic), + ), + null, + ) + '~' -> + SymbolAnnotation( + AnnotatedString( + text = matchResult.value.trim('~'), + spanStyle = SpanStyle(textDecoration = TextDecoration.LineThrough), + ), + null, + ) + '`' -> + SymbolAnnotation( + AnnotatedString( + text = matchResult.value.trim('`'), + spanStyle = + SpanStyle( + fontFamily = FontFamily.Monospace, + fontSize = 12.sp, + background = codeSnippetBackground, + baselineShift = BaselineShift(0.2f), + ), + ), + null, + ) + 'h' -> + SymbolAnnotation( + AnnotatedString( + text = matchResult.value, + spanStyle = + SpanStyle( + color = if (primary) colorScheme.inversePrimary else colorScheme.primary, + ), + ), + StringAnnotation( + item = matchResult.value, + start = matchResult.range.first, + end = matchResult.range.last, + tag = SymbolAnnotationType.LINK.name, + ), ) - ) else -> SymbolAnnotation(AnnotatedString(matchResult.value), null) } -} diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/RecordButton.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/RecordButton.kt index 5c2b7d3167..5b06f9f098 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/RecordButton.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/RecordButton.kt @@ -53,8 +53,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.example.compose.jetchat.R -import kotlin.math.abs import kotlinx.coroutines.launch +import kotlin.math.abs @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -65,27 +65,33 @@ fun RecordButton( onStartRecording: () -> Boolean, onFinishRecording: () -> Unit, onCancelRecording: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val transition = updateTransition(targetState = recording, label = "record") - val scale = transition.animateFloat( - transitionSpec = { spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessLow) }, - label = "record-scale", - targetValueByState = { rec -> if (rec) 2f else 1f } - ) - val containerAlpha = transition.animateFloat( - transitionSpec = { tween(2000) }, - label = "record-scale", - targetValueByState = { rec -> if (rec) 1f else 0f } - ) - val iconColor = transition.animateColor( - transitionSpec = { tween(200) }, - label = "record-scale", - targetValueByState = { rec -> - if (rec) contentColorFor(LocalContentColor.current) - else LocalContentColor.current - } - ) + val scale = + transition.animateFloat( + transitionSpec = { spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessLow) }, + label = "record-scale", + targetValueByState = { rec -> if (rec) 2f else 1f }, + ) + val containerAlpha = + transition.animateFloat( + transitionSpec = { tween(2000) }, + label = "record-scale", + targetValueByState = { rec -> if (rec) 1f else 0f }, + ) + val iconColor = + transition.animateColor( + transitionSpec = { tween(200) }, + label = "record-scale", + targetValueByState = { rec -> + if (rec) { + contentColorFor(LocalContentColor.current) + } else { + LocalContentColor.current + } + }, + ) Box { // Background during recording @@ -95,10 +101,10 @@ fun RecordButton( .aspectRatio(1f) .graphicsLayer { alpha = containerAlpha.value - scaleX = scale.value; scaleY = scale.value - } - .clip(CircleShape) - .background(LocalContentColor.current) + scaleX = scale.value + scaleY = scale.value + }.clip(CircleShape) + .background(LocalContentColor.current), ) val scope = rememberCoroutineScope() val tooltipState = remember { TooltipState() } @@ -110,24 +116,25 @@ fun RecordButton( } }, enableUserInput = false, - state = tooltipState + state = tooltipState, ) { Icon( Icons.Default.Mic, contentDescription = stringResource(R.string.record_message), tint = iconColor.value, - modifier = modifier - .sizeIn(minWidth = 56.dp, minHeight = 6.dp) - .padding(18.dp) - .clickable { } - .voiceRecordingGesture( - horizontalSwipeProgress = swipeOffset, - onSwipeProgressChanged = onSwipeOffsetChange, - onClick = { scope.launch { tooltipState.show() } }, - onStartRecording = onStartRecording, - onFinishRecording = onFinishRecording, - onCancelRecording = onCancelRecording, - ) + modifier = + modifier + .sizeIn(minWidth = 56.dp, minHeight = 6.dp) + .padding(18.dp) + .clickable { } + .voiceRecordingGesture( + horizontalSwipeProgress = swipeOffset, + onSwipeProgressChanged = onSwipeOffsetChange, + onClick = { scope.launch { tooltipState.show() } }, + onStartRecording = onStartRecording, + onFinishRecording = onFinishRecording, + onCancelRecording = onCancelRecording, + ), ) } } @@ -142,45 +149,46 @@ private fun Modifier.voiceRecordingGesture( onCancelRecording: () -> Unit = {}, swipeToCancelThreshold: Dp = 200.dp, verticalThreshold: Dp = 80.dp, -): Modifier = this - .pointerInput(Unit) { detectTapGestures { onClick() } } - .pointerInput(Unit) { - var offsetY = 0f - var dragging = false - val swipeToCancelThresholdPx = swipeToCancelThreshold.toPx() - val verticalThresholdPx = verticalThreshold.toPx() +): Modifier = + this + .pointerInput(Unit) { detectTapGestures { onClick() } } + .pointerInput(Unit) { + var offsetY = 0f + var dragging = false + val swipeToCancelThresholdPx = swipeToCancelThreshold.toPx() + val verticalThresholdPx = verticalThreshold.toPx() - detectDragGesturesAfterLongPress( - onDragStart = { - onSwipeProgressChanged(0f) - offsetY = 0f - dragging = true - onStartRecording() - }, - onDragCancel = { - onCancelRecording() - dragging = false - }, - onDragEnd = { - if (dragging) { - onFinishRecording() - } - dragging = false - }, - onDrag = { change, dragAmount -> - if (dragging) { - onSwipeProgressChanged(horizontalSwipeProgress() + dragAmount.x) - offsetY += dragAmount.y - val offsetX = horizontalSwipeProgress() - if ( - offsetX < 0 && - abs(offsetX) >= swipeToCancelThresholdPx && - abs(offsetY) <= verticalThresholdPx - ) { - onCancelRecording() - dragging = false + detectDragGesturesAfterLongPress( + onDragStart = { + onSwipeProgressChanged(0f) + offsetY = 0f + dragging = true + onStartRecording() + }, + onDragCancel = { + onCancelRecording() + dragging = false + }, + onDragEnd = { + if (dragging) { + onFinishRecording() } - } - } - ) - } + dragging = false + }, + onDrag = { change, dragAmount -> + if (dragging) { + onSwipeProgressChanged(horizontalSwipeProgress() + dragAmount.x) + offsetY += dragAmount.y + val offsetX = horizontalSwipeProgress() + if ( + offsetX < 0 && + abs(offsetX) >= swipeToCancelThresholdPx && + abs(offsetY) <= verticalThresholdPx + ) { + onCancelRecording() + dragging = false + } + } + }, + ) + } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt index 94988a239b..b0632bd9dc 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt @@ -111,10 +111,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.compose.jetchat.FunctionalityNotAvailablePopup import com.example.compose.jetchat.R +import kotlinx.coroutines.delay import kotlin.math.absoluteValue import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.delay enum class InputSelector { NONE, @@ -122,12 +122,12 @@ enum class InputSelector { DM, EMOJI, PHONE, - PICTURE + PICTURE, } enum class EmojiStickerSelector { EMOJI, - STICKER + STICKER, } @Preview @@ -180,7 +180,7 @@ fun UserInput( // Move scroll to bottom resetScroll() }, - focusState = textFieldFocusState + focusState = textFieldFocusState, ) UserInputSelector( onSelectorChange = { currentInputSelector = it }, @@ -193,27 +193,29 @@ fun UserInput( resetScroll() dismissKeyboard() }, - currentInputSelector = currentInputSelector + currentInputSelector = currentInputSelector, ) SelectorExpanded( onCloseRequested = dismissKeyboard, onTextAdded = { textState = textState.addText(it) }, - currentSelector = currentInputSelector + currentSelector = currentInputSelector, ) } } } private fun TextFieldValue.addText(newString: String): TextFieldValue { - val newText = this.text.replaceRange( - this.selection.start, - this.selection.end, - newString - ) - val newSelection = TextRange( - start = newText.length, - end = newText.length - ) + val newText = + this.text.replaceRange( + this.selection.start, + this.selection.end, + newString, + ) + val newSelection = + TextRange( + start = newText.length, + end = newText.length, + ) return this.copy(text = newText, selection = newSelection) } @@ -222,7 +224,7 @@ private fun TextFieldValue.addText(newString: String): TextFieldValue { private fun SelectorExpanded( currentSelector: InputSelector, onCloseRequested: () -> Unit, - onTextAdded: (String) -> Unit + onTextAdded: (String) -> Unit, ) { if (currentSelector == InputSelector.NONE) return @@ -254,24 +256,25 @@ fun FunctionalityNotAvailablePanel() { AnimatedVisibility( visibleState = remember { MutableTransitionState(false).apply { targetState = true } }, enter = expandHorizontally() + fadeIn(), - exit = shrinkHorizontally() + fadeOut() + exit = shrinkHorizontally() + fadeOut(), ) { Column( - modifier = Modifier - .height(320.dp) - .fillMaxWidth(), + modifier = + Modifier + .height(320.dp) + .fillMaxWidth(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { Text( text = stringResource(id = R.string.not_available), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) Text( text = stringResource(id = R.string.not_available_subtitle), modifier = Modifier.paddingFrom(FirstBaseline, before = 32.dp), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -283,62 +286,65 @@ private fun UserInputSelector( sendMessageEnabled: Boolean, onMessageSent: () -> Unit, currentInputSelector: InputSelector, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row( - modifier = modifier - .height(72.dp) - .wrapContentHeight() - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - verticalAlignment = Alignment.CenterVertically + modifier = + modifier + .height(72.dp) + .wrapContentHeight() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, ) { InputSelectorButton( onClick = { onSelectorChange(InputSelector.EMOJI) }, icon = Icons.Outlined.Mood, selected = currentInputSelector == InputSelector.EMOJI, - description = stringResource(id = R.string.emoji_selector_bt_desc) + description = stringResource(id = R.string.emoji_selector_bt_desc), ) InputSelectorButton( onClick = { onSelectorChange(InputSelector.DM) }, icon = Icons.Outlined.AlternateEmail, selected = currentInputSelector == InputSelector.DM, - description = stringResource(id = R.string.dm_desc) + description = stringResource(id = R.string.dm_desc), ) InputSelectorButton( onClick = { onSelectorChange(InputSelector.PICTURE) }, icon = Icons.Outlined.InsertPhoto, selected = currentInputSelector == InputSelector.PICTURE, - description = stringResource(id = R.string.attach_photo_desc) + description = stringResource(id = R.string.attach_photo_desc), ) InputSelectorButton( onClick = { onSelectorChange(InputSelector.MAP) }, icon = Icons.Outlined.Place, selected = currentInputSelector == InputSelector.MAP, - description = stringResource(id = R.string.map_selector_desc) + description = stringResource(id = R.string.map_selector_desc), ) InputSelectorButton( onClick = { onSelectorChange(InputSelector.PHONE) }, icon = Icons.Outlined.Duo, selected = currentInputSelector == InputSelector.PHONE, - description = stringResource(id = R.string.videochat_desc) + description = stringResource(id = R.string.videochat_desc), ) - val border = if (!sendMessageEnabled) { - BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) - ) - } else { - null - } + val border = + if (!sendMessageEnabled) { + BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), + ) + } else { + null + } Spacer(modifier = Modifier.weight(1f)) val disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) - val buttonColors = ButtonDefaults.buttonColors( - disabledContainerColor = Color.Transparent, - disabledContentColor = disabledContentColor - ) + val buttonColors = + ButtonDefaults.buttonColors( + disabledContainerColor = Color.Transparent, + disabledContentColor = disabledContentColor, + ) // Send button Button( @@ -347,11 +353,11 @@ private fun UserInputSelector( onClick = onMessageSent, colors = buttonColors, border = border, - contentPadding = PaddingValues(0.dp) + contentPadding = PaddingValues(0.dp), ) { Text( stringResource(id = R.string.send), - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier.padding(horizontal = 16.dp), ) } } @@ -363,32 +369,35 @@ private fun InputSelectorButton( icon: ImageVector, description: String, selected: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val backgroundModifier = if (selected) { - Modifier.background( - color = LocalContentColor.current, - shape = RoundedCornerShape(14.dp) - ) - } else { - Modifier - } + val backgroundModifier = + if (selected) { + Modifier.background( + color = LocalContentColor.current, + shape = RoundedCornerShape(14.dp), + ) + } else { + Modifier + } IconButton( onClick = onClick, - modifier = modifier.then(backgroundModifier) + modifier = modifier.then(backgroundModifier), ) { - val tint = if (selected) { - contentColorFor(backgroundColor = LocalContentColor.current) - } else { - LocalContentColor.current - } + val tint = + if (selected) { + contentColorFor(backgroundColor = LocalContentColor.current) + } else { + LocalContentColor.current + } Icon( icon, tint = tint, - modifier = Modifier - .padding(8.dp) - .size(56.dp), - contentDescription = description + modifier = + Modifier + .padding(8.dp) + .size(56.dp), + contentDescription = description, ) } } @@ -411,23 +420,25 @@ private fun UserInputText( keyboardShown: Boolean, onTextFieldFocused: (Boolean) -> Unit, onMessageSent: (String) -> Unit, - focusState: Boolean + focusState: Boolean, ) { val swipeOffset = remember { mutableStateOf(0f) } var isRecordingMessage by remember { mutableStateOf(false) } val a11ylabel = stringResource(id = R.string.textfield_desc) Row( - modifier = Modifier - .fillMaxWidth() - .height(64.dp), - horizontalArrangement = Arrangement.End + modifier = + Modifier + .fillMaxWidth() + .height(64.dp), + horizontalArrangement = Arrangement.End, ) { AnimatedContent( targetState = isRecordingMessage, label = "text-field", - modifier = Modifier - .weight(1f) - .fillMaxHeight() + modifier = + Modifier + .weight(1f) + .fillMaxHeight(), ) { recording -> Box(Modifier.fillMaxSize()) { if (recording) { @@ -443,7 +454,7 @@ private fun UserInputText( Modifier.semantics { contentDescription = a11ylabel keyboardShownProperty = keyboardShown - } + }, ) } } @@ -464,7 +475,7 @@ private fun UserInputText( onCancelRecording = { isRecordingMessage = false }, - modifier = Modifier.fillMaxHeight() + modifier = Modifier.fillMaxHeight(), ) } } @@ -477,42 +488,46 @@ private fun BoxScope.UserInputTextField( keyboardType: KeyboardType, focusState: Boolean, onMessageSent: (String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { var lastFocusState by remember { mutableStateOf(false) } BasicTextField( value = textFieldValue, onValueChange = { onTextChanged(it) }, - modifier = modifier - .padding(start = 32.dp) - .align(Alignment.CenterStart) - .onFocusChanged { state -> - if (lastFocusState != state.isFocused) { - onTextFieldFocused(state.isFocused) - } - lastFocusState = state.isFocused + modifier = + modifier + .padding(start = 32.dp) + .align(Alignment.CenterStart) + .onFocusChanged { state -> + if (lastFocusState != state.isFocused) { + onTextFieldFocused(state.isFocused) + } + lastFocusState = state.isFocused + }, + keyboardOptions = + KeyboardOptions( + keyboardType = keyboardType, + imeAction = ImeAction.Send, + ), + keyboardActions = + KeyboardActions { + if (textFieldValue.text.isNotBlank()) onMessageSent(textFieldValue.text) }, - keyboardOptions = KeyboardOptions( - keyboardType = keyboardType, - imeAction = ImeAction.Send - ), - keyboardActions = KeyboardActions { - if (textFieldValue.text.isNotBlank()) onMessageSent(textFieldValue.text) - }, maxLines = 1, cursorBrush = SolidColor(LocalContentColor.current), - textStyle = LocalTextStyle.current.copy(color = LocalContentColor.current) + textStyle = LocalTextStyle.current.copy(color = LocalContentColor.current), ) val disableContentColor = MaterialTheme.colorScheme.onSurfaceVariant if (textFieldValue.text.isEmpty() && !focusState) { Text( - modifier = Modifier - .align(Alignment.CenterStart) - .padding(start = 32.dp), + modifier = + Modifier + .align(Alignment.CenterStart) + .padding(start = 32.dp), text = stringResource(R.string.textfield_hint), - style = MaterialTheme.typography.bodyLarge.copy(color = disableContentColor) + style = MaterialTheme.typography.bodyLarge.copy(color = disableContentColor), ) } } @@ -528,28 +543,30 @@ private fun RecordingIndicator(swipeOffset: () -> Float) { } Row( Modifier.fillMaxSize(), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { val infiniteTransition = rememberInfiniteTransition(label = "pulse") - val animatedPulse = infiniteTransition.animateFloat( - initialValue = 1f, - targetValue = 0.2f, - animationSpec = infiniteRepeatable( - tween(2000), - repeatMode = RepeatMode.Reverse - ), - label = "pulse", - ) + val animatedPulse = + infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 0.2f, + animationSpec = + infiniteRepeatable( + tween(2000), + repeatMode = RepeatMode.Reverse, + ), + label = "pulse", + ) Box( Modifier .size(56.dp) .padding(24.dp) .graphicsLayer { - scaleX = animatedPulse.value; scaleY = animatedPulse.value - } - .clip(CircleShape) - .background(Color.Red) + scaleX = animatedPulse.value + scaleY = animatedPulse.value + }.clip(CircleShape) + .background(Color.Red), ) Text( duration.toComponents { minutes, seconds, _ -> @@ -557,25 +574,26 @@ private fun RecordingIndicator(swipeOffset: () -> Float) { val sec = seconds.toString().padStart(2, '0') "$min:$sec" }, - Modifier.alignByBaseline() + Modifier.alignByBaseline(), ) Box( Modifier .fillMaxSize() .alignByBaseline() - .clipToBounds() + .clipToBounds(), ) { val swipeThreshold = with(LocalDensity.current) { 200.dp.toPx() } Text( - modifier = Modifier - .align(Alignment.Center) - .graphicsLayer { - translationX = swipeOffset() / 2 - alpha = 1 - (swipeOffset().absoluteValue / swipeThreshold) - }, + modifier = + Modifier + .align(Alignment.Center) + .graphicsLayer { + translationX = swipeOffset() / 2 + alpha = 1 - (swipeOffset().absoluteValue / swipeThreshold) + }, textAlign = TextAlign.Center, text = stringResource(R.string.swipe_to_cancel_recording), - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyLarge, ) } } @@ -584,34 +602,36 @@ private fun RecordingIndicator(swipeOffset: () -> Float) { @Composable fun EmojiSelector( onTextAdded: (String) -> Unit, - focusRequester: FocusRequester + focusRequester: FocusRequester, ) { var selected by remember { mutableStateOf(EmojiStickerSelector.EMOJI) } val a11yLabel = stringResource(id = R.string.emoji_selector_desc) Column( - modifier = Modifier - .focusRequester(focusRequester) // Requests focus when the Emoji selector is displayed - // Make the emoji selector focusable so it can steal focus from TextField - .focusTarget() - .semantics { contentDescription = a11yLabel } + modifier = + Modifier + .focusRequester(focusRequester) // Requests focus when the Emoji selector is displayed + // Make the emoji selector focusable so it can steal focus from TextField + .focusTarget() + .semantics { contentDescription = a11yLabel }, ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), ) { ExtendedSelectorInnerButton( text = stringResource(id = R.string.emojis_label), onClick = { selected = EmojiStickerSelector.EMOJI }, selected = true, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) ExtendedSelectorInnerButton( text = stringResource(id = R.string.stickers_label), onClick = { selected = EmojiStickerSelector.STICKER }, selected = false, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) } Row(modifier = Modifier.verticalScroll(rememberScrollState())) { @@ -628,26 +648,32 @@ fun ExtendedSelectorInnerButton( text: String, onClick: () -> Unit, selected: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val colors = ButtonDefaults.buttonColors( - containerColor = if (selected) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f) - else Color.Transparent, - disabledContainerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onSurface, - disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.74f) - ) + val colors = + ButtonDefaults.buttonColors( + containerColor = + if (selected) { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f) + } else { + Color.Transparent + }, + disabledContainerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.74f), + ) TextButton( onClick = onClick, - modifier = modifier - .padding(8.dp) - .height(36.dp), + modifier = + modifier + .padding(8.dp) + .height(36.dp), colors = colors, - contentPadding = PaddingValues(0.dp) + contentPadding = PaddingValues(0.dp), ) { Text( text = text, - style = MaterialTheme.typography.titleSmall + style = MaterialTheme.typography.titleSmall, ) } } @@ -655,26 +681,28 @@ fun ExtendedSelectorInnerButton( @Composable fun EmojiTable( onTextAdded: (String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Column(modifier.fillMaxWidth()) { repeat(4) { x -> Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly + horizontalArrangement = Arrangement.SpaceEvenly, ) { repeat(EMOJI_COLUMNS) { y -> val emoji = emojis[x * EMOJI_COLUMNS + y] Text( - modifier = Modifier - .clickable(onClick = { onTextAdded(emoji) }) - .sizeIn(minWidth = 42.dp, minHeight = 42.dp) - .padding(8.dp), + modifier = + Modifier + .clickable(onClick = { onTextAdded(emoji) }) + .sizeIn(minWidth = 42.dp, minHeight = 42.dp) + .padding(8.dp), text = emoji, - style = LocalTextStyle.current.copy( - fontSize = 18.sp, - textAlign = TextAlign.Center - ) + style = + LocalTextStyle.current.copy( + fontSize = 18.sp, + textAlign = TextAlign.Center, + ), ) } } @@ -684,133 +712,134 @@ fun EmojiTable( private const val EMOJI_COLUMNS = 10 -private val emojis = listOf( - "\ud83d\ude00", // Grinning Face - "\ud83d\ude01", // Grinning Face With Smiling Eyes - "\ud83d\ude02", // Face With Tears of Joy - "\ud83d\ude03", // Smiling Face With Open Mouth - "\ud83d\ude04", // Smiling Face With Open Mouth and Smiling Eyes - "\ud83d\ude05", // Smiling Face With Open Mouth and Cold Sweat - "\ud83d\ude06", // Smiling Face With Open Mouth and Tightly-Closed Eyes - "\ud83d\ude09", // Winking Face - "\ud83d\ude0a", // Smiling Face With Smiling Eyes - "\ud83d\ude0b", // Face Savouring Delicious Food - "\ud83d\ude0e", // Smiling Face With Sunglasses - "\ud83d\ude0d", // Smiling Face With Heart-Shaped Eyes - "\ud83d\ude18", // Face Throwing a Kiss - "\ud83d\ude17", // Kissing Face - "\ud83d\ude19", // Kissing Face With Smiling Eyes - "\ud83d\ude1a", // Kissing Face With Closed Eyes - "\u263a", // White Smiling Face - "\ud83d\ude42", // Slightly Smiling Face - "\ud83e\udd17", // Hugging Face - "\ud83d\ude07", // Smiling Face With Halo - "\ud83e\udd13", // Nerd Face - "\ud83e\udd14", // Thinking Face - "\ud83d\ude10", // Neutral Face - "\ud83d\ude11", // Expressionless Face - "\ud83d\ude36", // Face Without Mouth - "\ud83d\ude44", // Face With Rolling Eyes - "\ud83d\ude0f", // Smirking Face - "\ud83d\ude23", // Persevering Face - "\ud83d\ude25", // Disappointed but Relieved Face - "\ud83d\ude2e", // Face With Open Mouth - "\ud83e\udd10", // Zipper-Mouth Face - "\ud83d\ude2f", // Hushed Face - "\ud83d\ude2a", // Sleepy Face - "\ud83d\ude2b", // Tired Face - "\ud83d\ude34", // Sleeping Face - "\ud83d\ude0c", // Relieved Face - "\ud83d\ude1b", // Face With Stuck-Out Tongue - "\ud83d\ude1c", // Face With Stuck-Out Tongue and Winking Eye - "\ud83d\ude1d", // Face With Stuck-Out Tongue and Tightly-Closed Eyes - "\ud83d\ude12", // Unamused Face - "\ud83d\ude13", // Face With Cold Sweat - "\ud83d\ude14", // Pensive Face - "\ud83d\ude15", // Confused Face - "\ud83d\ude43", // Upside-Down Face - "\ud83e\udd11", // Money-Mouth Face - "\ud83d\ude32", // Astonished Face - "\ud83d\ude37", // Face With Medical Mask - "\ud83e\udd12", // Face With Thermometer - "\ud83e\udd15", // Face With Head-Bandage - "\u2639", // White Frowning Face - "\ud83d\ude41", // Slightly Frowning Face - "\ud83d\ude16", // Confounded Face - "\ud83d\ude1e", // Disappointed Face - "\ud83d\ude1f", // Worried Face - "\ud83d\ude24", // Face With Look of Triumph - "\ud83d\ude22", // Crying Face - "\ud83d\ude2d", // Loudly Crying Face - "\ud83d\ude26", // Frowning Face With Open Mouth - "\ud83d\ude27", // Anguished Face - "\ud83d\ude28", // Fearful Face - "\ud83d\ude29", // Weary Face - "\ud83d\ude2c", // Grimacing Face - "\ud83d\ude30", // Face With Open Mouth and Cold Sweat - "\ud83d\ude31", // Face Screaming in Fear - "\ud83d\ude33", // Flushed Face - "\ud83d\ude35", // Dizzy Face - "\ud83d\ude21", // Pouting Face - "\ud83d\ude20", // Angry Face - "\ud83d\ude08", // Smiling Face With Horns - "\ud83d\udc7f", // Imp - "\ud83d\udc79", // Japanese Ogre - "\ud83d\udc7a", // Japanese Goblin - "\ud83d\udc80", // Skull - "\ud83d\udc7b", // Ghost - "\ud83d\udc7d", // Extraterrestrial Alien - "\ud83e\udd16", // Robot Face - "\ud83d\udca9", // Pile of Poo - "\ud83d\ude3a", // Smiling Cat Face With Open Mouth - "\ud83d\ude38", // Grinning Cat Face With Smiling Eyes - "\ud83d\ude39", // Cat Face With Tears of Joy - "\ud83d\ude3b", // Smiling Cat Face With Heart-Shaped Eyes - "\ud83d\ude3c", // Cat Face With Wry Smile - "\ud83d\ude3d", // Kissing Cat Face With Closed Eyes - "\ud83d\ude40", // Weary Cat Face - "\ud83d\ude3f", // Crying Cat Face - "\ud83d\ude3e", // Pouting Cat Face - "\ud83d\udc66", // Boy - "\ud83d\udc67", // Girl - "\ud83d\udc68", // Man - "\ud83d\udc69", // Woman - "\ud83d\udc74", // Older Man - "\ud83d\udc75", // Older Woman - "\ud83d\udc76", // Baby - "\ud83d\udc71", // Person With Blond Hair - "\ud83d\udc6e", // Police Officer - "\ud83d\udc72", // Man With Gua Pi Mao - "\ud83d\udc73", // Man With Turban - "\ud83d\udc77", // Construction Worker - "\u26d1", // Helmet With White Cross - "\ud83d\udc78", // Princess - "\ud83d\udc82", // Guardsman - "\ud83d\udd75", // Sleuth or Spy - "\ud83c\udf85", // Father Christmas - "\ud83d\udc70", // Bride With Veil - "\ud83d\udc7c", // Baby Angel - "\ud83d\udc86", // Face Massage - "\ud83d\udc87", // Haircut - "\ud83d\ude4d", // Person Frowning - "\ud83d\ude4e", // Person With Pouting Face - "\ud83d\ude45", // Face With No Good Gesture - "\ud83d\ude46", // Face With OK Gesture - "\ud83d\udc81", // Information Desk Person - "\ud83d\ude4b", // Happy Person Raising One Hand - "\ud83d\ude47", // Person Bowing Deeply - "\ud83d\ude4c", // Person Raising Both Hands in Celebration - "\ud83d\ude4f", // Person With Folded Hands - "\ud83d\udde3", // Speaking Head in Silhouette - "\ud83d\udc64", // Bust in Silhouette - "\ud83d\udc65", // Busts in Silhouette - "\ud83d\udeb6", // Pedestrian - "\ud83c\udfc3", // Runner - "\ud83d\udc6f", // Woman With Bunny Ears - "\ud83d\udc83", // Dancer - "\ud83d\udd74", // Man in Business Suit Levitating - "\ud83d\udc6b", // Man and Woman Holding Hands - "\ud83d\udc6c", // Two Men Holding Hands - "\ud83d\udc6d", // Two Women Holding Hands - "\ud83d\udc8f" // Kiss -) +private val emojis = + listOf( + "\ud83d\ude00", // Grinning Face + "\ud83d\ude01", // Grinning Face With Smiling Eyes + "\ud83d\ude02", // Face With Tears of Joy + "\ud83d\ude03", // Smiling Face With Open Mouth + "\ud83d\ude04", // Smiling Face With Open Mouth and Smiling Eyes + "\ud83d\ude05", // Smiling Face With Open Mouth and Cold Sweat + "\ud83d\ude06", // Smiling Face With Open Mouth and Tightly-Closed Eyes + "\ud83d\ude09", // Winking Face + "\ud83d\ude0a", // Smiling Face With Smiling Eyes + "\ud83d\ude0b", // Face Savouring Delicious Food + "\ud83d\ude0e", // Smiling Face With Sunglasses + "\ud83d\ude0d", // Smiling Face With Heart-Shaped Eyes + "\ud83d\ude18", // Face Throwing a Kiss + "\ud83d\ude17", // Kissing Face + "\ud83d\ude19", // Kissing Face With Smiling Eyes + "\ud83d\ude1a", // Kissing Face With Closed Eyes + "\u263a", // White Smiling Face + "\ud83d\ude42", // Slightly Smiling Face + "\ud83e\udd17", // Hugging Face + "\ud83d\ude07", // Smiling Face With Halo + "\ud83e\udd13", // Nerd Face + "\ud83e\udd14", // Thinking Face + "\ud83d\ude10", // Neutral Face + "\ud83d\ude11", // Expressionless Face + "\ud83d\ude36", // Face Without Mouth + "\ud83d\ude44", // Face With Rolling Eyes + "\ud83d\ude0f", // Smirking Face + "\ud83d\ude23", // Persevering Face + "\ud83d\ude25", // Disappointed but Relieved Face + "\ud83d\ude2e", // Face With Open Mouth + "\ud83e\udd10", // Zipper-Mouth Face + "\ud83d\ude2f", // Hushed Face + "\ud83d\ude2a", // Sleepy Face + "\ud83d\ude2b", // Tired Face + "\ud83d\ude34", // Sleeping Face + "\ud83d\ude0c", // Relieved Face + "\ud83d\ude1b", // Face With Stuck-Out Tongue + "\ud83d\ude1c", // Face With Stuck-Out Tongue and Winking Eye + "\ud83d\ude1d", // Face With Stuck-Out Tongue and Tightly-Closed Eyes + "\ud83d\ude12", // Unamused Face + "\ud83d\ude13", // Face With Cold Sweat + "\ud83d\ude14", // Pensive Face + "\ud83d\ude15", // Confused Face + "\ud83d\ude43", // Upside-Down Face + "\ud83e\udd11", // Money-Mouth Face + "\ud83d\ude32", // Astonished Face + "\ud83d\ude37", // Face With Medical Mask + "\ud83e\udd12", // Face With Thermometer + "\ud83e\udd15", // Face With Head-Bandage + "\u2639", // White Frowning Face + "\ud83d\ude41", // Slightly Frowning Face + "\ud83d\ude16", // Confounded Face + "\ud83d\ude1e", // Disappointed Face + "\ud83d\ude1f", // Worried Face + "\ud83d\ude24", // Face With Look of Triumph + "\ud83d\ude22", // Crying Face + "\ud83d\ude2d", // Loudly Crying Face + "\ud83d\ude26", // Frowning Face With Open Mouth + "\ud83d\ude27", // Anguished Face + "\ud83d\ude28", // Fearful Face + "\ud83d\ude29", // Weary Face + "\ud83d\ude2c", // Grimacing Face + "\ud83d\ude30", // Face With Open Mouth and Cold Sweat + "\ud83d\ude31", // Face Screaming in Fear + "\ud83d\ude33", // Flushed Face + "\ud83d\ude35", // Dizzy Face + "\ud83d\ude21", // Pouting Face + "\ud83d\ude20", // Angry Face + "\ud83d\ude08", // Smiling Face With Horns + "\ud83d\udc7f", // Imp + "\ud83d\udc79", // Japanese Ogre + "\ud83d\udc7a", // Japanese Goblin + "\ud83d\udc80", // Skull + "\ud83d\udc7b", // Ghost + "\ud83d\udc7d", // Extraterrestrial Alien + "\ud83e\udd16", // Robot Face + "\ud83d\udca9", // Pile of Poo + "\ud83d\ude3a", // Smiling Cat Face With Open Mouth + "\ud83d\ude38", // Grinning Cat Face With Smiling Eyes + "\ud83d\ude39", // Cat Face With Tears of Joy + "\ud83d\ude3b", // Smiling Cat Face With Heart-Shaped Eyes + "\ud83d\ude3c", // Cat Face With Wry Smile + "\ud83d\ude3d", // Kissing Cat Face With Closed Eyes + "\ud83d\ude40", // Weary Cat Face + "\ud83d\ude3f", // Crying Cat Face + "\ud83d\ude3e", // Pouting Cat Face + "\ud83d\udc66", // Boy + "\ud83d\udc67", // Girl + "\ud83d\udc68", // Man + "\ud83d\udc69", // Woman + "\ud83d\udc74", // Older Man + "\ud83d\udc75", // Older Woman + "\ud83d\udc76", // Baby + "\ud83d\udc71", // Person With Blond Hair + "\ud83d\udc6e", // Police Officer + "\ud83d\udc72", // Man With Gua Pi Mao + "\ud83d\udc73", // Man With Turban + "\ud83d\udc77", // Construction Worker + "\u26d1", // Helmet With White Cross + "\ud83d\udc78", // Princess + "\ud83d\udc82", // Guardsman + "\ud83d\udd75", // Sleuth or Spy + "\ud83c\udf85", // Father Christmas + "\ud83d\udc70", // Bride With Veil + "\ud83d\udc7c", // Baby Angel + "\ud83d\udc86", // Face Massage + "\ud83d\udc87", // Haircut + "\ud83d\ude4d", // Person Frowning + "\ud83d\ude4e", // Person With Pouting Face + "\ud83d\ude45", // Face With No Good Gesture + "\ud83d\ude46", // Face With OK Gesture + "\ud83d\udc81", // Information Desk Person + "\ud83d\ude4b", // Happy Person Raising One Hand + "\ud83d\ude47", // Person Bowing Deeply + "\ud83d\ude4c", // Person Raising Both Hands in Celebration + "\ud83d\ude4f", // Person With Folded Hands + "\ud83d\udde3", // Speaking Head in Silhouette + "\ud83d\udc64", // Bust in Silhouette + "\ud83d\udc65", // Busts in Silhouette + "\ud83d\udeb6", // Pedestrian + "\ud83c\udfc3", // Runner + "\ud83d\udc6f", // Woman With Bunny Ears + "\ud83d\udc83", // Dancer + "\ud83d\udd74", // Man in Business Suit Levitating + "\ud83d\udc6b", // Man and Woman Holding Hands + "\ud83d\udc6c", // Two Men Holding Hands + "\ud83d\udc6d", // Two Women Holding Hands + "\ud83d\udc8f", // Kiss + ) diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/data/FakeData.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/data/FakeData.kt index 1c996a40d3..70f28898cf 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/data/FakeData.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/data/FakeData.kt @@ -26,80 +26,84 @@ import com.example.compose.jetchat.data.EMOJIS.EMOJI_PINK_HEART import com.example.compose.jetchat.data.EMOJIS.EMOJI_POINTS import com.example.compose.jetchat.profile.ProfileScreenState -private val initialMessages = listOf( - Message( - "me", - "Check it out!", - "8:07 PM" - ), - Message( - "me", - "Thank you!$EMOJI_PINK_HEART", - "8:06 PM", - R.drawable.sticker - ), - Message( - "Taylor Brooks", - "You can use all the same stuff", - "8:05 PM" - ), - Message( - "Taylor Brooks", - "@aliconors Take a look at the `Flow.collectAsStateWithLifecycle()` APIs", - "8:05 PM" - ), - Message( - "John Glenn", - "Compose newbie as well $EMOJI_FLAMINGO, have you looked at the JetNews sample? " + - "Most blog posts end up out of date pretty fast but this sample is always up to " + - "date and deals with async data loading (it's faked but the same idea " + - "applies) $EMOJI_POINTS https://goo.gle/jetnews", - "8:04 PM" - ), - Message( - "me", - "Compose newbie: I’ve scourged the internet for tutorials about async data " + - "loading but haven’t found any good ones $EMOJI_MELTING $EMOJI_CLOUDS. " + - "What’s the recommended way to load async data and emit composable widgets?", - "8:03 PM" +private val initialMessages = + listOf( + Message( + "me", + "Check it out!", + "8:07 PM", + ), + Message( + "me", + "Thank you!$EMOJI_PINK_HEART", + "8:06 PM", + R.drawable.sticker, + ), + Message( + "Taylor Brooks", + "You can use all the same stuff", + "8:05 PM", + ), + Message( + "Taylor Brooks", + "@aliconors Take a look at the `Flow.collectAsStateWithLifecycle()` APIs", + "8:05 PM", + ), + Message( + "John Glenn", + "Compose newbie as well $EMOJI_FLAMINGO, have you looked at the JetNews sample? " + + "Most blog posts end up out of date pretty fast but this sample is always up to " + + "date and deals with async data loading (it's faked but the same idea " + + "applies) $EMOJI_POINTS https://goo.gle/jetnews", + "8:04 PM", + ), + Message( + "me", + "Compose newbie: I’ve scourged the internet for tutorials about async data " + + "loading but haven’t found any good ones $EMOJI_MELTING $EMOJI_CLOUDS. " + + "What’s the recommended way to load async data and emit composable widgets?", + "8:03 PM", + ), ) -) -val exampleUiState = ConversationUiState( - initialMessages = initialMessages, - channelName = "#composers", - channelMembers = 42 -) +val exampleUiState = + ConversationUiState( + initialMessages = initialMessages, + channelName = "#composers", + channelMembers = 42, + ) /** * Example colleague profile */ -val colleagueProfile = ProfileScreenState( - userId = "12345", - photo = R.drawable.someone_else, - name = "Taylor Brooks", - status = "Away", - displayName = "taylor", - position = "Senior Android Dev at Openlane", - twitter = "twitter.com/taylorbrookscodes", - timeZone = "12:25 AM local time (Eastern Daylight Time)", - commonChannels = "2" -) +val colleagueProfile = + ProfileScreenState( + userId = "12345", + photo = R.drawable.someone_else, + name = "Taylor Brooks", + status = "Away", + displayName = "taylor", + position = "Senior Android Dev at Openlane", + twitter = "twitter.com/taylorbrookscodes", + timeZone = "12:25 AM local time (Eastern Daylight Time)", + commonChannels = "2", + ) /** * Example "me" profile. */ -val meProfile = ProfileScreenState( - userId = "me", - photo = R.drawable.ali, - name = "Ali Conors", - status = "Online", - displayName = "aliconors", - position = "Senior Android Dev at Yearin\nGoogle Developer Expert", - twitter = "twitter.com/aliconors", - timeZone = "In your timezone", - commonChannels = null -) +val meProfile = + ProfileScreenState( + userId = "me", + photo = R.drawable.ali, + name = "Ali Conors", + status = "Online", + displayName = "aliconors", + position = "Senior Android Dev at Yearin\nGoogle Developer Expert", + twitter = "twitter.com/aliconors", + timeZone = "In your timezone", + commonChannels = null, + ) object EMOJIS { // EMOJI 15 diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Previews.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Previews.kt index 337abb5a0f..018899bd5f 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Previews.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Previews.kt @@ -45,6 +45,7 @@ fun ProfilePreview480Other() { ProfileScreen(colleagueProfile) } } + @Preview(widthDp = 340, name = "340 width - Me - Dark") @Composable fun ProfilePreview340MeDark() { diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Profile.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Profile.kt index 0bc8c651ae..3a9053d894 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Profile.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Profile.kt @@ -76,7 +76,7 @@ import com.example.compose.jetchat.theme.JetchatTheme @Composable fun ProfileScreen( userData: ProfileScreenState, - nestedScrollInteropConnection: NestedScrollConnection = rememberNestedScrollInteropConnection() + nestedScrollInteropConnection: NestedScrollConnection = rememberNestedScrollInteropConnection(), ) { var functionalityNotAvailablePopupShown by remember { mutableStateOf(false) } if (functionalityNotAvailablePopupShown) { @@ -86,21 +86,23 @@ fun ProfileScreen( val scrollState = rememberScrollState() BoxWithConstraints( - modifier = Modifier - .fillMaxSize() - .nestedScroll(nestedScrollInteropConnection) - .systemBarsPadding() + modifier = + Modifier + .fillMaxSize() + .nestedScroll(nestedScrollInteropConnection) + .systemBarsPadding(), ) { Surface { Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState), + modifier = + Modifier + .fillMaxSize() + .verticalScroll(scrollState), ) { ProfileHeader( scrollState, userData, - this@BoxWithConstraints.maxHeight + this@BoxWithConstraints.maxHeight, ) UserInfoFields(userData, this@BoxWithConstraints.maxHeight) } @@ -110,17 +112,21 @@ fun ProfileScreen( ProfileFab( extended = fabExtended, userIsMe = userData.isMe(), - modifier = Modifier - .align(Alignment.BottomEnd) - // Offsets the FAB to compensate for CoordinatorLayout collapsing behaviour - .offset(y = ((-100).dp)), - onFabClicked = { functionalityNotAvailablePopupShown = true } + modifier = + Modifier + .align(Alignment.BottomEnd) + // Offsets the FAB to compensate for CoordinatorLayout collapsing behaviour + .offset(y = ((-100).dp)), + onFabClicked = { functionalityNotAvailablePopupShown = true }, ) } } @Composable -private fun UserInfoFields(userData: ProfileScreenState, containerHeight: Dp) { +private fun UserInfoFields( + userData: ProfileScreenState, + containerHeight: Dp, +) { Column { Spacer(modifier = Modifier.height(8.dp)) @@ -143,39 +149,44 @@ private fun UserInfoFields(userData: ProfileScreenState, containerHeight: Dp) { } @Composable -private fun NameAndPosition( - userData: ProfileScreenState -) { +private fun NameAndPosition(userData: ProfileScreenState) { Column(modifier = Modifier.padding(horizontal = 16.dp)) { Name( userData, - modifier = Modifier.baselineHeight(32.dp) + modifier = Modifier.baselineHeight(32.dp), ) Position( userData, - modifier = Modifier - .padding(bottom = 20.dp) - .baselineHeight(24.dp) + modifier = + Modifier + .padding(bottom = 20.dp) + .baselineHeight(24.dp), ) } } @Composable -private fun Name(userData: ProfileScreenState, modifier: Modifier = Modifier) { +private fun Name( + userData: ProfileScreenState, + modifier: Modifier = Modifier, +) { Text( text = userData.name, modifier = modifier, - style = MaterialTheme.typography.headlineSmall + style = MaterialTheme.typography.headlineSmall, ) } @Composable -private fun Position(userData: ProfileScreenState, modifier: Modifier = Modifier) { +private fun Position( + userData: ProfileScreenState, + modifier: Modifier = Modifier, +) { Text( text = userData.position, modifier = modifier, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -183,49 +194,54 @@ private fun Position(userData: ProfileScreenState, modifier: Modifier = Modifier private fun ProfileHeader( scrollState: ScrollState, data: ProfileScreenState, - containerHeight: Dp + containerHeight: Dp, ) { val offset = (scrollState.value / 2) val offsetDp = with(LocalDensity.current) { offset.toDp() } data.photo?.let { Image( - modifier = Modifier - .heightIn(max = containerHeight / 2) - .fillMaxWidth() - // TODO: Update to use offset to avoid recomposition - .padding( - start = 16.dp, - top = offsetDp, - end = 16.dp - ) - .clip(CircleShape), + modifier = + Modifier + .heightIn(max = containerHeight / 2) + .fillMaxWidth() + // TODO: Update to use offset to avoid recomposition + .padding( + start = 16.dp, + top = offsetDp, + end = 16.dp, + ).clip(CircleShape), painter = painterResource(id = it), contentScale = ContentScale.Crop, - contentDescription = null + contentDescription = null, ) } } @Composable -fun ProfileProperty(label: String, value: String, isLink: Boolean = false) { +fun ProfileProperty( + label: String, + value: String, + isLink: Boolean = false, +) { Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)) { Divider() Text( text = label, modifier = Modifier.baselineHeight(24.dp), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - val style = if (isLink) { - MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary) - } else { - MaterialTheme.typography.bodyLarge - } + val style = + if (isLink) { + MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary) + } else { + MaterialTheme.typography.bodyLarge + } Text( text = value, modifier = Modifier.baselineHeight(24.dp), - style = style + style = style, ) } } @@ -240,35 +256,39 @@ fun ProfileFab( extended: Boolean, userIsMe: Boolean, modifier: Modifier = Modifier, - onFabClicked: () -> Unit = { } + onFabClicked: () -> Unit = { }, ) { - key(userIsMe) { // Prevent multiple invocations to execute during composition + key(userIsMe) { + // Prevent multiple invocations to execute during composition FloatingActionButton( onClick = onFabClicked, - modifier = modifier - .padding(16.dp) - .navigationBarsPadding() - .height(48.dp) - .widthIn(min = 48.dp), - containerColor = MaterialTheme.colorScheme.tertiaryContainer + modifier = + modifier + .padding(16.dp) + .navigationBarsPadding() + .height(48.dp) + .widthIn(min = 48.dp), + containerColor = MaterialTheme.colorScheme.tertiaryContainer, ) { AnimatingFabContent( icon = { Icon( imageVector = if (userIsMe) Icons.Outlined.Create else Icons.Outlined.Chat, - contentDescription = stringResource( - if (userIsMe) R.string.edit_profile else R.string.message - ) + contentDescription = + stringResource( + if (userIsMe) R.string.edit_profile else R.string.message, + ), ) }, text = { Text( - text = stringResource( - id = if (userIsMe) R.string.edit_profile else R.string.message - ), + text = + stringResource( + id = if (userIsMe) R.string.edit_profile else R.string.message, + ), ) }, - extended = extended + extended = extended, ) } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/ProfileFragment.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/ProfileFragment.kt index 971a008772..08fb232688 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/ProfileFragment.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/ProfileFragment.kt @@ -51,7 +51,6 @@ import com.example.compose.jetchat.components.JetchatAppBar import com.example.compose.jetchat.theme.JetchatTheme class ProfileFragment : Fragment() { - private val viewModel: ProfileViewModel by viewModels() private val activityViewModel: MainViewModel by activityViewModels() @@ -66,7 +65,7 @@ class ProfileFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { val rootView: View = inflater.inflate(R.layout.fragment_profile, container, false) @@ -88,15 +87,16 @@ class ProfileFragment : Fragment() { Icon( imageVector = Icons.Outlined.MoreVert, tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .clickable(onClick = { - functionalityNotAvailablePopupShown = true - }) - .padding(horizontal = 12.dp, vertical = 16.dp) - .height(24.dp), - contentDescription = stringResource(id = R.string.more_options) + modifier = + Modifier + .clickable(onClick = { + functionalityNotAvailablePopupShown = true + }) + .padding(horizontal = 12.dp, vertical = 16.dp) + .height(24.dp), + contentDescription = stringResource(id = R.string.more_options), ) - } + }, ) } } @@ -113,7 +113,7 @@ class ProfileFragment : Fragment() { } else { ProfileScreen( userData = userData!!, - nestedScrollInteropConnection = nestedScrollInteropConnection + nestedScrollInteropConnection = nestedScrollInteropConnection, ) } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/ProfileViewModel.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/ProfileViewModel.kt index bbb7b8d823..2e6d8ca3a5 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/ProfileViewModel.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/ProfileViewModel.kt @@ -25,7 +25,6 @@ import com.example.compose.jetchat.data.colleagueProfile import com.example.compose.jetchat.data.meProfile class ProfileViewModel : ViewModel() { - private var userId: String = "" fun setUserId(newUserId: String?) { @@ -33,11 +32,12 @@ class ProfileViewModel : ViewModel() { userId = newUserId ?: meProfile.userId } // Workaround for simplicity - _userData.value = if (userId == meProfile.userId || userId == meProfile.displayName) { - meProfile - } else { - colleagueProfile - } + _userData.value = + if (userId == meProfile.userId || userId == meProfile.displayName) { + meProfile + } else { + colleagueProfile + } } private val _userData = MutableLiveData() @@ -53,8 +53,10 @@ data class ProfileScreenState( val displayName: String, val position: String, val twitter: String = "", - val timeZone: String?, // Null if me - val commonChannels: String? // Null if me + // value is null if me + val timeZone: String?, + // value is null if me + val commonChannels: String?, ) { fun isMe() = userId == meProfile.userId } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Themes.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Themes.kt index 667ef184f8..d7df847c32 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Themes.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Themes.kt @@ -28,86 +28,89 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -private val JetchatDarkColorScheme = darkColorScheme( - primary = Blue80, - onPrimary = Blue20, - primaryContainer = Blue30, - onPrimaryContainer = Blue90, - inversePrimary = Blue40, - secondary = DarkBlue80, - onSecondary = DarkBlue20, - secondaryContainer = DarkBlue30, - onSecondaryContainer = DarkBlue90, - tertiary = Yellow80, - onTertiary = Yellow20, - tertiaryContainer = Yellow30, - onTertiaryContainer = Yellow90, - error = Red80, - onError = Red20, - errorContainer = Red30, - onErrorContainer = Red90, - background = Grey10, - onBackground = Grey90, - surface = Grey10, - onSurface = Grey80, - inverseSurface = Grey90, - inverseOnSurface = Grey20, - surfaceVariant = BlueGrey30, - onSurfaceVariant = BlueGrey80, - outline = BlueGrey60 -) +private val JetchatDarkColorScheme = + darkColorScheme( + primary = Blue80, + onPrimary = Blue20, + primaryContainer = Blue30, + onPrimaryContainer = Blue90, + inversePrimary = Blue40, + secondary = DarkBlue80, + onSecondary = DarkBlue20, + secondaryContainer = DarkBlue30, + onSecondaryContainer = DarkBlue90, + tertiary = Yellow80, + onTertiary = Yellow20, + tertiaryContainer = Yellow30, + onTertiaryContainer = Yellow90, + error = Red80, + onError = Red20, + errorContainer = Red30, + onErrorContainer = Red90, + background = Grey10, + onBackground = Grey90, + surface = Grey10, + onSurface = Grey80, + inverseSurface = Grey90, + inverseOnSurface = Grey20, + surfaceVariant = BlueGrey30, + onSurfaceVariant = BlueGrey80, + outline = BlueGrey60, + ) -private val JetchatLightColorScheme = lightColorScheme( - primary = Blue40, - onPrimary = Color.White, - primaryContainer = Blue90, - onPrimaryContainer = Blue10, - inversePrimary = Blue80, - secondary = DarkBlue40, - onSecondary = Color.White, - secondaryContainer = DarkBlue90, - onSecondaryContainer = DarkBlue10, - tertiary = Yellow40, - onTertiary = Color.White, - tertiaryContainer = Yellow90, - onTertiaryContainer = Yellow10, - error = Red40, - onError = Color.White, - errorContainer = Red90, - onErrorContainer = Red10, - background = Grey99, - onBackground = Grey10, - surface = Grey99, - onSurface = Grey10, - inverseSurface = Grey20, - inverseOnSurface = Grey95, - surfaceVariant = BlueGrey90, - onSurfaceVariant = BlueGrey30, - outline = BlueGrey50 -) +private val JetchatLightColorScheme = + lightColorScheme( + primary = Blue40, + onPrimary = Color.White, + primaryContainer = Blue90, + onPrimaryContainer = Blue10, + inversePrimary = Blue80, + secondary = DarkBlue40, + onSecondary = Color.White, + secondaryContainer = DarkBlue90, + onSecondaryContainer = DarkBlue10, + tertiary = Yellow40, + onTertiary = Color.White, + tertiaryContainer = Yellow90, + onTertiaryContainer = Yellow10, + error = Red40, + onError = Color.White, + errorContainer = Red90, + onErrorContainer = Red10, + background = Grey99, + onBackground = Grey10, + surface = Grey99, + onSurface = Grey10, + inverseSurface = Grey20, + inverseOnSurface = Grey95, + surfaceVariant = BlueGrey90, + onSurfaceVariant = BlueGrey30, + outline = BlueGrey50, + ) @SuppressLint("NewApi") @Composable fun JetchatTheme( isDarkTheme: Boolean = isSystemInDarkTheme(), isDynamicColor: Boolean = true, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - val myColorScheme = when { - dynamicColor && isDarkTheme -> { - dynamicDarkColorScheme(LocalContext.current) - } - dynamicColor && !isDarkTheme -> { - dynamicLightColorScheme(LocalContext.current) + val myColorScheme = + when { + dynamicColor && isDarkTheme -> { + dynamicDarkColorScheme(LocalContext.current) + } + dynamicColor && !isDarkTheme -> { + dynamicLightColorScheme(LocalContext.current) + } + isDarkTheme -> JetchatDarkColorScheme + else -> JetchatLightColorScheme } - isDarkTheme -> JetchatDarkColorScheme - else -> JetchatLightColorScheme - } MaterialTheme( colorScheme = myColorScheme, typography = JetchatTypography, - content = content + content = content, ) } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Typography.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Typography.kt index 035d179988..672107c7bc 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Typography.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Typography.kt @@ -26,138 +26,157 @@ import androidx.compose.ui.text.googlefonts.GoogleFont import androidx.compose.ui.unit.sp import com.example.compose.jetchat.R -val provider = GoogleFont.Provider( - providerAuthority = "com.google.android.gms.fonts", - providerPackage = "com.google.android.gms", - certificates = R.array.com_google_android_gms_fonts_certs -) +val provider = + GoogleFont.Provider( + providerAuthority = "com.google.android.gms.fonts", + providerPackage = "com.google.android.gms", + certificates = R.array.com_google_android_gms_fonts_certs, + ) val MontserratFont = GoogleFont(name = "Montserrat") val KarlaFont = GoogleFont(name = "Karla") -val MontserratFontFamily = FontFamily( - Font(googleFont = MontserratFont, fontProvider = provider), - Font(resId = R.font.montserrat_regular), - Font(googleFont = MontserratFont, fontProvider = provider, weight = FontWeight.Light), - Font(resId = R.font.montserrat_light, weight = FontWeight.Light), - Font(googleFont = MontserratFont, fontProvider = provider, weight = FontWeight.Medium), - Font(resId = R.font.montserrat_medium, weight = FontWeight.Medium), - Font(googleFont = MontserratFont, fontProvider = provider, weight = FontWeight.SemiBold), - Font(resId = R.font.montserrat_semibold, weight = FontWeight.SemiBold), -) +val MontserratFontFamily = + FontFamily( + Font(googleFont = MontserratFont, fontProvider = provider), + Font(resId = R.font.montserrat_regular), + Font(googleFont = MontserratFont, fontProvider = provider, weight = FontWeight.Light), + Font(resId = R.font.montserrat_light, weight = FontWeight.Light), + Font(googleFont = MontserratFont, fontProvider = provider, weight = FontWeight.Medium), + Font(resId = R.font.montserrat_medium, weight = FontWeight.Medium), + Font(googleFont = MontserratFont, fontProvider = provider, weight = FontWeight.SemiBold), + Font(resId = R.font.montserrat_semibold, weight = FontWeight.SemiBold), + ) -val KarlaFontFamily = FontFamily( - Font(googleFont = KarlaFont, fontProvider = provider), - Font(resId = R.font.karla_regular), - Font(googleFont = KarlaFont, fontProvider = provider, weight = FontWeight.Bold), - Font(resId = R.font.karla_bold, weight = FontWeight.Bold), -) +val KarlaFontFamily = + FontFamily( + Font(googleFont = KarlaFont, fontProvider = provider), + Font(resId = R.font.karla_regular), + Font(googleFont = KarlaFont, fontProvider = provider, weight = FontWeight.Bold), + Font(resId = R.font.karla_bold, weight = FontWeight.Bold), + ) -val JetchatTypography = Typography( - displayLarge = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.Light, - fontSize = 57.sp, - lineHeight = 64.sp, - letterSpacing = 0.sp - ), - displayMedium = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.Light, - fontSize = 45.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp - ), - displaySmall = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.Normal, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp - ), - headlineLarge = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp - ), - headlineMedium = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp - ), - headlineSmall = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.sp - ), - titleLarge = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - titleMedium = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - titleSmall = TextStyle( - fontFamily = KarlaFontFamily, - fontWeight = FontWeight.Bold, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - bodyLarge = TextStyle( - fontFamily = KarlaFontFamily, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - bodyMedium = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp - ), - bodySmall = TextStyle( - fontFamily = KarlaFontFamily, - fontWeight = FontWeight.Bold, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp - ), - labelLarge = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - labelMedium = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ), - labelSmall = TextStyle( - fontFamily = MontserratFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp +val JetchatTypography = + Typography( + displayLarge = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.Light, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = 0.sp, + ), + displayMedium = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.Light, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp, + ), + displaySmall = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp, + ), + headlineLarge = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + ), + headlineMedium = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp, + ), + headlineSmall = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp, + ), + titleLarge = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp, + ), + titleMedium = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + ), + titleSmall = + TextStyle( + fontFamily = KarlaFontFamily, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + bodyLarge = + TextStyle( + fontFamily = KarlaFontFamily, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + ), + bodyMedium = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + bodySmall = + TextStyle( + fontFamily = KarlaFontFamily, + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + ), + labelLarge = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + labelMedium = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), + labelSmall = + TextStyle( + fontFamily = MontserratFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), ) -) diff --git a/Jetchat/build.gradle.kts b/Jetchat/build.gradle.kts index 08ccea3e70..a776efc594 100644 --- a/Jetchat/build.gradle.kts +++ b/Jetchat/build.gradle.kts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import com.diffplug.gradle.spotless.SpotlessExtension plugins { alias(libs.plugins.gradle.versions) @@ -21,6 +22,30 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.compose) apply false + alias(libs.plugins.spotless) } apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") + +subprojects { + apply { + plugin(rootProject.libs.plugins.spotless.get().pluginId) + } + configure { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt") + ktlint(libs.versions.ktlint.get()) + .setEditorConfigPath("${project.rootDir}/.editorconfig") + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } + // Additional configuration for Kotlin Gradle scripts + kotlinGradle { + target("**/*.kts") + targetExclude("**/build/**/*.kts") + ktlint(libs.versions.ktlint.get()) // Apply ktlint to Gradle Kotlin scripts + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") + } + } +} diff --git a/Jetchat/buildscripts/init.gradle.kts b/Jetchat/buildscripts/init.gradle.kts deleted file mode 100644 index 1b7a54264c..0000000000 --- a/Jetchat/buildscripts/init.gradle.kts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -val ktlintVersion = "0.46.1" - -initscript { - val spotlessVersion = "6.10.0" - - repositories { - mavenCentral() - } - - dependencies { - classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion") - } -} - -allprojects { - if (this == rootProject) { - return@allprojects - } - apply() - extensions.configure { - kotlin { - target("**/*.kt") - targetExclude("**/build/**/*.kt") - ktlint(ktlintVersion).editorConfigOverride( - mapOf( - "ktlint_code_style" to "android", - "ij_kotlin_allow_trailing_comma" to true, - // These rules were introduced in ktlint 0.46.0 and should not be - // enabled without further discussion. They are disabled for now. - // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 - "disabled_rules" to - "filename," + - "annotation,annotation-spacing," + - "argument-list-wrapping," + - "double-colon-spacing," + - "enum-entry-name-case," + - "multiline-if-else," + - "no-empty-first-line-in-method-block," + - "package-name," + - "trailing-comma," + - "spacing-around-angle-brackets," + - "spacing-between-declarations-with-annotations," + - "spacing-between-declarations-with-comments," + - "unary-op-spacing" - ) - ) - licenseHeaderFile(rootProject.file("spotless/copyright.kt")) - } - format("kts") { - target("**/*.kts") - targetExclude("**/build/**/*.kts") - // Look for the first line that doesn't have a block comment (assumed to be the license) - licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") - } - } -} \ No newline at end of file diff --git a/Jetchat/gradle/libs.versions.toml b/Jetchat/gradle/libs.versions.toml index 2c34e54e54..016f154a0d 100644 --- a/Jetchat/gradle/libs.versions.toml +++ b/Jetchat/gradle/libs.versions.toml @@ -47,6 +47,7 @@ junit = "4.13.2" kotlin = "2.0.0" kotlinx_immutable = "0.3.7" ksp = "2.0.0-1.0.21" +ktlint = "1.3.1" maps-compose = "3.1.1" # @keep minSdk = "21" @@ -57,6 +58,7 @@ roborazzi = "1.12.0" rome = "1.18.0" room = "2.6.0" secrets = "2.0.1" +spotless = "6.25.0" # @keep targetSdk = "33" version-catalog-update = "0.8.4" @@ -179,4 +181,5 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } diff --git a/Jetsnack/.editorconfig b/Jetsnack/.editorconfig new file mode 100644 index 0000000000..43f0af8237 --- /dev/null +++ b/Jetsnack/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{kt,kts}] +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL + +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ktlint_standard_property-naming = disabled +ktlint_function_naming_ignore_when_annotated_with = Composable diff --git a/Jetsnack/app/build.gradle.kts b/Jetsnack/app/build.gradle.kts index 18a9ce6d51..daf2ea3770 100644 --- a/Jetsnack/app/build.gradle.kts +++ b/Jetsnack/app/build.gradle.kts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,21 +14,32 @@ * limitations under the License. */ + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.compose) + alias(libs.plugins.spotless) } android { - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() namespace = "com.example.jetsnack" defaultConfig { applicationId = "com.example.jetsnack" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() + targetSdk = + libs.versions.targetSdk + .get() + .toInt() versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -49,7 +60,6 @@ android { buildTypes { getByName("debug") { - } getByName("release") { @@ -57,7 +67,7 @@ android { signingConfig = signingConfigs.getByName("release") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } @@ -67,7 +77,7 @@ android { matchingFallbacks.add("release") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-benchmark-rules.pro" + "proguard-benchmark-rules.pro", ) isDebuggable = false } diff --git a/Jetsnack/app/src/androidTest/java/com/example/jetsnack/AppTest.kt b/Jetsnack/app/src/androidTest/java/com/example/jetsnack/AppTest.kt index 646a3d6a4f..6ee3ea9a04 100644 --- a/Jetsnack/app/src/androidTest/java/com/example/jetsnack/AppTest.kt +++ b/Jetsnack/app/src/androidTest/java/com/example/jetsnack/AppTest.kt @@ -25,7 +25,6 @@ import org.junit.Rule import org.junit.Test class AppTest { - @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/model/Filter.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/model/Filter.kt index 8b0e8ddf9c..c857bf63db 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/model/Filter.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/model/Filter.kt @@ -28,42 +28,47 @@ import androidx.compose.ui.graphics.vector.ImageVector class Filter( val name: String, enabled: Boolean = false, - val icon: ImageVector? = null + val icon: ImageVector? = null, ) { val enabled = mutableStateOf(enabled) } -val filters = listOf( - Filter(name = "Organic"), - Filter(name = "Gluten-free"), - Filter(name = "Dairy-free"), - Filter(name = "Sweet"), - Filter(name = "Savory") -) -val priceFilters = listOf( - Filter(name = "$"), - Filter(name = "$$"), - Filter(name = "$$$"), - Filter(name = "$$$$") -) -val sortFilters = listOf( - Filter(name = "Android's favorite (default)", icon = Icons.Filled.Android), - Filter(name = "Rating", icon = Icons.Filled.Star), - Filter(name = "Alphabetical", icon = Icons.Filled.SortByAlpha) -) +val filters = + listOf( + Filter(name = "Organic"), + Filter(name = "Gluten-free"), + Filter(name = "Dairy-free"), + Filter(name = "Sweet"), + Filter(name = "Savory"), + ) +val priceFilters = + listOf( + Filter(name = "$"), + Filter(name = "$$"), + Filter(name = "$$$"), + Filter(name = "$$$$"), + ) +val sortFilters = + listOf( + Filter(name = "Android's favorite (default)", icon = Icons.Filled.Android), + Filter(name = "Rating", icon = Icons.Filled.Star), + Filter(name = "Alphabetical", icon = Icons.Filled.SortByAlpha), + ) -val categoryFilters = listOf( - Filter(name = "Chips & crackers"), - Filter(name = "Fruit snacks"), - Filter(name = "Desserts"), - Filter(name = "Nuts") -) -val lifeStyleFilters = listOf( - Filter(name = "Organic"), - Filter(name = "Gluten-free"), - Filter(name = "Dairy-free"), - Filter(name = "Sweet"), - Filter(name = "Savory") -) +val categoryFilters = + listOf( + Filter(name = "Chips & crackers"), + Filter(name = "Fruit snacks"), + Filter(name = "Desserts"), + Filter(name = "Nuts"), + ) +val lifeStyleFilters = + listOf( + Filter(name = "Organic"), + Filter(name = "Gluten-free"), + Filter(name = "Dairy-free"), + Filter(name = "Sweet"), + Filter(name = "Savory"), + ) var sortDefault = sortFilters.get(0).name diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/model/Search.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/model/Search.kt index 28eda384f0..74f86423b5 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/model/Search.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/model/Search.kt @@ -27,112 +27,120 @@ import kotlinx.coroutines.withContext */ object SearchRepo { fun getCategories(): List = searchCategoryCollections + fun getSuggestions(): List = searchSuggestions - suspend fun search(query: String): List = withContext(Dispatchers.Default) { - delay(200L) // simulate an I/O delay - snacks.filter { it.name.contains(query, ignoreCase = true) } - } + suspend fun search(query: String): List = + withContext(Dispatchers.Default) { + delay(200L) // simulate an I/O delay + snacks.filter { it.name.contains(query, ignoreCase = true) } + } } @Immutable data class SearchCategoryCollection( val id: Long, val name: String, - val categories: List + val categories: List, ) @Immutable data class SearchCategory( val name: String, - val imageRes: Int + val imageRes: Int, ) @Immutable data class SearchSuggestionGroup( val id: Long, val name: String, - val suggestions: List + val suggestions: List, ) /** * Static data */ -private val searchCategoryCollections = listOf( - SearchCategoryCollection( - id = 0L, - name = "Categories", - categories = listOf( - SearchCategory( - name = "Chips & crackers", - imageRes = R.drawable.chips - ), - SearchCategory( - name = "Fruit snacks", - imageRes = R.drawable.fruit, - ), - SearchCategory( - name = "Desserts", - imageRes = R.drawable.desserts - ), - SearchCategory( - name = "Nuts", - imageRes = R.drawable.nuts, - ) - ) - ), - SearchCategoryCollection( - id = 1L, - name = "Lifestyles", - categories = listOf( - SearchCategory( - name = "Organic", - imageRes = R.drawable.organic - ), - SearchCategory( - name = "Gluten Free", - imageRes = R.drawable.gluten_free - ), - SearchCategory( - name = "Paleo", - imageRes = R.drawable.paleo, - ), - SearchCategory( - name = "Vegan", - imageRes = R.drawable.vegan, - ), - SearchCategory( - name = "Vegetarian", - imageRes = R.drawable.organic, - ), - SearchCategory( - name = "Whole30", - imageRes = R.drawable.paleo - ) - ) +private val searchCategoryCollections = + listOf( + SearchCategoryCollection( + id = 0L, + name = "Categories", + categories = + listOf( + SearchCategory( + name = "Chips & crackers", + imageRes = R.drawable.chips, + ), + SearchCategory( + name = "Fruit snacks", + imageRes = R.drawable.fruit, + ), + SearchCategory( + name = "Desserts", + imageRes = R.drawable.desserts, + ), + SearchCategory( + name = "Nuts", + imageRes = R.drawable.nuts, + ), + ), + ), + SearchCategoryCollection( + id = 1L, + name = "Lifestyles", + categories = + listOf( + SearchCategory( + name = "Organic", + imageRes = R.drawable.organic, + ), + SearchCategory( + name = "Gluten Free", + imageRes = R.drawable.gluten_free, + ), + SearchCategory( + name = "Paleo", + imageRes = R.drawable.paleo, + ), + SearchCategory( + name = "Vegan", + imageRes = R.drawable.vegan, + ), + SearchCategory( + name = "Vegetarian", + imageRes = R.drawable.organic, + ), + SearchCategory( + name = "Whole30", + imageRes = R.drawable.paleo, + ), + ), + ), ) -) -private val searchSuggestions = listOf( - SearchSuggestionGroup( - id = 0L, - name = "Recent searches", - suggestions = listOf( - "Cheese", - "Apple Sauce" - ) - ), - SearchSuggestionGroup( - id = 1L, - name = "Popular searches", - suggestions = listOf( - "Organic", - "Gluten Free", - "Paleo", - "Vegan", - "Vegitarian", - "Whole30" - ) +private val searchSuggestions = + listOf( + SearchSuggestionGroup( + id = 0L, + name = "Recent searches", + suggestions = + listOf( + "Cheese", + "Apple Sauce", + ), + ), + SearchSuggestionGroup( + id = 1L, + name = "Popular searches", + suggestions = + listOf( + "Organic", + "Gluten Free", + "Paleo", + "Vegan", + "Vegitarian", + "Whole30", + ), + ), ) -) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/model/Snack.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/model/Snack.kt index 48fea46f1f..e706af1508 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/model/Snack.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/model/Snack.kt @@ -29,202 +29,203 @@ data class Snack( val imageRes: Int, val price: Long, val tagline: String = "", - val tags: Set = emptySet() + val tags: Set = emptySet(), ) /** * Static data */ -val snacks = listOf( - Snack( - id = 1L, - name = "Cupcake", - tagline = "A tag line", - imageRes = R.drawable.cupcake, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Donut", - tagline = "A tag line", - imageRes = R.drawable.donut, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Eclair", - tagline = "A tag line", - imageRes = R.drawable.eclair, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Froyo", - tagline = "A tag line", - imageRes = R.drawable.froyo, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Gingerbread", - tagline = "A tag line", - imageRes = R.drawable.gingerbread, - price = 499 - ), - Snack( - id = Random.nextLong(), - name = "Honeycomb", - tagline = "A tag line", - imageRes = R.drawable.honeycomb, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Ice Cream Sandwich", - tagline = "A tag line", - imageRes = R.drawable.ice_cream_sandwich, - price = 1299 - ), - Snack( - id = Random.nextLong(), - name = "Jellybean", - tagline = "A tag line", - imageRes = R.drawable.jelly_bean, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "KitKat", - tagline = "A tag line", - imageRes = R.drawable.kitkat, - price = 549 - ), - Snack( - id = Random.nextLong(), - name = "Lollipop", - tagline = "A tag line", - imageRes = R.drawable.lollipop, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Marshmallow", - tagline = "A tag line", - imageRes = R.drawable.marshmallow, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Nougat", - tagline = "A tag line", - imageRes = R.drawable.nougat, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Oreo", - tagline = "A tag line", - imageRes = R.drawable.oreo, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Pie", - tagline = "A tag line", - imageRes = R.drawable.pie, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Chips", - imageRes = R.drawable.chips, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Pretzels", - imageRes = R.drawable.pretzels, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Smoothies", - imageRes = R.drawable.smoothies, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Popcorn", - imageRes = R.drawable.popcorn, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Almonds", - imageRes = R.drawable.almonds, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Cheese", - imageRes = R.drawable.cheese, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Apples", - tagline = "A tag line", - imageRes = R.drawable.apples, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Apple sauce", - tagline = "A tag line", - imageRes = R.drawable.apple_sauce, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Apple chips", - tagline = "A tag line", - imageRes = R.drawable.apple_chips, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Apple juice", - tagline = "A tag line", - imageRes = R.drawable.apple_juice, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Apple pie", - tagline = "A tag line", - imageRes = R.drawable.apple_pie, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Grapes", - tagline = "A tag line", - imageRes = R.drawable.grapes, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Kiwi", - tagline = "A tag line", - imageRes = R.drawable.kiwi, - price = 299 - ), - Snack( - id = Random.nextLong(), - name = "Mango", - tagline = "A tag line", - imageRes = R.drawable.mango, - price = 299 +val snacks = + listOf( + Snack( + id = 1L, + name = "Cupcake", + tagline = "A tag line", + imageRes = R.drawable.cupcake, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Donut", + tagline = "A tag line", + imageRes = R.drawable.donut, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Eclair", + tagline = "A tag line", + imageRes = R.drawable.eclair, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Froyo", + tagline = "A tag line", + imageRes = R.drawable.froyo, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Gingerbread", + tagline = "A tag line", + imageRes = R.drawable.gingerbread, + price = 499, + ), + Snack( + id = Random.nextLong(), + name = "Honeycomb", + tagline = "A tag line", + imageRes = R.drawable.honeycomb, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Ice Cream Sandwich", + tagline = "A tag line", + imageRes = R.drawable.ice_cream_sandwich, + price = 1299, + ), + Snack( + id = Random.nextLong(), + name = "Jellybean", + tagline = "A tag line", + imageRes = R.drawable.jelly_bean, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "KitKat", + tagline = "A tag line", + imageRes = R.drawable.kitkat, + price = 549, + ), + Snack( + id = Random.nextLong(), + name = "Lollipop", + tagline = "A tag line", + imageRes = R.drawable.lollipop, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Marshmallow", + tagline = "A tag line", + imageRes = R.drawable.marshmallow, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Nougat", + tagline = "A tag line", + imageRes = R.drawable.nougat, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Oreo", + tagline = "A tag line", + imageRes = R.drawable.oreo, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Pie", + tagline = "A tag line", + imageRes = R.drawable.pie, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Chips", + imageRes = R.drawable.chips, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Pretzels", + imageRes = R.drawable.pretzels, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Smoothies", + imageRes = R.drawable.smoothies, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Popcorn", + imageRes = R.drawable.popcorn, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Almonds", + imageRes = R.drawable.almonds, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Cheese", + imageRes = R.drawable.cheese, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Apples", + tagline = "A tag line", + imageRes = R.drawable.apples, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Apple sauce", + tagline = "A tag line", + imageRes = R.drawable.apple_sauce, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Apple chips", + tagline = "A tag line", + imageRes = R.drawable.apple_chips, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Apple juice", + tagline = "A tag line", + imageRes = R.drawable.apple_juice, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Apple pie", + tagline = "A tag line", + imageRes = R.drawable.apple_pie, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Grapes", + tagline = "A tag line", + imageRes = R.drawable.grapes, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Kiwi", + tagline = "A tag line", + imageRes = R.drawable.kiwi, + price = 299, + ), + Snack( + id = Random.nextLong(), + name = "Mango", + tagline = "A tag line", + imageRes = R.drawable.mango, + price = 299, + ), ) -) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackCollection.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackCollection.kt index ad12b40284..af894b05a0 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackCollection.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackCollection.kt @@ -24,7 +24,7 @@ data class SnackCollection( val id: Long, val name: String, val snacks: List, - val type: CollectionType = CollectionType.Normal + val type: CollectionType = CollectionType.Normal, ) enum class CollectionType { Normal, Highlight } @@ -34,15 +34,27 @@ enum class CollectionType { Normal, Highlight } */ object SnackRepo { fun getSnacks(): List = snackCollections + fun getSnack(snackId: Long) = snacks.find { it.id == snackId }!! - fun getRelated(@Suppress("UNUSED_PARAMETER") snackId: Long) = related + + fun getRelated( + @Suppress("UNUSED_PARAMETER") snackId: Long, + ) = related + fun getInspiredByCart() = inspiredByCart + fun getFilters() = filters + fun getPriceFilters() = priceFilters + fun getCart() = cart + fun getSortFilters() = sortFilters + fun getCategoryFilters() = categoryFilters + fun getSortDefault() = sortDefault + fun getLifeStyleFilters() = lifeStyleFilters } @@ -50,65 +62,75 @@ object SnackRepo { * Static data */ -private val tastyTreats = SnackCollection( - id = 1L, - name = "Android's picks", - type = CollectionType.Highlight, - snacks = snacks.subList(0, 13) -) - -private val popular = SnackCollection( - id = Random.nextLong(), - name = "Popular on Jetsnack", - snacks = snacks.subList(14, 19) -) - -private val wfhFavs = tastyTreats.copy( - id = Random.nextLong(), - name = "WFH favourites" -) - -private val newlyAdded = popular.copy( - id = Random.nextLong(), - name = "Newly Added" -) - -private val exclusive = tastyTreats.copy( - id = Random.nextLong(), - name = "Only on Jetsnack" -) - -private val also = tastyTreats.copy( - id = Random.nextLong(), - name = "Customers also bought" -) - -private val inspiredByCart = tastyTreats.copy( - id = Random.nextLong(), - name = "Inspired by your cart" -) - -private val snackCollections = listOf( - tastyTreats, - popular, - wfhFavs, - newlyAdded, - exclusive -) - -private val related = listOf( - also.copy(id = Random.nextLong()), - popular.copy(id = Random.nextLong()) -) - -private val cart = listOf( - OrderLine(snacks[4], 2), - OrderLine(snacks[6], 3), - OrderLine(snacks[8], 1) -) +private val tastyTreats = + SnackCollection( + id = 1L, + name = "Android's picks", + type = CollectionType.Highlight, + snacks = snacks.subList(0, 13), + ) + +private val popular = + SnackCollection( + id = Random.nextLong(), + name = "Popular on Jetsnack", + snacks = snacks.subList(14, 19), + ) + +private val wfhFavs = + tastyTreats.copy( + id = Random.nextLong(), + name = "WFH favourites", + ) + +private val newlyAdded = + popular.copy( + id = Random.nextLong(), + name = "Newly Added", + ) + +private val exclusive = + tastyTreats.copy( + id = Random.nextLong(), + name = "Only on Jetsnack", + ) + +private val also = + tastyTreats.copy( + id = Random.nextLong(), + name = "Customers also bought", + ) + +private val inspiredByCart = + tastyTreats.copy( + id = Random.nextLong(), + name = "Inspired by your cart", + ) + +private val snackCollections = + listOf( + tastyTreats, + popular, + wfhFavs, + newlyAdded, + exclusive, + ) + +private val related = + listOf( + also.copy(id = Random.nextLong()), + popular.copy(id = Random.nextLong()), + ) + +private val cart = + listOf( + OrderLine(snacks[4], 2), + OrderLine(snacks[6], 3), + OrderLine(snacks[8], 1), + ) @Immutable data class OrderLine( val snack: Snack, - val count: Int + val count: Int, ) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackbarManager.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackbarManager.kt index 4098d4057c..d40d4bbbe0 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackbarManager.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/model/SnackbarManager.kt @@ -17,28 +17,33 @@ package com.example.jetsnack.model import androidx.annotation.StringRes -import java.util.UUID import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import java.util.UUID -data class Message(val id: Long, @StringRes val messageId: Int) +data class Message( + val id: Long, + @StringRes val messageId: Int, +) /** * Class responsible for managing Snackbar messages to show on the screen */ object SnackbarManager { - private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) val messages: StateFlow> get() = _messages.asStateFlow() - fun showMessage(@StringRes messageTextId: Int) { + fun showMessage( + @StringRes messageTextId: Int, + ) { _messages.update { currentMessages -> - currentMessages + Message( - id = UUID.randomUUID().mostSignificantBits, - messageId = messageTextId - ) + currentMessages + + Message( + id = UUID.randomUUID().mostSignificantBits, + messageId = messageTextId, + ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt index 04991fa1cf..554c4b99b8 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt @@ -15,7 +15,7 @@ */ @file:OptIn( - ExperimentalSharedTransitionApi::class + ExperimentalSharedTransitionApi::class, ) package com.example.jetsnack.ui @@ -64,17 +64,17 @@ fun JetsnackApp() { val jetsnackNavController = rememberJetsnackNavController() SharedTransitionLayout { CompositionLocalProvider( - LocalSharedTransitionScope provides this + LocalSharedTransitionScope provides this, ) { NavHost( navController = jetsnackNavController.navController, - startDestination = MainDestinations.HOME_ROUTE + startDestination = MainDestinations.HOME_ROUTE, ) { composableWithCompositionLocal( - route = MainDestinations.HOME_ROUTE + route = MainDestinations.HOME_ROUTE, ) { backStackEntry -> MainContainer( - onSnackSelected = jetsnackNavController::navigateToSnackDetail + onSnackSelected = jetsnackNavController::navigateToSnackDetail, ) } @@ -82,12 +82,12 @@ fun JetsnackApp() { "${MainDestinations.SNACK_DETAIL_ROUTE}/" + "{${MainDestinations.SNACK_ID_KEY}}" + "?origin={${MainDestinations.ORIGIN}}", - arguments = listOf( - navArgument(MainDestinations.SNACK_ID_KEY) { - type = NavType.LongType - } - ), - + arguments = + listOf( + navArgument(MainDestinations.SNACK_ID_KEY) { + type = NavType.LongType + }, + ), ) { backStackEntry -> val arguments = requireNotNull(backStackEntry.arguments) val snackId = arguments.getLong(MainDestinations.SNACK_ID_KEY) @@ -95,7 +95,7 @@ fun JetsnackApp() { SnackDetail( snackId, origin = origin ?: "", - upPress = jetsnackNavController::upPress + upPress = jetsnackNavController::upPress, ) } } @@ -107,16 +107,18 @@ fun JetsnackApp() { @Composable fun MainContainer( modifier: Modifier = Modifier, - onSnackSelected: (Long, String, NavBackStackEntry) -> Unit + onSnackSelected: (Long, String, NavBackStackEntry) -> Unit, ) { val jetsnackScaffoldState = rememberJetsnackScaffoldState() val nestedNavController = rememberJetsnackNavController() val navBackStackEntry by nestedNavController.navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalStateException("No SharedElementScope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalStateException("No SharedElementScope found") + val sharedTransitionScope = + LocalSharedTransitionScope.current + ?: throw IllegalStateException("No SharedElementScope found") + val animatedVisibilityScope = + LocalNavAnimatedVisibilityScope.current + ?: throw IllegalStateException("No SharedElementScope found") JetsnackScaffold( bottomBar = { with(animatedVisibilityScope) { @@ -125,22 +127,26 @@ fun MainContainer( tabs = HomeSections.entries.toTypedArray(), currentRoute = currentRoute ?: HomeSections.FEED.route, navigateToRoute = nestedNavController::navigateToBottomBarRoute, - modifier = Modifier - .renderInSharedTransitionScopeOverlay( - zIndexInOverlay = 1f, - ) - .animateEnterExit( - enter = fadeIn(nonSpatialExpressiveSpring()) + slideInVertically( - spatialExpressiveSpring() - ) { - it - }, - exit = fadeOut(nonSpatialExpressiveSpring()) + slideOutVertically( - spatialExpressiveSpring() - ) { - it - } - ) + modifier = + Modifier + .renderInSharedTransitionScopeOverlay( + zIndexInOverlay = 1f, + ).animateEnterExit( + enter = + fadeIn(nonSpatialExpressiveSpring()) + + slideInVertically( + spatialExpressiveSpring(), + ) { + it + }, + exit = + fadeOut(nonSpatialExpressiveSpring()) + + slideOutVertically( + spatialExpressiveSpring(), + ) { + it + }, + ), ) } } @@ -150,20 +156,21 @@ fun MainContainer( SnackbarHost( hostState = it, modifier = Modifier.systemBarsPadding(), - snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) } + snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) }, ) }, snackBarHostState = jetsnackScaffoldState.snackBarHostState, ) { padding -> NavHost( navController = nestedNavController.navController, - startDestination = HomeSections.FEED.route + startDestination = HomeSections.FEED.route, ) { addHomeGraph( onSnackSelected = onSnackSelected, - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + modifier = + Modifier + .padding(padding) + .consumeWindowInsets(padding), ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/SnackSharedElementKey.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/SnackSharedElementKey.kt index a40b6091d0..a1717c2e0c 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/SnackSharedElementKey.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/SnackSharedElementKey.kt @@ -19,7 +19,7 @@ package com.example.jetsnack.ui data class SnackSharedElementKey( val snackId: Long, val origin: String, - val type: SnackSharedElementType + val type: SnackSharedElementType, ) enum class SnackSharedElementType { @@ -27,7 +27,7 @@ enum class SnackSharedElementType { Image, Title, Tagline, - Background + Background, } object FilterSharedElementKey diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt index 11e915cfa6..7d7c6fdd3f 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt @@ -48,7 +48,6 @@ import androidx.compose.ui.tooling.preview.Preview import com.example.jetsnack.ui.theme.JetsnackTheme @Composable - fun JetsnackButton( onClick: () -> Unit, modifier: Modifier = Modifier, @@ -61,42 +60,41 @@ fun JetsnackButton( contentColor: Color = JetsnackTheme.colors.textInteractive, disabledContentColor: Color = JetsnackTheme.colors.textHelp, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, - content: @Composable RowScope.() -> Unit + content: @Composable RowScope.() -> Unit, ) { JetsnackSurface( shape = shape, color = Color.Transparent, contentColor = if (enabled) contentColor else disabledContentColor, border = border, - modifier = modifier - .clip(shape) - .background( - Brush.horizontalGradient( - colors = if (enabled) backgroundGradient else disabledBackgroundGradient - ) - ) - .clickable( - onClick = onClick, - enabled = enabled, - role = Role.Button, - interactionSource = interactionSource, - indication = null - ) + modifier = + modifier + .clip(shape) + .background( + Brush.horizontalGradient( + colors = if (enabled) backgroundGradient else disabledBackgroundGradient, + ), + ).clickable( + onClick = onClick, + enabled = enabled, + role = Role.Button, + interactionSource = interactionSource, + indication = null, + ), ) { ProvideTextStyle( - value = MaterialTheme.typography.labelLarge + value = MaterialTheme.typography.labelLarge, ) { Row( Modifier .defaultMinSize( minWidth = ButtonDefaults.MinWidth, - minHeight = ButtonDefaults.MinHeight - ) - .indication(interactionSource, rememberRipple()) + minHeight = ButtonDefaults.MinHeight, + ).indication(interactionSource, rememberRipple()) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, - content = content + content = content, ) } } @@ -123,7 +121,8 @@ private fun ButtonPreview() { private fun RectangleButtonPreview() { JetsnackTheme { JetsnackButton( - onClick = {}, shape = RectangleShape + onClick = {}, + shape = RectangleShape, ) { Text(text = "Demo") } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Card.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Card.kt index f89478e979..8507472164 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Card.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Card.kt @@ -38,7 +38,7 @@ fun JetsnackCard( contentColor: Color = JetsnackTheme.colors.textPrimary, border: BorderStroke? = null, elevation: Dp = 4.dp, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { JetsnackSurface( modifier = modifier, @@ -47,7 +47,7 @@ fun JetsnackCard( contentColor = contentColor, elevation = elevation, border = border, - content = content + content = content, ) } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Divider.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Divider.kt index 49a397bb7d..089313675a 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Divider.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Divider.kt @@ -33,12 +33,12 @@ import com.example.jetsnack.ui.theme.JetsnackTheme fun JetsnackDivider( modifier: Modifier = Modifier, color: Color = JetsnackTheme.colors.uiBorder.copy(alpha = DividerAlpha), - thickness: Dp = 1.dp + thickness: Dp = 1.dp, ) { HorizontalDivider( modifier = modifier, color = color, - thickness = thickness + thickness = thickness, ) } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt index 5cc0fb263e..dc8158c7b8 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt @@ -61,34 +61,36 @@ fun FilterBar( filters: List, onShowFilters: () -> Unit, filterScreenVisible: Boolean, - sharedTransitionScope: SharedTransitionScope + sharedTransitionScope: SharedTransitionScope, ) { with(sharedTransitionScope) { LazyRow( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(start = 12.dp, end = 8.dp), - modifier = Modifier.heightIn(min = 56.dp) + modifier = Modifier.heightIn(min = 56.dp), ) { item { AnimatedVisibility(visible = !filterScreenVisible) { IconButton( onClick = onShowFilters, - modifier = Modifier - .sharedBounds( - rememberSharedContentState(FilterSharedElementKey), - animatedVisibilityScope = this@AnimatedVisibility, - resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds - ) + modifier = + Modifier + .sharedBounds( + rememberSharedContentState(FilterSharedElementKey), + animatedVisibilityScope = this@AnimatedVisibility, + resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds, + ), ) { Icon( imageVector = Icons.Rounded.FilterList, tint = JetsnackTheme.colors.brand, contentDescription = stringResource(R.string.label_filters), - modifier = Modifier.diagonalGradientBorder( - colors = JetsnackTheme.colors.interactiveSecondary, - shape = CircleShape - ) + modifier = + Modifier.diagonalGradientBorder( + colors = JetsnackTheme.colors.interactiveSecondary, + shape = CircleShape, + ), ) } } @@ -104,21 +106,22 @@ fun FilterBar( fun FilterChip( filter: Filter, modifier: Modifier = Modifier, - shape: Shape = MaterialTheme.shapes.small + shape: Shape = MaterialTheme.shapes.small, ) { val (selected, setSelected) = filter.enabled val backgroundColor by animateColorAsState( if (selected) JetsnackTheme.colors.brandSecondary else JetsnackTheme.colors.uiBackground, - label = "background color" - ) - val border = Modifier.fadeInDiagonalGradientBorder( - showBorder = !selected, - colors = JetsnackTheme.colors.interactiveSecondary, - shape = shape + label = "background color", ) + val border = + Modifier.fadeInDiagonalGradientBorder( + showBorder = !selected, + colors = JetsnackTheme.colors.interactiveSecondary, + shape = shape, + ) val textColor by animateColorAsState( if (selected) Color.Black else JetsnackTheme.colors.textSecondary, - label = "text color" + label = "text color", ) JetsnackSurface( @@ -126,7 +129,7 @@ fun FilterChip( color = backgroundColor, contentColor = textColor, shape = shape, - elevation = 2.dp + elevation = 2.dp, ) { val interactionSource = remember { MutableInteractionSource() } @@ -136,30 +139,31 @@ fun FilterChip( Modifier.offsetGradientBackground( JetsnackTheme.colors.interactiveSecondary, 200f, - 0f + 0f, ) } else { Modifier.background(Color.Transparent) } Box( - modifier = Modifier - .toggleable( - value = selected, - onValueChange = setSelected, - interactionSource = interactionSource, - indication = null - ) - .then(backgroundPressed) - .then(border), + modifier = + Modifier + .toggleable( + value = selected, + onValueChange = setSelected, + interactionSource = interactionSource, + indication = null, + ).then(backgroundPressed) + .then(border), ) { Text( text = filter.name, style = MaterialTheme.typography.bodySmall, maxLines = 1, - modifier = Modifier.padding( - horizontal = 20.dp, - vertical = 6.dp - ) + modifier = + Modifier.padding( + horizontal = 20.dp, + vertical = 6.dp, + ), ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt index 53a61e052a..9131648c2a 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt @@ -34,32 +34,32 @@ import androidx.compose.ui.unit.dp fun Modifier.diagonalGradientTint( colors: List, - blendMode: BlendMode + blendMode: BlendMode, ) = drawWithContent { drawContent() drawRect( brush = Brush.linearGradient(colors), - blendMode = blendMode + blendMode = blendMode, ) } fun Modifier.offsetGradientBackground( colors: List, width: Float, - offset: Float = 0f + offset: Float = 0f, ) = background( Brush.horizontalGradient( colors = colors, startX = -offset, endX = width - offset, - tileMode = TileMode.Mirror - ) + tileMode = TileMode.Mirror, + ), ) fun Modifier.offsetGradientBackground( colors: List, width: Density.() -> Float, - offset: Density.() -> Float = { 0f } + offset: Density.() -> Float = { 0f }, ) = drawBehind { val actualOffset = offset() @@ -68,36 +68,37 @@ fun Modifier.offsetGradientBackground( colors = colors, startX = -actualOffset, endX = width() - actualOffset, - tileMode = TileMode.Mirror - ) + tileMode = TileMode.Mirror, + ), ) } fun Modifier.diagonalGradientBorder( colors: List, borderSize: Dp = 2.dp, - shape: Shape + shape: Shape, ) = border( width = borderSize, brush = Brush.linearGradient(colors), - shape = shape + shape = shape, ) fun Modifier.fadeInDiagonalGradientBorder( showBorder: Boolean, colors: List, borderSize: Dp = 2.dp, - shape: Shape + shape: Shape, ) = composed { - val animatedColors = List(colors.size) { i -> - animateColorAsState( - if (showBorder) colors[i] else colors[i].copy(alpha = 0f), - label = "animated color" - ).value - } + val animatedColors = + List(colors.size) { i -> + animateColorAsState( + if (showBorder) colors[i] else colors[i].copy(alpha = 0f), + label = "animated color", + ).value + } diagonalGradientBorder( colors = animatedColors, borderSize = borderSize, - shape = shape + shape = shape, ) } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt index 45a32f8381..adaec64856 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt @@ -45,53 +45,57 @@ fun JetsnackGradientTintedIconButton( onClick: () -> Unit, contentDescription: String?, modifier: Modifier = Modifier, - colors: List = JetsnackTheme.colors.interactiveSecondary + colors: List = JetsnackTheme.colors.interactiveSecondary, ) { val interactionSource = remember { MutableInteractionSource() } // This should use a layer + srcIn but needs investigation - val border = Modifier.fadeInDiagonalGradientBorder( - showBorder = true, - colors = JetsnackTheme.colors.interactiveSecondary, - shape = CircleShape - ) + val border = + Modifier.fadeInDiagonalGradientBorder( + showBorder = true, + colors = JetsnackTheme.colors.interactiveSecondary, + shape = CircleShape, + ) val pressed by interactionSource.collectIsPressedAsState() - val background = if (pressed) { - Modifier.offsetGradientBackground(colors, 200f, 0f) - } else { - Modifier.background(JetsnackTheme.colors.uiBackground) - } + val background = + if (pressed) { + Modifier.offsetGradientBackground(colors, 200f, 0f) + } else { + Modifier.background(JetsnackTheme.colors.uiBackground) + } val blendMode = if (JetsnackTheme.colors.isDark) BlendMode.Darken else BlendMode.Plus - val modifierColor = if (pressed) { - Modifier.diagonalGradientTint( - colors = listOf( - JetsnackTheme.colors.textSecondary, - JetsnackTheme.colors.textSecondary - ), - blendMode = blendMode - ) - } else { - Modifier.diagonalGradientTint( - colors = colors, - blendMode = blendMode - ) - } - Surface( - modifier = modifier - .clickable( - onClick = onClick, - interactionSource = interactionSource, - indication = null + val modifierColor = + if (pressed) { + Modifier.diagonalGradientTint( + colors = + listOf( + JetsnackTheme.colors.textSecondary, + JetsnackTheme.colors.textSecondary, + ), + blendMode = blendMode, ) - .clip(CircleShape) - .then(border) - .then(background), - color = Color.Transparent + } else { + Modifier.diagonalGradientTint( + colors = colors, + blendMode = blendMode, + ) + } + Surface( + modifier = + modifier + .clickable( + onClick = onClick, + interactionSource = interactionSource, + indication = null, + ).clip(CircleShape) + .then(border) + .then(background), + color = Color.Transparent, ) { Icon( imageVector = imageVector, contentDescription = contentDescription, - modifier = modifierColor + modifier = modifierColor, ) } } @@ -105,7 +109,7 @@ private fun GradientTintedIconButtonPreview() { imageVector = Icons.Default.Add, onClick = {}, contentDescription = "Demo", - modifier = Modifier.padding(4.dp) + modifier = Modifier.padding(4.dp), ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt index 26d3151cc4..cc9da5dc44 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt @@ -27,18 +27,19 @@ import androidx.compose.ui.layout.Layout fun VerticalGrid( modifier: Modifier = Modifier, columns: Int = 2, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { Layout( content = content, - modifier = modifier + modifier = modifier, ) { measurables, constraints -> val itemWidth = constraints.maxWidth / columns // Keep given height constraints, but set an exact width - val itemConstraints = constraints.copy( - minWidth = itemWidth, - maxWidth = itemWidth - ) + val itemConstraints = + constraints.copy( + minWidth = itemWidth, + maxWidth = itemWidth, + ) // Measure each item with these constraints val placeables = measurables.map { it.measure(itemConstraints) } // Track each columns height so we can calculate the overall height @@ -47,11 +48,12 @@ fun VerticalGrid( val column = index % columns columnHeights[column] += placeable.height } - val height = (columnHeights.maxOrNull() ?: constraints.minHeight) - .coerceAtMost(constraints.maxHeight) + val height = + (columnHeights.maxOrNull() ?: constraints.minHeight) + .coerceAtMost(constraints.maxHeight) layout( width = constraints.maxWidth, - height = height + height = height, ) { // Track the Y co-ord per column we have placed up to val columnY = Array(columns) { 0 } @@ -59,7 +61,7 @@ fun VerticalGrid( val column = index % columns placeable.placeRelative( x = column * itemWidth, - y = columnY[column] + y = columnY[column], ) columnY[column] += placeable.height } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt index b2dde9ac0c..0c36e6e6aa 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt @@ -46,7 +46,7 @@ fun QuantitySelector( count: Int, decreaseItemCount: () -> Unit, increaseItemCount: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row(modifier = modifier) { Text( @@ -54,20 +54,22 @@ fun QuantitySelector( style = MaterialTheme.typography.titleMedium, color = JetsnackTheme.colors.textSecondary, fontWeight = FontWeight.Normal, - modifier = Modifier - .padding(end = 18.dp) - .align(Alignment.CenterVertically) + modifier = + Modifier + .padding(end = 18.dp) + .align(Alignment.CenterVertically), ) JetsnackGradientTintedIconButton( imageVector = Icons.Default.Remove, onClick = decreaseItemCount, contentDescription = stringResource(R.string.label_decrease), - modifier = Modifier.align(Alignment.CenterVertically) + modifier = Modifier.align(Alignment.CenterVertically), ) Crossfade( targetState = count, - modifier = Modifier - .align(Alignment.CenterVertically) + modifier = + Modifier + .align(Alignment.CenterVertically), ) { Text( text = "$it", @@ -75,14 +77,14 @@ fun QuantitySelector( fontSize = 18.sp, color = JetsnackTheme.colors.textPrimary, textAlign = TextAlign.Center, - modifier = Modifier.widthIn(min = 24.dp) + modifier = Modifier.widthIn(min = 24.dp), ) } JetsnackGradientTintedIconButton( imageVector = Icons.Default.Add, onClick = increaseItemCount, contentDescription = stringResource(R.string.label_increase), - modifier = Modifier.align(Alignment.CenterVertically) + modifier = Modifier.align(Alignment.CenterVertically), ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Scaffold.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Scaffold.kt index 1cc197f736..249565b09b 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Scaffold.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Scaffold.kt @@ -50,7 +50,7 @@ fun JetsnackScaffold( floatingActionButtonPosition: FabPosition = FabPosition.End, backgroundColor: Color = JetsnackTheme.colors.uiBackground, contentColor: Color = JetsnackTheme.colors.textSecondary, - content: @Composable (PaddingValues) -> Unit + content: @Composable (PaddingValues) -> Unit, ) { Scaffold( modifier = modifier, @@ -63,7 +63,7 @@ fun JetsnackScaffold( floatingActionButtonPosition = floatingActionButtonPosition, containerColor = backgroundColor, contentColor = contentColor, - content = content + content = content, ) } @@ -75,10 +75,11 @@ fun rememberJetsnackScaffoldState( snackBarHostState: SnackbarHostState = remember { SnackbarHostState() }, snackbarManager: SnackbarManager = SnackbarManager, resources: Resources = resources(), - coroutineScope: CoroutineScope = rememberCoroutineScope() -): JetsnackScaffoldState = remember(snackBarHostState, snackbarManager, resources, coroutineScope) { - JetsnackScaffoldState(snackBarHostState, snackbarManager, resources, coroutineScope) -} + coroutineScope: CoroutineScope = rememberCoroutineScope(), +): JetsnackScaffoldState = + remember(snackBarHostState, snackbarManager, resources, coroutineScope) { + JetsnackScaffoldState(snackBarHostState, snackbarManager, resources, coroutineScope) + } /** * Responsible for holding [ScaffoldState], handles the logic of showing snackbar messages @@ -88,7 +89,7 @@ class JetsnackScaffoldState( val snackBarHostState: SnackbarHostState, private val snackbarManager: SnackbarManager, private val resources: Resources, - coroutineScope: CoroutineScope + coroutineScope: CoroutineScope, ) { // Process snackbars coming from SnackbarManager init { diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snackbar.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snackbar.kt index 996666f1a6..1f1d6a3216 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snackbar.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snackbar.kt @@ -37,7 +37,7 @@ fun JetsnackSnackbar( shape: Shape = MaterialTheme.shapes.small, backgroundColor: Color = JetsnackTheme.colors.uiBackground, contentColor: Color = JetsnackTheme.colors.textSecondary, - actionColor: Color = JetsnackTheme.colors.brand + actionColor: Color = JetsnackTheme.colors.brand, ) { Snackbar( snackbarData = snackbarData, @@ -46,6 +46,6 @@ fun JetsnackSnackbar( shape = shape, containerColor = backgroundColor, contentColor = contentColor, - actionColor = actionColor + actionColor = actionColor, ) } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt index 5db06f3b20..00b076eab2 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt @@ -97,14 +97,15 @@ fun SnackCollection( onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier, index: Int = 0, - highlight: Boolean = true + highlight: Boolean = true, ) { Column(modifier = modifier) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .heightIn(min = 56.dp) - .padding(start = 24.dp) + modifier = + Modifier + .heightIn(min = 56.dp) + .padding(start = 24.dp), ) { Text( text = snackCollection.name, @@ -112,18 +113,19 @@ fun SnackCollection( color = JetsnackTheme.colors.brand, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier - .weight(1f) - .wrapContentWidth(Alignment.Start) + modifier = + Modifier + .weight(1f) + .wrapContentWidth(Alignment.Start), ) IconButton( onClick = { /* todo */ }, - modifier = Modifier.align(Alignment.CenterVertically) + modifier = Modifier.align(Alignment.CenterVertically), ) { Icon( imageVector = Icons.AutoMirrored.Outlined.ArrowBack, tint = JetsnackTheme.colors.brand, - contentDescription = null + contentDescription = null, ) } } @@ -141,7 +143,7 @@ private fun HighlightedSnacks( index: Int, snacks: List, onSnackClick: (Long, String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val rowState = rememberLazyListState() val cardWidthWithPaddingPx = with(LocalDensity.current) { cardWidthWithPaddingPx } @@ -152,16 +154,17 @@ private fun HighlightedSnacks( offsetFromStart + rowState.firstVisibleItemScrollOffset } - val gradient = when ((index / 2) % 2) { - 0 -> JetsnackTheme.colors.gradient6_1 - else -> JetsnackTheme.colors.gradient6_2 - } + val gradient = + when ((index / 2) % 2) { + 0 -> JetsnackTheme.colors.gradient6_1 + else -> JetsnackTheme.colors.gradient6_2 + } LazyRow( state = rowState, modifier = modifier, horizontalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(start = 24.dp, end = 24.dp) + contentPadding = PaddingValues(start = 24.dp, end = 24.dp), ) { itemsIndexed(snacks) { index, snack -> HighlightSnackItem( @@ -170,7 +173,7 @@ private fun HighlightedSnacks( onSnackClick = onSnackClick, index = index, gradient = gradient, - scrollProvider = scrollProvider + scrollProvider = scrollProvider, ) } } @@ -181,11 +184,11 @@ private fun Snacks( snackCollectionId: Long, snacks: List, onSnackClick: (Long, String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { LazyRow( modifier = modifier, - contentPadding = PaddingValues(start = 12.dp, end = 12.dp) + contentPadding = PaddingValues(start = 12.dp, end = 12.dp), ) { items(snacks) { snack -> SnackItem(snack, snackCollectionId, onSnackClick) @@ -198,70 +201,77 @@ fun SnackItem( snack: Snack, snackCollectionId: Long, onSnackClick: (Long, String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { JetsnackSurface( shape = MaterialTheme.shapes.medium, - modifier = modifier.padding( - start = 4.dp, - end = 4.dp, - bottom = 8.dp - ) - + modifier = + modifier.padding( + start = 4.dp, + end = 4.dp, + bottom = 8.dp, + ), ) { - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalStateException("No sharedTransitionScope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalStateException("No animatedVisibilityScope found") + val sharedTransitionScope = + LocalSharedTransitionScope.current + ?: throw IllegalStateException("No sharedTransitionScope found") + val animatedVisibilityScope = + LocalNavAnimatedVisibilityScope.current + ?: throw IllegalStateException("No animatedVisibilityScope found") with(sharedTransitionScope) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .clickable(onClick = { - onSnackClick(snack.id, snackCollectionId.toString()) - }) - .padding(8.dp) + modifier = + Modifier + .clickable(onClick = { + onSnackClick(snack.id, snackCollectionId.toString()) + }) + .padding(8.dp), ) { SnackImage( imageRes = snack.imageRes, elevation = 1.dp, contentDescription = null, - modifier = Modifier - .size(120.dp) - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Image - ) + modifier = + Modifier + .size(120.dp) + .sharedBounds( + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snack.id, + origin = snackCollectionId.toString(), + type = SnackSharedElementType.Image, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = snackDetailBoundsTransform, ), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = snackDetailBoundsTransform - ) ) Text( text = snack.name, style = MaterialTheme.typography.titleMedium, color = JetsnackTheme.colors.textSecondary, - modifier = Modifier - .padding(top = 8.dp) - .wrapContentWidth() - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Title - ) + modifier = + Modifier + .padding(top = 8.dp) + .wrapContentWidth() + .sharedBounds( + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snack.id, + origin = snackCollectionId.toString(), + type = SnackSharedElementType.Title, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(nonSpatialExpressiveSpring()), + exit = fadeOut(nonSpatialExpressiveSpring()), + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds(), + boundsTransform = snackDetailBoundsTransform, ), - animatedVisibilityScope = animatedVisibilityScope, - enter = fadeIn(nonSpatialExpressiveSpring()), - exit = fadeOut(nonSpatialExpressiveSpring()), - resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds(), - boundsTransform = snackDetailBoundsTransform - ) ) } } @@ -276,12 +286,14 @@ private fun HighlightSnackItem( index: Int, gradient: List, scrollProvider: () -> Float, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalStateException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalStateException("No Scope found") + val sharedTransitionScope = + LocalSharedTransitionScope.current + ?: throw IllegalStateException("No Scope found") + val animatedVisibilityScope = + LocalNavAnimatedVisibilityScope.current + ?: throw IllegalStateException("No Scope found") with(sharedTransitionScope) { val roundedCornerAnimation by animatedVisibilityScope.transition .animateDp(label = "rounded corner") { enterExit: EnterExitState -> @@ -294,105 +306,109 @@ private fun HighlightSnackItem( JetsnackCard( elevation = 0.dp, shape = RoundedCornerShape(roundedCornerAnimation), - modifier = modifier - .padding(bottom = 16.dp) - .sharedBounds( - sharedContentState = rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Bounds - ) - ), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = snackDetailBoundsTransform, - clipInOverlayDuringTransition = OverlayClip( - RoundedCornerShape( - roundedCornerAnimation - ) + modifier = + modifier + .padding(bottom = 16.dp) + .sharedBounds( + sharedContentState = + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snack.id, + origin = snackCollectionId.toString(), + type = SnackSharedElementType.Bounds, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = snackDetailBoundsTransform, + clipInOverlayDuringTransition = + OverlayClip( + RoundedCornerShape( + roundedCornerAnimation, + ), + ), + enter = fadeIn(), + exit = fadeOut(), + ).size( + width = HighlightCardWidth, + height = 250.dp, + ).border( + 1.dp, + JetsnackTheme.colors.uiBorder.copy(alpha = 0.12f), + RoundedCornerShape(roundedCornerAnimation), ), - enter = fadeIn(), - exit = fadeOut() - ) - .size( - width = HighlightCardWidth, - height = 250.dp - ) - .border( - 1.dp, - JetsnackTheme.colors.uiBorder.copy(alpha = 0.12f), - RoundedCornerShape(roundedCornerAnimation) - ) - ) { Column( - modifier = Modifier - .clickable(onClick = { - onSnackClick( - snack.id, - snackCollectionId.toString() - ) - }) - .fillMaxSize() - + modifier = + Modifier + .clickable(onClick = { + onSnackClick( + snack.id, + snackCollectionId.toString(), + ) + }) + .fillMaxSize(), ) { Box( - modifier = Modifier - .height(160.dp) - .fillMaxWidth() + modifier = + Modifier + .height(160.dp) + .fillMaxWidth(), ) { Box( - modifier = Modifier - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Background - ) + modifier = + Modifier + .sharedBounds( + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snack.id, + origin = snackCollectionId.toString(), + type = SnackSharedElementType.Background, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = snackDetailBoundsTransform, + enter = fadeIn(nonSpatialExpressiveSpring()), + exit = fadeOut(nonSpatialExpressiveSpring()), + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds(), + ).height(100.dp) + .fillMaxWidth() + .offsetGradientBackground( + colors = gradient, + width = { + // The Cards show a gradient which spans 6 cards and + // scrolls with parallax. + 6 * cardWidthWithPaddingPx + }, + offset = { + val left = index * cardWidthWithPaddingPx + val gradientOffset = left - (scrollProvider() / 3f) + gradientOffset + }, ), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = snackDetailBoundsTransform, - enter = fadeIn(nonSpatialExpressiveSpring()), - exit = fadeOut(nonSpatialExpressiveSpring()), - resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() - ) - .height(100.dp) - .fillMaxWidth() - .offsetGradientBackground( - colors = gradient, - width = { - // The Cards show a gradient which spans 6 cards and - // scrolls with parallax. - 6 * cardWidthWithPaddingPx - }, - offset = { - val left = index * cardWidthWithPaddingPx - val gradientOffset = left - (scrollProvider() / 3f) - gradientOffset - } - ) ) SnackImage( imageRes = snack.imageRes, contentDescription = null, - modifier = Modifier - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Image - ) - ), - animatedVisibilityScope = animatedVisibilityScope, - exit = fadeOut(nonSpatialExpressiveSpring()), - enter = fadeIn(nonSpatialExpressiveSpring()), - boundsTransform = snackDetailBoundsTransform - ) - .align(Alignment.BottomCenter) - .size(120.dp) + modifier = + Modifier + .sharedBounds( + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snack.id, + origin = snackCollectionId.toString(), + type = SnackSharedElementType.Image, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + exit = fadeOut(nonSpatialExpressiveSpring()), + enter = fadeIn(nonSpatialExpressiveSpring()), + boundsTransform = snackDetailBoundsTransform, + ).align(Alignment.BottomCenter) + .size(120.dp), ) } @@ -403,23 +419,24 @@ private fun HighlightSnackItem( overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleLarge, color = JetsnackTheme.colors.textSecondary, - modifier = Modifier - .padding(horizontal = 16.dp) - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Title - ) - ), - animatedVisibilityScope = animatedVisibilityScope, - enter = fadeIn(nonSpatialExpressiveSpring()), - exit = fadeOut(nonSpatialExpressiveSpring()), - boundsTransform = snackDetailBoundsTransform, - resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() - ) - .wrapContentWidth() + modifier = + Modifier + .padding(horizontal = 16.dp) + .sharedBounds( + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snack.id, + origin = snackCollectionId.toString(), + type = SnackSharedElementType.Title, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(nonSpatialExpressiveSpring()), + exit = fadeOut(nonSpatialExpressiveSpring()), + boundsTransform = snackDetailBoundsTransform, + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds(), + ).wrapContentWidth(), ) Spacer(modifier = Modifier.height(4.dp)) @@ -427,23 +444,24 @@ private fun HighlightSnackItem( text = snack.tagline, style = MaterialTheme.typography.bodyLarge, color = JetsnackTheme.colors.textHelp, - modifier = Modifier - .padding(horizontal = 16.dp) - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Tagline - ) - ), - animatedVisibilityScope = animatedVisibilityScope, - enter = fadeIn(nonSpatialExpressiveSpring()), - exit = fadeOut(nonSpatialExpressiveSpring()), - boundsTransform = snackDetailBoundsTransform, - resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() - ) - .wrapContentWidth() + modifier = + Modifier + .padding(horizontal = 16.dp) + .sharedBounds( + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snack.id, + origin = snackCollectionId.toString(), + type = SnackSharedElementType.Tagline, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(nonSpatialExpressiveSpring()), + exit = fadeOut(nonSpatialExpressiveSpring()), + boundsTransform = snackDetailBoundsTransform, + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds(), + ).wrapContentWidth(), ) } } @@ -451,12 +469,13 @@ private fun HighlightSnackItem( } @Composable -fun debugPlaceholder(@DrawableRes debugPreview: Int) = - if (LocalInspectionMode.current) { - painterResource(id = debugPreview) - } else { - null - } +fun debugPlaceholder( + @DrawableRes debugPreview: Int, +) = if (LocalInspectionMode.current) { + painterResource(id = debugPreview) +} else { + null +} @Composable fun SnackImage( @@ -464,19 +483,20 @@ fun SnackImage( imageRes: Int, contentDescription: String?, modifier: Modifier = Modifier, - elevation: Dp = 0.dp + elevation: Dp = 0.dp, ) { JetsnackSurface( elevation = elevation, shape = CircleShape, - modifier = modifier + modifier = modifier, ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageRes) - .crossfade(true) - .build(), + model = + ImageRequest + .Builder(LocalContext.current) + .data(imageRes) + .crossfade(true) + .build(), placeholder = debugPlaceholder(debugPreview = R.drawable.placeholder), contentDescription = contentDescription, modifier = Modifier.fillMaxSize(), @@ -498,7 +518,7 @@ fun SnackCardPreview() { onSnackClick = { _, _ -> }, index = 0, gradient = JetsnackTheme.colors.gradient6_1, - scrollProvider = { 0f } + scrollProvider = { 0f }, ) } } @@ -510,7 +530,7 @@ fun JetsnackPreviewWrapper(content: @Composable () -> Unit) { AnimatedVisibility(visible = true) { CompositionLocalProvider( LocalSharedTransitionScope provides this@SharedTransitionLayout, - LocalNavAnimatedVisibilityScope provides this + LocalNavAnimatedVisibilityScope provides this, ) { content() } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Surface.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Surface.kt index f109fe77a5..a0affb9886 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Surface.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Surface.kt @@ -48,26 +48,29 @@ fun JetsnackSurface( contentColor: Color = JetsnackTheme.colors.textSecondary, border: BorderStroke? = null, elevation: Dp = 0.dp, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { Box( - modifier = modifier - .shadow(elevation = elevation, shape = shape, clip = false) - .zIndex(elevation.value) - .then(if (border != null) Modifier.border(border, shape) else Modifier) - .background( - color = getBackgroundColorForElevation(color, elevation), - shape = shape - ) - .clip(shape) + modifier = + modifier + .shadow(elevation = elevation, shape = shape, clip = false) + .zIndex(elevation.value) + .then(if (border != null) Modifier.border(border, shape) else Modifier) + .background( + color = getBackgroundColorForElevation(color, elevation), + shape = shape, + ).clip(shape), ) { CompositionLocalProvider(LocalContentColor provides contentColor, content = content) } } @Composable -private fun getBackgroundColorForElevation(color: Color, elevation: Dp): Color { - return if (elevation > 0.dp // && https://issuetracker.google.com/issues/161429530 +private fun getBackgroundColorForElevation( + color: Color, + elevation: Dp, +): Color = + if (elevation > 0.dp // && https://issuetracker.google.com/issues/161429530 // JetsnackTheme.colors.isDark //&& // color == JetsnackTheme.colors.uiBackground ) { @@ -75,7 +78,6 @@ private fun getBackgroundColorForElevation(color: Color, elevation: Dp): Color { } else { color } -} /** * Applies a [Color.White] overlay to this color based on the [elevation]. This increases visibility diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt index 691998ceba..c1ed0eaf9f 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt @@ -60,12 +60,13 @@ fun DestinationBar(modifier: Modifier = Modifier) { with(sharedElementScope) { with(navAnimatedScope) { Column( - modifier = modifier - .renderInSharedTransitionScopeOverlay() - .animateEnterExit( - enter = slideInVertically(spatialExpressiveSpring()) { -it * 2 }, - exit = slideOutVertically(spatialExpressiveSpring()) { -it * 2 } - ) + modifier = + modifier + .renderInSharedTransitionScopeOverlay() + .animateEnterExit( + enter = slideInVertically(spatialExpressiveSpring()) { -it * 2 }, + exit = slideOutVertically(spatialExpressiveSpring()) { -it * 2 }, + ), ) { TopAppBar( windowInsets = WindowInsets(0, 0, 0, 0), @@ -78,28 +79,31 @@ fun DestinationBar(modifier: Modifier = Modifier) { textAlign = TextAlign.Center, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) + modifier = + Modifier + .weight(1f) + .align(Alignment.CenterVertically), ) IconButton( onClick = { /* todo */ }, - modifier = Modifier.align(Alignment.CenterVertically) + modifier = Modifier.align(Alignment.CenterVertically), ) { Icon( imageVector = Icons.Outlined.ExpandMore, tint = JetsnackTheme.colors.brand, contentDescription = - stringResource(R.string.label_select_delivery) + stringResource(R.string.label_select_delivery), ) } } }, - colors = TopAppBarDefaults.topAppBarColors().copy( - containerColor = JetsnackTheme.colors.uiBackground - .copy(alpha = AlphaNearOpaque), - titleContentColor = JetsnackTheme.colors.textSecondary - ), + colors = + TopAppBarDefaults.topAppBarColors().copy( + containerColor = + JetsnackTheme.colors.uiBackground + .copy(alpha = AlphaNearOpaque), + titleContentColor = JetsnackTheme.colors.textSecondary, + ), ) JetsnackDivider() } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt index d18c06e1f2..788217e156 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt @@ -54,7 +54,7 @@ import com.example.jetsnack.ui.theme.JetsnackTheme @Composable fun Feed( onSnackClick: (Long, String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val snackCollections = remember { SnackRepo.getSnacks() } val filters = remember { SnackRepo.getFilters() } @@ -62,7 +62,7 @@ fun Feed( snackCollections, filters, onSnackClick, - modifier + modifier, ) } @@ -71,7 +71,7 @@ private fun Feed( snackCollections: List, filters: List, onSnackClick: (Long, String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { JetsnackSurface(modifier = modifier.fillMaxSize()) { var filtersVisible by remember { @@ -87,13 +87,13 @@ private fun Feed( filtersVisible = true }, sharedTransitionScope = this@SharedTransitionLayout, - onSnackClick = onSnackClick + onSnackClick = onSnackClick, ) DestinationBar() AnimatedVisibility(filtersVisible, enter = fadeIn(), exit = fadeOut()) { FilterScreen( animatedVisibilityScope = this@AnimatedVisibility, - sharedTransitionScope = this@SharedTransitionLayout + sharedTransitionScope = this@SharedTransitionLayout, ) { filtersVisible = false } } } @@ -109,20 +109,20 @@ private fun SnackCollectionList( onFiltersSelected: () -> Unit, onSnackClick: (Long, String) -> Unit, sharedTransitionScope: SharedTransitionScope, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { LazyColumn(modifier = modifier) { item { Spacer( Modifier.windowInsetsTopHeight( - WindowInsets.statusBars.add(WindowInsets(top = 56.dp)) - ) + WindowInsets.statusBars.add(WindowInsets(top = 56.dp)), + ), ) FilterBar( filters, sharedTransitionScope = sharedTransitionScope, filterScreenVisible = filtersVisible, - onShowFilters = onFiltersSelected + onShowFilters = onFiltersSelected, ) } itemsIndexed(snackCollections) { index, snackCollection -> @@ -133,7 +133,7 @@ private fun SnackCollectionList( SnackCollection( snackCollection = snackCollection, onSnackClick = onSnackClick, - index = index + index = index, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/FilterScreen.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/FilterScreen.kt index e64fd2c955..1029574bbb 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/FilterScreen.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/FilterScreen.kt @@ -79,35 +79,37 @@ import com.example.jetsnack.ui.theme.JetsnackTheme fun FilterScreen( sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, - onDismiss: () -> Unit + onDismiss: () -> Unit, ) { var sortState by remember { mutableStateOf(SnackRepo.getSortDefault()) } var maxCalories by remember { mutableFloatStateOf(0f) } val defaultFilter = SnackRepo.getSortDefault() Box( - modifier = Modifier - .fillMaxSize() - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - // capture click - } + modifier = + Modifier + .fillMaxSize() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + // capture click + }, ) { val priceFilters = remember { SnackRepo.getPriceFilters() } val categoryFilters = remember { SnackRepo.getCategoryFilters() } val lifeStyleFilters = remember { SnackRepo.getLifeStyleFilters() } Spacer( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.5f)) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - onDismiss() - } + modifier = + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + onDismiss() + }, ) with(sharedTransitionScope) { Column( @@ -119,14 +121,13 @@ fun FilterScreen( rememberSharedContentState(FilterSharedElementKey), animatedVisibilityScope = animatedVisibilityScope, resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds, - clipInOverlayDuringTransition = OverlayClip(MaterialTheme.shapes.medium) - ) - .wrapContentSize() + clipInOverlayDuringTransition = OverlayClip(MaterialTheme.shapes.medium), + ).wrapContentSize() .heightIn(max = 450.dp) .verticalScroll(rememberScrollState()) .clickable( indication = null, - interactionSource = remember { MutableInteractionSource() } + interactionSource = remember { MutableInteractionSource() }, ) { } .background(JetsnackTheme.colors.uiFloated) .padding(horizontal = 24.dp, vertical = 16.dp) @@ -136,36 +137,39 @@ fun FilterScreen( IconButton(onClick = onDismiss) { Icon( imageVector = Icons.Filled.Close, - contentDescription = stringResource(id = R.string.close) + contentDescription = stringResource(id = R.string.close), ) } Text( text = stringResource(id = R.string.label_filters), - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(top = 8.dp, end = 48.dp), + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(top = 8.dp, end = 48.dp), textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleLarge, ) val resetEnabled = sortState != defaultFilter IconButton( onClick = { /* TODO: Open search */ }, - enabled = resetEnabled + enabled = resetEnabled, ) { - val fontWeight = if (resetEnabled) { - FontWeight.Bold - } else { - FontWeight.Normal - } + val fontWeight = + if (resetEnabled) { + FontWeight.Bold + } else { + FontWeight.Normal + } Text( text = stringResource(id = R.string.reset), style = MaterialTheme.typography.bodyMedium, fontWeight = fontWeight, - color = JetsnackTheme.colors.uiBackground - .copy(alpha = if (!resetEnabled) 0.38f else 1f) + color = + JetsnackTheme.colors.uiBackground + .copy(alpha = if (!resetEnabled) 0.38f else 1f), ) } } @@ -174,26 +178,26 @@ fun FilterScreen( sortState = sortState, onFilterChange = { filter -> sortState = filter.name - } + }, ) FilterChipSection( title = stringResource(id = R.string.price), - filters = priceFilters + filters = priceFilters, ) FilterChipSection( title = stringResource(id = R.string.category), - filters = categoryFilters + filters = categoryFilters, ) MaxCalories( sliderPosition = maxCalories, onValueChanged = { newValue -> maxCalories = newValue - } + }, ) FilterChipSection( title = stringResource(id = R.string.lifestyle), - filters = lifeStyleFilters + filters = lifeStyleFilters, ) } } @@ -202,30 +206,37 @@ fun FilterScreen( @OptIn(ExperimentalLayoutApi::class) @Composable -fun FilterChipSection(title: String, filters: List) { +fun FilterChipSection( + title: String, + filters: List, +) { FilterTitle(text = title) FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp, bottom = 16.dp) - .padding(horizontal = 4.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 16.dp) + .padding(horizontal = 4.dp), ) { filters.forEach { filter -> FilterChip( filter = filter, - modifier = Modifier.padding(end = 4.dp, bottom = 8.dp) + modifier = Modifier.padding(end = 4.dp, bottom = 8.dp), ) } } } @Composable -fun SortFiltersSection(sortState: String, onFilterChange: (Filter) -> Unit) { +fun SortFiltersSection( + sortState: String, + onFilterChange: (Filter) -> Unit, +) { FilterTitle(text = stringResource(id = R.string.sort)) Column(Modifier.padding(bottom = 24.dp)) { SortFilters( sortState = sortState, - onChanged = onFilterChange + onChanged = onFilterChange, ) } } @@ -234,9 +245,8 @@ fun SortFiltersSection(sortState: String, onFilterChange: (Filter) -> Unit) { fun SortFilters( sortFilters: List = SnackRepo.getSortFilters(), sortState: String, - onChanged: (Filter) -> Unit + onChanged: (Filter) -> Unit, ) { - sortFilters.forEach { filter -> SortOption( text = filter.name, @@ -244,20 +254,23 @@ fun SortFilters( selected = sortState == filter.name, onClickOption = { onChanged(filter) - } + }, ) } } @Composable -fun MaxCalories(sliderPosition: Float, onValueChanged: (Float) -> Unit) { +fun MaxCalories( + sliderPosition: Float, + onValueChanged: (Float) -> Unit, +) { FlowRow { FilterTitle(text = stringResource(id = R.string.max_calories)) Text( text = stringResource(id = R.string.per_serving), style = MaterialTheme.typography.bodyMedium, color = JetsnackTheme.colors.brand, - modifier = Modifier.padding(top = 5.dp, start = 10.dp) + modifier = Modifier.padding(top = 5.dp, start = 10.dp), ) } Slider( @@ -267,13 +280,15 @@ fun MaxCalories(sliderPosition: Float, onValueChanged: (Float) -> Unit) { }, valueRange = 0f..300f, steps = 5, - modifier = Modifier - .fillMaxWidth(), - colors = SliderDefaults.colors( - thumbColor = JetsnackTheme.colors.brand, - activeTrackColor = JetsnackTheme.colors.brand, - inactiveTrackColor = JetsnackTheme.colors.iconInteractive - ) + modifier = + Modifier + .fillMaxWidth(), + colors = + SliderDefaults.colors( + thumbColor = JetsnackTheme.colors.brand, + activeTrackColor = JetsnackTheme.colors.brand, + inactiveTrackColor = JetsnackTheme.colors.iconInteractive, + ), ) } @@ -283,7 +298,7 @@ fun FilterTitle(text: String) { text = text, style = MaterialTheme.typography.titleLarge, color = JetsnackTheme.colors.brand, - modifier = Modifier.padding(bottom = 8.dp) + modifier = Modifier.padding(bottom = 8.dp), ) } @@ -292,12 +307,13 @@ fun SortOption( text: String, icon: ImageVector?, onClickOption: () -> Unit, - selected: Boolean + selected: Boolean, ) { Row( - modifier = Modifier - .padding(top = 14.dp) - .selectable(selected) { onClickOption() } + modifier = + Modifier + .padding(top = 14.dp) + .selectable(selected) { onClickOption() }, ) { if (icon != null) { Icon(imageVector = icon, contentDescription = null) @@ -305,15 +321,16 @@ fun SortOption( Text( text = text, style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .padding(start = 10.dp) - .weight(1f) + modifier = + Modifier + .padding(start = 10.dp) + .weight(1f), ) if (selected) { Icon( imageVector = Icons.Filled.Done, contentDescription = null, - tint = JetsnackTheme.colors.brand + tint = JetsnackTheme.colors.brand, ) } } @@ -328,7 +345,7 @@ fun FilterScreenPreview() { FilterScreen( animatedVisibilityScope = this, sharedTransitionScope = this@SharedTransitionLayout, - onDismiss = {} + onDismiss = {}, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt index 520d962ad9..5bcc73d717 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt @@ -91,28 +91,24 @@ fun NavGraphBuilder.composableWithCompositionLocal( arguments: List = emptyList(), deepLinks: List = emptyList(), enterTransition: ( - @JvmSuppressWildcards - AnimatedContentTransitionScope.() -> EnterTransition? + @JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition? )? = { fadeIn(nonSpatialExpressiveSpring()) }, exitTransition: ( - @JvmSuppressWildcards - AnimatedContentTransitionScope.() -> ExitTransition? + @JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition? )? = { fadeOut(nonSpatialExpressiveSpring()) }, popEnterTransition: ( - @JvmSuppressWildcards - AnimatedContentTransitionScope.() -> EnterTransition? + @JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition? )? = enterTransition, popExitTransition: ( - @JvmSuppressWildcards - AnimatedContentTransitionScope.() -> ExitTransition? + @JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition? )? = exitTransition, - content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit + content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) { composable( route, @@ -121,10 +117,10 @@ fun NavGraphBuilder.composableWithCompositionLocal( enterTransition, exitTransition, popEnterTransition, - popExitTransition + popExitTransition, ) { CompositionLocalProvider( - LocalNavAnimatedVisibilityScope provides this@composable + LocalNavAnimatedVisibilityScope provides this@composable, ) { content(it) } @@ -133,24 +129,24 @@ fun NavGraphBuilder.composableWithCompositionLocal( fun NavGraphBuilder.addHomeGraph( onSnackSelected: (Long, String, NavBackStackEntry) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { composable(HomeSections.FEED.route) { from -> Feed( onSnackClick = { id, origin -> onSnackSelected(id, origin, from) }, - modifier + modifier, ) } composable(HomeSections.SEARCH.route) { from -> Search( onSnackClick = { id, origin -> onSnackSelected(id, origin, from) }, - modifier + modifier, ) } composable(HomeSections.CART.route) { from -> Cart( onSnackClick = { id, origin -> onSnackSelected(id, origin, from) }, - modifier + modifier, ) } composable(HomeSections.PROFILE.route) { @@ -161,12 +157,12 @@ fun NavGraphBuilder.addHomeGraph( enum class HomeSections( @StringRes val title: Int, val icon: ImageVector, - val route: String + val route: String, ) { FEED(R.string.home_feed, Icons.Outlined.Home, "home/feed"), SEARCH(R.string.home_search, Icons.Outlined.Search, "home/search"), CART(R.string.home_cart, Icons.Outlined.ShoppingCart, "home/cart"), - PROFILE(R.string.home_profile, Icons.Outlined.AccountCircle, "home/profile") + PROFILE(R.string.home_profile, Icons.Outlined.AccountCircle, "home/profile"), } @Composable @@ -176,7 +172,7 @@ fun JetsnackBottomBar( navigateToRoute: (String) -> Unit, modifier: Modifier = Modifier, color: Color = JetsnackTheme.colors.iconPrimary, - contentColor: Color = JetsnackTheme.colors.iconInteractive + contentColor: Color = JetsnackTheme.colors.iconInteractive, ) { val routes = remember { tabs.map { it.route } } val currentSection = tabs.first { it.route == currentRoute } @@ -184,7 +180,7 @@ fun JetsnackBottomBar( JetsnackSurface( modifier = modifier, color = color, - contentColor = contentColor + contentColor = contentColor, ) { val springSpec = spatialExpressiveSpring() JetsnackBottomNavLayout( @@ -192,7 +188,7 @@ fun JetsnackBottomBar( itemCount = routes.size, indicator = { JetsnackBottomNavIndicator() }, animSpec = springSpec, - modifier = Modifier.navigationBarsPadding() + modifier = Modifier.navigationBarsPadding(), ) { val configuration = LocalConfiguration.current val currentLocale: Locale = @@ -206,7 +202,7 @@ fun JetsnackBottomBar( } else { JetsnackTheme.colors.iconInteractiveInactive }, - label = "tint" + label = "tint", ) val text = stringResource(section.title).uppercase(currentLocale) @@ -216,7 +212,7 @@ fun JetsnackBottomBar( Icon( imageVector = section.icon, tint = tint, - contentDescription = text + contentDescription = text, ) }, text = { @@ -224,14 +220,15 @@ fun JetsnackBottomBar( text = text, color = tint, style = MaterialTheme.typography.labelLarge, - maxLines = 1 + maxLines = 1, ) }, selected = selected, onSelected = { navigateToRoute(section.route) }, animSpec = springSpec, - modifier = BottomNavigationItemPadding - .clip(BottomNavIndicatorShape) + modifier = + BottomNavigationItemPadding + .clip(BottomNavIndicatorShape), ) } } @@ -245,14 +242,15 @@ private fun JetsnackBottomNavLayout( animSpec: AnimationSpec, indicator: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { // Track how "selected" each item is [0, 1] - val selectionFractions = remember(itemCount) { - List(itemCount) { i -> - Animatable(if (i == selectedIndex) 1f else 0f) + val selectionFractions = + remember(itemCount) { + List(itemCount) { i -> + Animatable(if (i == selectedIndex) 1f else 0f) + } } - } selectionFractions.forEachIndexed { index, selectionFraction -> val target = if (index == selectedIndex) 1f else 0f LaunchedEffect(target, animSpec) { @@ -272,7 +270,7 @@ private fun JetsnackBottomNavLayout( content = { content() Box(Modifier.layoutId("indicator"), content = indicator) - } + }, ) { measurables, constraints -> check(itemCount == (measurables.size - 1)) // account for indicator @@ -281,28 +279,30 @@ private fun JetsnackBottomNavLayout( val selectedWidth = 2 * unselectedWidth val indicatorMeasurable = measurables.first { it.layoutId == "indicator" } - val itemPlaceables = measurables - .filterNot { it == indicatorMeasurable } - .mapIndexed { index, measurable -> - // Animate item's width based upon the selection amount - val width = lerp(unselectedWidth, selectedWidth, selectionFractions[index].value) - measurable.measure( - constraints.copy( - minWidth = width, - maxWidth = width + val itemPlaceables = + measurables + .filterNot { it == indicatorMeasurable } + .mapIndexed { index, measurable -> + // Animate item's width based upon the selection amount + val width = lerp(unselectedWidth, selectedWidth, selectionFractions[index].value) + measurable.measure( + constraints.copy( + minWidth = width, + maxWidth = width, + ), ) - ) - } - val indicatorPlaceable = indicatorMeasurable.measure( - constraints.copy( - minWidth = selectedWidth, - maxWidth = selectedWidth + } + val indicatorPlaceable = + indicatorMeasurable.measure( + constraints.copy( + minWidth = selectedWidth, + maxWidth = selectedWidth, + ), ) - ) layout( width = constraints.maxWidth, - height = itemPlaceables.maxByOrNull { it.height }?.height ?: 0 + height = itemPlaceables.maxByOrNull { it.height }?.height ?: 0, ) { val indicatorLeft = indicatorIndex.value * unselectedWidth indicatorPlaceable.placeRelative(x = indicatorLeft.toInt(), y = 0) @@ -322,18 +322,22 @@ fun JetsnackBottomNavigationItem( selected: Boolean, onSelected: () -> Unit, animSpec: AnimationSpec, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { // Animate the icon/text positions within the item based on selection - val animationProgress by animateFloatAsState(if (selected) 1f else 0f, animSpec, - label = "animation progress") + val animationProgress by animateFloatAsState( + if (selected) 1f else 0f, + animSpec, + label = "animation progress", + ) JetsnackBottomNavItemLayout( icon = icon, text = text, animationProgress = animationProgress, - modifier = modifier - .selectable(selected = selected, onClick = onSelected) - .wrapContentSize() + modifier = + modifier + .selectable(selected = selected, onClick = onSelected) + .wrapContentSize(), ) } @@ -342,31 +346,33 @@ private fun JetsnackBottomNavItemLayout( icon: @Composable BoxScope.() -> Unit, text: @Composable BoxScope.() -> Unit, @FloatRange(from = 0.0, to = 1.0) animationProgress: Float, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Layout( modifier = modifier, content = { Box( - modifier = Modifier - .layoutId("icon") - .padding(horizontal = TextIconSpacing), - content = icon + modifier = + Modifier + .layoutId("icon") + .padding(horizontal = TextIconSpacing), + content = icon, ) val scale = lerp(0.6f, 1f, animationProgress) Box( - modifier = Modifier - .layoutId("text") - .padding(horizontal = TextIconSpacing) - .graphicsLayer { - alpha = animationProgress - scaleX = scale - scaleY = scale - transformOrigin = BottomNavLabelTransformOrigin - }, - content = text + modifier = + Modifier + .layoutId("text") + .padding(horizontal = TextIconSpacing) + .graphicsLayer { + alpha = animationProgress + scaleX = scale + scaleY = scale + transformOrigin = BottomNavLabelTransformOrigin + }, + content = text, ) - } + }, ) { measurables, constraints -> val iconPlaceable = measurables.first { it.layoutId == "icon" }.measure(constraints) val textPlaceable = measurables.first { it.layoutId == "text" }.measure(constraints) @@ -376,7 +382,7 @@ private fun JetsnackBottomNavItemLayout( iconPlaceable, constraints.maxWidth, constraints.maxHeight, - animationProgress + animationProgress, ) } } @@ -386,7 +392,7 @@ private fun MeasureScope.placeTextAndIcon( iconPlaceable: Placeable, width: Int, height: Int, - @FloatRange(from = 0.0, to = 1.0) animationProgress: Float + @FloatRange(from = 0.0, to = 1.0) animationProgress: Float, ): MeasureResult { val iconY = (height - iconPlaceable.height) / 2 val textY = (height - textPlaceable.height) / 2 @@ -407,13 +413,14 @@ private fun MeasureScope.placeTextAndIcon( private fun JetsnackBottomNavIndicator( strokeWidth: Dp = 2.dp, color: Color = JetsnackTheme.colors.iconInteractive, - shape: Shape = BottomNavIndicatorShape + shape: Shape = BottomNavIndicatorShape, ) { Spacer( - modifier = Modifier - .fillMaxSize() - .then(BottomNavigationItemPadding) - .border(strokeWidth, color, shape) + modifier = + Modifier + .fillMaxSize() + .then(BottomNavigationItemPadding) + .border(strokeWidth, color, shape), ) } @@ -430,7 +437,7 @@ private fun JetsnackBottomNavPreview() { JetsnackBottomBar( tabs = HomeSections.entries.toTypedArray(), currentRoute = "home/feed", - navigateToRoute = { } + navigateToRoute = { }, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Profile.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Profile.kt index c425d5bb42..1493268ee2 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Profile.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Profile.kt @@ -39,33 +39,32 @@ import com.example.jetsnack.R import com.example.jetsnack.ui.theme.JetsnackTheme @Composable -fun Profile( - modifier: Modifier = Modifier -) { +fun Profile(modifier: Modifier = Modifier) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxSize() - .wrapContentSize() - .padding(24.dp) + modifier = + modifier + .fillMaxSize() + .wrapContentSize() + .padding(24.dp), ) { Image( painterResource(R.drawable.empty_state_search), - contentDescription = null + contentDescription = null, ) Spacer(Modifier.height(24.dp)) Text( text = stringResource(R.string.work_in_progress), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(16.dp)) Text( text = stringResource(R.string.grab_beverage), style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt index 3fd93f68e3..503c204985 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt @@ -93,7 +93,7 @@ import kotlin.math.roundToInt fun Cart( onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier, - viewModel: CartViewModel = viewModel(factory = CartViewModel.provideFactory()) + viewModel: CartViewModel = viewModel(factory = CartViewModel.provideFactory()), ) { val orderLines by viewModel.orderLines.collectAsStateWithLifecycle() val inspiredByCart = remember { SnackRepo.getInspiredByCart() } @@ -104,7 +104,7 @@ fun Cart( decreaseItemCount = viewModel::decreaseSnackCount, inspiredByCart = inspiredByCart, onSnackClick = onSnackClick, - modifier = modifier + modifier = modifier, ) } @@ -116,7 +116,7 @@ fun Cart( decreaseItemCount: (Long) -> Unit, inspiredByCart: SnackCollection, onSnackClick: (Long, String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { JetsnackSurface(modifier = modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) { @@ -127,7 +127,7 @@ fun Cart( decreaseItemCount = decreaseItemCount, inspiredByCart = inspiredByCart, onSnackClick = onSnackClick, - modifier = Modifier.align(Alignment.TopCenter) + modifier = Modifier.align(Alignment.TopCenter), ) DestinationBar(modifier = Modifier.align(Alignment.TopCenter)) CheckoutBar(modifier = Modifier.align(Alignment.BottomCenter)) @@ -143,23 +143,25 @@ private fun CartContent( decreaseItemCount: (Long) -> Unit, inspiredByCart: SnackCollection, onSnackClick: (Long, String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val resources = LocalContext.current.resources - val snackCountFormattedString = remember(orderLines.size, resources) { - resources.getQuantityString( - R.plurals.cart_order_count, - orderLines.size, orderLines.size - ) - } + val snackCountFormattedString = + remember(orderLines.size, resources) { + resources.getQuantityString( + R.plurals.cart_order_count, + orderLines.size, + orderLines.size, + ) + } val itemAnimationSpecFade = nonSpatialExpressiveSpring() val itemPlacementSpec = spatialExpressiveSpring() LazyColumn(modifier) { item(key = "title") { Spacer( Modifier.windowInsetsTopHeight( - WindowInsets.statusBars.add(WindowInsets(top = 56.dp)) - ) + WindowInsets.statusBars.add(WindowInsets(top = 56.dp)), + ), ) Text( text = stringResource(R.string.cart_order_header, snackCountFormattedString), @@ -167,19 +169,21 @@ private fun CartContent( color = JetsnackTheme.colors.brand, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier - .heightIn(min = 56.dp) - .padding(horizontal = 24.dp, vertical = 4.dp) - .wrapContentHeight() + modifier = + Modifier + .heightIn(min = 56.dp) + .padding(horizontal = 24.dp, vertical = 4.dp) + .wrapContentHeight(), ) } items(orderLines, key = { it.snack.id }) { orderLine -> SwipeDismissItem( - modifier = Modifier.animateItem( - fadeInSpec = itemAnimationSpecFade, - fadeOutSpec = itemAnimationSpecFade, - placementSpec = itemPlacementSpec - ), + modifier = + Modifier.animateItem( + fadeInSpec = itemAnimationSpecFade, + fadeOutSpec = itemAnimationSpecFade, + placementSpec = itemPlacementSpec, + ), background = { progress -> SwipeDismissItemBackground(progress) }, @@ -189,31 +193,33 @@ private fun CartContent( removeSnack = removeSnack, increaseItemCount = increaseItemCount, decreaseItemCount = decreaseItemCount, - onSnackClick = onSnackClick + onSnackClick = onSnackClick, ) } } item("summary") { SummaryItem( - modifier = Modifier.animateItem( - fadeInSpec = itemAnimationSpecFade, - fadeOutSpec = itemAnimationSpecFade, - placementSpec = itemPlacementSpec - ), + modifier = + Modifier.animateItem( + fadeInSpec = itemAnimationSpecFade, + fadeOutSpec = itemAnimationSpecFade, + placementSpec = itemPlacementSpec, + ), subtotal = orderLines.sumOf { it.snack.price * it.count }, - shippingCosts = 369 + shippingCosts = 369, ) } item(key = "inspiredByCart") { SnackCollection( - modifier = Modifier.animateItem( - fadeInSpec = itemAnimationSpecFade, - fadeOutSpec = itemAnimationSpecFade, - placementSpec = itemPlacementSpec - ), + modifier = + Modifier.animateItem( + fadeInSpec = itemAnimationSpecFade, + fadeOutSpec = itemAnimationSpecFade, + placementSpec = itemPlacementSpec, + ), snackCollection = inspiredByCart, onSnackClick = onSnackClick, - highlight = false + highlight = false, ) Spacer(Modifier.height(56.dp)) } @@ -223,46 +229,51 @@ private fun CartContent( @Composable private fun SwipeDismissItemBackground(progress: Float) { Column( - modifier = Modifier - .background(JetsnackTheme.colors.uiBackground) - .fillMaxWidth() - .fillMaxHeight(), + modifier = + Modifier + .background(JetsnackTheme.colors.uiBackground) + .fillMaxWidth() + .fillMaxHeight(), horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { // Set 4.dp padding only if progress is less than halfway val padding: Dp by animateDpAsState( - if (progress < 0.5f) 4.dp else 0.dp, label = "padding" + if (progress < 0.5f) 4.dp else 0.dp, + label = "padding", ) BoxWithConstraints( Modifier - .fillMaxWidth(progress) + .fillMaxWidth(progress), ) { Surface( - modifier = Modifier - .padding(padding) - .fillMaxWidth() - .height(maxWidth) - .align(Alignment.Center), + modifier = + Modifier + .padding(padding) + .fillMaxWidth() + .height(maxWidth) + .align(Alignment.Center), shape = RoundedCornerShape(percent = ((1 - progress) * 100).roundToInt()), - color = JetsnackTheme.colors.error + color = JetsnackTheme.colors.error, ) { Box( modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { // Icon must be visible while in this width range if (progress in 0.125f..0.475f) { // Icon alpha decreases as it is about to disappear val iconAlpha: Float by animateFloatAsState( - if (progress > 0.4f) 0.5f else 1f, label = "icon alpha" + if (progress > 0.4f) 0.5f else 1f, + label = "icon alpha", ) Icon( imageVector = Icons.Filled.DeleteForever, - modifier = Modifier - .size(32.dp) - .graphicsLayer(alpha = iconAlpha), + modifier = + Modifier + .size(32.dp) + .graphicsLayer(alpha = iconAlpha), tint = JetsnackTheme.colors.uiBackground, contentDescription = null, ) @@ -270,7 +281,8 @@ private fun SwipeDismissItemBackground(progress: Float) { /*Text opacity increases as the text is supposed to appear in the screen*/ val textAlpha by animateFloatAsState( - if (progress > 0.5f) 1f else 0.5f, label = "text alpha" + if (progress > 0.5f) 1f else 0.5f, + label = "text alpha", ) if (progress > 0.5f) { Text( @@ -278,10 +290,11 @@ private fun SwipeDismissItemBackground(progress: Float) { style = MaterialTheme.typography.titleMedium, color = JetsnackTheme.colors.uiBackground, textAlign = TextAlign.Center, - modifier = Modifier - .graphicsLayer( - alpha = textAlpha - ) + modifier = + Modifier + .graphicsLayer( + alpha = textAlpha, + ), ) } } @@ -297,108 +310,113 @@ fun CartItem( increaseItemCount: (Long) -> Unit, decreaseItemCount: (Long) -> Unit, onSnackClick: (Long, String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val snack = orderLine.snack ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .clickable { onSnackClick(snack.id, "cart") } - .background(JetsnackTheme.colors.uiBackground) - .padding(horizontal = 24.dp) - + modifier = + modifier + .fillMaxWidth() + .clickable { onSnackClick(snack.id, "cart") } + .background(JetsnackTheme.colors.uiBackground) + .padding(horizontal = 24.dp), ) { val (divider, image, name, tag, priceSpacer, price, remove, quantity) = createRefs() createVerticalChain(name, tag, priceSpacer, price, chainStyle = ChainStyle.Packed) SnackImage( imageRes = snack.imageRes, contentDescription = null, - modifier = Modifier - .size(100.dp) - .constrainAs(image) { - top.linkTo(parent.top, margin = 16.dp) - bottom.linkTo(parent.bottom, margin = 16.dp) - start.linkTo(parent.start) - } + modifier = + Modifier + .size(100.dp) + .constrainAs(image) { + top.linkTo(parent.top, margin = 16.dp) + bottom.linkTo(parent.bottom, margin = 16.dp) + start.linkTo(parent.start) + }, ) Text( text = snack.name, style = MaterialTheme.typography.titleMedium, color = JetsnackTheme.colors.textSecondary, - modifier = Modifier.constrainAs(name) { - linkTo( - start = image.end, - startMargin = 16.dp, - end = remove.start, - endMargin = 16.dp, - bias = 0f - ) - } + modifier = + Modifier.constrainAs(name) { + linkTo( + start = image.end, + startMargin = 16.dp, + end = remove.start, + endMargin = 16.dp, + bias = 0f, + ) + }, ) IconButton( onClick = { removeSnack(snack.id) }, - modifier = Modifier - .constrainAs(remove) { - top.linkTo(parent.top) - end.linkTo(parent.end) - } - .padding(top = 12.dp) + modifier = + Modifier + .constrainAs(remove) { + top.linkTo(parent.top) + end.linkTo(parent.end) + }.padding(top = 12.dp), ) { Icon( imageVector = Icons.Filled.Close, tint = JetsnackTheme.colors.iconSecondary, - contentDescription = stringResource(R.string.label_remove) + contentDescription = stringResource(R.string.label_remove), ) } Text( text = snack.tagline, style = MaterialTheme.typography.bodyLarge, color = JetsnackTheme.colors.textHelp, - modifier = Modifier.constrainAs(tag) { - linkTo( - start = image.end, - startMargin = 16.dp, - end = parent.end, - endMargin = 16.dp, - bias = 0f - ) - } + modifier = + Modifier.constrainAs(tag) { + linkTo( + start = image.end, + startMargin = 16.dp, + end = parent.end, + endMargin = 16.dp, + bias = 0f, + ) + }, ) Spacer( Modifier .height(8.dp) .constrainAs(priceSpacer) { linkTo(top = tag.bottom, bottom = price.top) - } + }, ) Text( text = formatPrice(snack.price), style = MaterialTheme.typography.titleMedium, color = JetsnackTheme.colors.textPrimary, - modifier = Modifier.constrainAs(price) { - linkTo( - start = image.end, - end = quantity.start, - startMargin = 16.dp, - endMargin = 16.dp, - bias = 0f - ) - } + modifier = + Modifier.constrainAs(price) { + linkTo( + start = image.end, + end = quantity.start, + startMargin = 16.dp, + endMargin = 16.dp, + bias = 0f, + ) + }, ) QuantitySelector( count = orderLine.count, decreaseItemCount = { decreaseItemCount(snack.id) }, increaseItemCount = { increaseItemCount(snack.id) }, - modifier = Modifier.constrainAs(quantity) { - baseline.linkTo(price.baseline) - end.linkTo(parent.end) - } + modifier = + Modifier.constrainAs(quantity) { + baseline.linkTo(price.baseline) + end.linkTo(parent.end) + }, ) JetsnackDivider( Modifier.constrainAs(divider) { linkTo(start = parent.start, end = parent.end) top.linkTo(parent.bottom) - } + }, ) } } @@ -407,7 +425,7 @@ fun CartItem( fun SummaryItem( subtotal: Long, shippingCosts: Long, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Column(modifier) { Text( @@ -416,39 +434,42 @@ fun SummaryItem( color = JetsnackTheme.colors.brand, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier - .padding(horizontal = 24.dp) - .heightIn(min = 56.dp) - .wrapContentHeight() + modifier = + Modifier + .padding(horizontal = 24.dp) + .heightIn(min = 56.dp) + .wrapContentHeight(), ) Row(modifier = Modifier.padding(horizontal = 24.dp)) { Text( text = stringResource(R.string.cart_subtotal_label), style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .weight(1f) - .wrapContentWidth(Alignment.Start) - .alignBy(LastBaseline) + modifier = + Modifier + .weight(1f) + .wrapContentWidth(Alignment.Start) + .alignBy(LastBaseline), ) Text( text = formatPrice(subtotal), style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.alignBy(LastBaseline) + modifier = Modifier.alignBy(LastBaseline), ) } Row(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { Text( text = stringResource(R.string.cart_shipping_label), style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .weight(1f) - .wrapContentWidth(Alignment.Start) - .alignBy(LastBaseline) + modifier = + Modifier + .weight(1f) + .wrapContentWidth(Alignment.Start) + .alignBy(LastBaseline), ) Text( text = formatPrice(shippingCosts), style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.alignBy(LastBaseline) + modifier = Modifier.alignBy(LastBaseline), ) } Spacer(modifier = Modifier.height(8.dp)) @@ -457,16 +478,17 @@ fun SummaryItem( Text( text = stringResource(R.string.cart_total_label), style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .weight(1f) - .padding(end = 16.dp) - .wrapContentWidth(Alignment.End) - .alignBy(LastBaseline) + modifier = + Modifier + .weight(1f) + .padding(end = 16.dp) + .wrapContentWidth(Alignment.End) + .alignBy(LastBaseline), ) Text( text = formatPrice(subtotal + shippingCosts), style = MaterialTheme.typography.titleMedium, - modifier = Modifier.alignBy(LastBaseline) + modifier = Modifier.alignBy(LastBaseline), ) } JetsnackDivider() @@ -477,25 +499,25 @@ fun SummaryItem( private fun CheckoutBar(modifier: Modifier = Modifier) { Column( modifier.background( - JetsnackTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque) - ) + JetsnackTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque), + ), ) { - JetsnackDivider() Row { Spacer(Modifier.weight(1f)) JetsnackButton( onClick = { /* todo */ }, shape = RectangleShape, - modifier = Modifier - .padding(horizontal = 12.dp, vertical = 8.dp) - .weight(1f) + modifier = + Modifier + .padding(horizontal = 12.dp, vertical = 8.dp) + .weight(1f), ) { Text( text = stringResource(id = R.string.cart_checkout), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Left, - maxLines = 1 + maxLines = 1, ) } } @@ -514,7 +536,7 @@ private fun CartPreview() { increaseItemCount = {}, decreaseItemCount = {}, inspiredByCart = SnackRepo.getInspiredByCart(), - onSnackClick = { _, _ -> } + onSnackClick = { _, _ -> }, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/CartViewModel.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/CartViewModel.kt index 41dbfa73ab..a11f839731 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/CartViewModel.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/CartViewModel.kt @@ -32,15 +32,15 @@ import kotlinx.coroutines.flow.StateFlow */ class CartViewModel( private val snackbarManager: SnackbarManager, - snackRepository: SnackRepo + snackRepository: SnackRepo, ) : ViewModel() { - private val _orderLines: MutableStateFlow> = MutableStateFlow(snackRepository.getCart()) val orderLines: StateFlow> get() = _orderLines // Logic to show errors every few requests private var requestCount = 0 + private fun shouldRandomlyFail(): Boolean = ++requestCount % 5 == 0 fun increaseSnackCount(snackId: Long) { @@ -71,14 +71,18 @@ class CartViewModel( _orderLines.value = _orderLines.value.filter { it.snack.id != snackId } } - private fun updateSnackCount(snackId: Long, count: Int) { - _orderLines.value = _orderLines.value.map { - if (it.snack.id == snackId) { - it.copy(count = count) - } else { - it + private fun updateSnackCount( + snackId: Long, + count: Int, + ) { + _orderLines.value = + _orderLines.value.map { + if (it.snack.id == snackId) { + it.copy(count = count) + } else { + it + } } - } } /** @@ -87,12 +91,11 @@ class CartViewModel( companion object { fun provideFactory( snackbarManager: SnackbarManager = SnackbarManager, - snackRepository: SnackRepo = SnackRepo - ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return CartViewModel(snackbarManager, snackRepository) as T + snackRepository: SnackRepo = SnackRepo, + ): ViewModelProvider.Factory = + object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = CartViewModel(snackbarManager, snackRepository) as T } - } } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt index ccc695715c..3e806fdc50 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt @@ -28,11 +28,11 @@ import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -@OptIn(ExperimentalMaterial3Api::class) -@Composable /** * Holds the Swipe to dismiss composable, its animation and the current state */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable fun SwipeDismissItem( modifier: Modifier = Modifier, enter: EnterTransition = expandVertically(), @@ -49,14 +49,14 @@ fun SwipeDismissItem( modifier = modifier, visible = !isDismissed, enter = enter, - exit = exit + exit = exit, ) { SwipeToDismissBox( modifier = modifier, state = dismissState, enableDismissFromStartToEnd = false, backgroundContent = { background(dismissState.progress) }, - content = { content(isDismissed) } + content = { content(isDismissed) }, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt index 2bce3b328c..c87824e032 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt @@ -51,9 +51,7 @@ import com.example.jetsnack.ui.theme.JetsnackTheme import kotlin.math.max @Composable -fun SearchCategories( - categories: List -) { +fun SearchCategories(categories: List) { LazyColumn { itemsIndexed(categories) { index, collection -> SearchCategoryCollection(collection, index) @@ -66,28 +64,30 @@ fun SearchCategories( private fun SearchCategoryCollection( collection: SearchCategoryCollection, index: Int, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Column(modifier) { Text( text = collection.name, style = MaterialTheme.typography.titleLarge, color = JetsnackTheme.colors.textPrimary, - modifier = Modifier - .heightIn(min = 56.dp) - .padding(horizontal = 24.dp, vertical = 4.dp) - .wrapContentHeight() + modifier = + Modifier + .heightIn(min = 56.dp) + .padding(horizontal = 24.dp, vertical = 4.dp) + .wrapContentHeight(), ) VerticalGrid(Modifier.padding(horizontal = 16.dp)) { - val gradient = when (index % 2) { - 0 -> JetsnackTheme.colors.gradient2_2 - else -> JetsnackTheme.colors.gradient2_3 - } + val gradient = + when (index % 2) { + 0 -> JetsnackTheme.colors.gradient2_2 + else -> JetsnackTheme.colors.gradient2_3 + } collection.categories.forEach { category -> SearchCategory( category = category, gradient = gradient, - modifier = Modifier.padding(8.dp) + modifier = Modifier.padding(8.dp), ) } } @@ -103,30 +103,32 @@ private const val CategoryTextProportion = 0.55f private fun SearchCategory( category: SearchCategory, gradient: List, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Layout( - modifier = modifier - .aspectRatio(1.45f) - .shadow(elevation = 3.dp, shape = CategoryShape) - .clip(CategoryShape) - .background(Brush.horizontalGradient(gradient)) - .clickable { /* todo */ }, + modifier = + modifier + .aspectRatio(1.45f) + .shadow(elevation = 3.dp, shape = CategoryShape) + .clip(CategoryShape) + .background(Brush.horizontalGradient(gradient)) + .clickable { /* todo */ }, content = { Text( text = category.name, style = MaterialTheme.typography.titleMedium, color = JetsnackTheme.colors.textSecondary, - modifier = Modifier - .padding(4.dp) - .padding(start = 8.dp) + modifier = + Modifier + .padding(4.dp) + .padding(start = 8.dp), ) SnackImage( imageRes = category.imageRes, contentDescription = null, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) - } + }, ) { measurables, constraints -> // Text given a set proportion of width (which is determined by the aspect ratio) val textWidth = (constraints.maxWidth * CategoryTextProportion).toInt() @@ -138,16 +140,16 @@ private fun SearchCategory( val imagePlaceable = measurables[1].measure(Constraints.fixed(imageSize, imageSize)) layout( width = constraints.maxWidth, - height = constraints.minHeight + height = constraints.minHeight, ) { textPlaceable.placeRelative( x = 0, - y = (constraints.maxHeight - textPlaceable.height) / 2 // centered + y = (constraints.maxHeight - textPlaceable.height) / 2, // centered ) imagePlaceable.placeRelative( // image is placed to end of text i.e. will overflow to the end (but be clipped) x = textWidth, - y = (constraints.maxHeight - imagePlaceable.height) / 2 // centered + y = (constraints.maxHeight - imagePlaceable.height) / 2, // centered ) } } @@ -160,11 +162,12 @@ private fun SearchCategory( private fun SearchCategoryPreview() { JetsnackTheme { SearchCategory( - category = SearchCategory( - name = "Desserts", - imageRes = R.drawable.desserts - ), - gradient = JetsnackTheme.colors.gradient3_2 + category = + SearchCategory( + name = "Desserts", + imageRes = R.drawable.desserts, + ), + gradient = JetsnackTheme.colors.gradient3_2, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt index d6e18fe7fe..71bd097ae5 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt @@ -17,7 +17,6 @@ package com.example.jetsnack.ui.home.search import android.content.res.Configuration -import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -48,10 +47,8 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ChainStyle import androidx.constraintlayout.compose.ConstraintLayout import com.example.jetsnack.R -import com.example.jetsnack.model.Filter import com.example.jetsnack.model.Snack import com.example.jetsnack.model.snacks -import com.example.jetsnack.ui.components.FilterBar import com.example.jetsnack.ui.components.JetsnackButton import com.example.jetsnack.ui.components.JetsnackDivider import com.example.jetsnack.ui.components.JetsnackSurface @@ -62,14 +59,14 @@ import com.example.jetsnack.ui.utils.formatPrice @Composable fun SearchResults( searchResults: List, - onSnackClick: (Long, String) -> Unit + onSnackClick: (Long, String) -> Unit, ) { Column { Text( text = stringResource(R.string.search_count, searchResults.size), style = MaterialTheme.typography.titleLarge, color = JetsnackTheme.colors.textPrimary, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp) + modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp), ) LazyColumn { itemsIndexed(searchResults) { index, snack -> @@ -84,13 +81,14 @@ private fun SearchResult( snack: Snack, onSnackClick: (Long, String) -> Unit, showDivider: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .clickable { onSnackClick(snack.id, "search") } - .padding(horizontal = 24.dp) + modifier = + modifier + .fillMaxWidth() + .clickable { onSnackClick(snack.id, "search") } + .padding(horizontal = 24.dp), ) { val (divider, image, name, tag, priceSpacer, price, add) = createRefs() createVerticalChain(name, tag, priceSpacer, price, chainStyle = ChainStyle.Packed) @@ -99,87 +97,92 @@ private fun SearchResult( Modifier.constrainAs(divider) { linkTo(start = parent.start, end = parent.end) top.linkTo(parent.top) - } + }, ) } SnackImage( imageRes = snack.imageRes, contentDescription = null, - modifier = Modifier - .size(100.dp) - .constrainAs(image) { - linkTo( - top = parent.top, - topMargin = 16.dp, - bottom = parent.bottom, - bottomMargin = 16.dp - ) - start.linkTo(parent.start) - } + modifier = + Modifier + .size(100.dp) + .constrainAs(image) { + linkTo( + top = parent.top, + topMargin = 16.dp, + bottom = parent.bottom, + bottomMargin = 16.dp, + ) + start.linkTo(parent.start) + }, ) Text( text = snack.name, style = MaterialTheme.typography.titleMedium, color = JetsnackTheme.colors.textSecondary, - modifier = Modifier.constrainAs(name) { - linkTo( - start = image.end, - startMargin = 16.dp, - end = add.start, - endMargin = 16.dp, - bias = 0f - ) - } + modifier = + Modifier.constrainAs(name) { + linkTo( + start = image.end, + startMargin = 16.dp, + end = add.start, + endMargin = 16.dp, + bias = 0f, + ) + }, ) Text( text = snack.tagline, style = MaterialTheme.typography.bodyLarge, color = JetsnackTheme.colors.textHelp, - modifier = Modifier.constrainAs(tag) { - linkTo( - start = image.end, - startMargin = 16.dp, - end = add.start, - endMargin = 16.dp, - bias = 0f - ) - } + modifier = + Modifier.constrainAs(tag) { + linkTo( + start = image.end, + startMargin = 16.dp, + end = add.start, + endMargin = 16.dp, + bias = 0f, + ) + }, ) Spacer( Modifier .height(8.dp) .constrainAs(priceSpacer) { linkTo(top = tag.bottom, bottom = price.top) - } + }, ) Text( text = formatPrice(snack.price), style = MaterialTheme.typography.titleMedium, color = JetsnackTheme.colors.textPrimary, - modifier = Modifier.constrainAs(price) { - linkTo( - start = image.end, - startMargin = 16.dp, - end = add.start, - endMargin = 16.dp, - bias = 0f - ) - } + modifier = + Modifier.constrainAs(price) { + linkTo( + start = image.end, + startMargin = 16.dp, + end = add.start, + endMargin = 16.dp, + bias = 0f, + ) + }, ) JetsnackButton( onClick = { /* todo */ }, shape = CircleShape, contentPadding = PaddingValues(0.dp), - modifier = Modifier - .size(36.dp) - .constrainAs(add) { - linkTo(top = parent.top, bottom = parent.bottom) - end.linkTo(parent.end) - } + modifier = + Modifier + .size(36.dp) + .constrainAs(add) { + linkTo(top = parent.top, bottom = parent.bottom) + end.linkTo(parent.end) + }, ) { Icon( imageVector = Icons.Outlined.Add, - contentDescription = stringResource(R.string.label_add) + contentDescription = stringResource(R.string.label_add), ) } } @@ -188,32 +191,33 @@ private fun SearchResult( @Composable fun NoResults( query: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxSize() - .wrapContentSize() - .padding(24.dp) + modifier = + modifier + .fillMaxSize() + .wrapContentSize() + .padding(24.dp), ) { Image( painterResource(R.drawable.empty_state_search), - contentDescription = null + contentDescription = null, ) Spacer(Modifier.height(24.dp)) Text( text = stringResource(R.string.search_no_matches, query), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(16.dp)) Text( text = stringResource(R.string.search_no_matches_retry), style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } } @@ -228,7 +232,7 @@ private fun SearchResultPreview() { SearchResult( snack = snacks[0], onSnackClick = { _, _ -> }, - showDivider = false + showDivider = false, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt index 059e023752..92bd2af9f6 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt @@ -68,7 +68,7 @@ import com.example.jetsnack.ui.theme.JetsnackTheme fun Search( onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier, - state: SearchState = rememberSearchState() + state: SearchState = rememberSearchState(), ) { JetsnackSurface(modifier = modifier.fillMaxSize()) { Column { @@ -79,7 +79,7 @@ fun Search( searchFocused = state.focused, onSearchFocusChange = { state.focused = it }, onClearQuery = { state.query = TextFieldValue("") }, - searching = state.searching + searching = state.searching, ) JetsnackDivider() @@ -90,17 +90,19 @@ fun Search( } when (state.searchDisplay) { SearchDisplay.Categories -> SearchCategories(state.categories) - SearchDisplay.Suggestions -> SearchSuggestions( - suggestions = state.suggestions, - onSuggestionSelect = { suggestion -> - state.query = TextFieldValue(suggestion) - } - ) + SearchDisplay.Suggestions -> + SearchSuggestions( + suggestions = state.suggestions, + onSuggestionSelect = { suggestion -> + state.query = TextFieldValue(suggestion) + }, + ) - SearchDisplay.Results -> SearchResults( - state.searchResults, - onSnackClick - ) + SearchDisplay.Results -> + SearchResults( + state.searchResults, + onSnackClick, + ) SearchDisplay.NoResults -> NoResults(state.query.text) } @@ -109,7 +111,10 @@ fun Search( } enum class SearchDisplay { - Categories, Suggestions, Results, NoResults + Categories, + Suggestions, + Results, + NoResults, } @Composable @@ -120,9 +125,9 @@ private fun rememberSearchState( categories: List = SearchRepo.getCategories(), suggestions: List = SearchRepo.getSuggestions(), filters: List = SnackRepo.getFilters(), - searchResults: List = emptyList() -): SearchState { - return remember { + searchResults: List = emptyList(), +): SearchState = + remember { SearchState( query = query, focused = focused, @@ -130,10 +135,9 @@ private fun rememberSearchState( categories = categories, suggestions = suggestions, filters = filters, - searchResults = searchResults + searchResults = searchResults, ) } -} @Stable class SearchState( @@ -143,7 +147,7 @@ class SearchState( categories: List, suggestions: List, filters: List, - searchResults: List + searchResults: List, ) { var query by mutableStateOf(query) var focused by mutableStateOf(focused) @@ -153,12 +157,13 @@ class SearchState( var filters by mutableStateOf(filters) var searchResults by mutableStateOf(searchResults) val searchDisplay: SearchDisplay - get() = when { - !focused && query.text.isEmpty() -> SearchDisplay.Categories - focused && query.text.isEmpty() -> SearchDisplay.Suggestions - searchResults.isEmpty() -> SearchDisplay.NoResults - else -> SearchDisplay.Results - } + get() = + when { + !focused && query.text.isEmpty() -> SearchDisplay.Categories + focused && query.text.isEmpty() -> SearchDisplay.Suggestions + searchResults.isEmpty() -> SearchDisplay.NoResults + else -> SearchDisplay.Results + } } @Composable @@ -169,16 +174,17 @@ private fun SearchBar( onSearchFocusChange: (Boolean) -> Unit, onClearQuery: () -> Unit, searching: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { JetsnackSurface( color = JetsnackTheme.colors.uiFloated, contentColor = JetsnackTheme.colors.textSecondary, shape = MaterialTheme.shapes.small, - modifier = modifier - .fillMaxWidth() - .height(56.dp) - .padding(horizontal = 24.dp, vertical = 8.dp) + modifier = + modifier + .fillMaxWidth() + .height(56.dp) + .padding(horizontal = 24.dp, vertical = 8.dp), ) { Box(Modifier.fillMaxSize()) { if (query.text.isEmpty()) { @@ -186,34 +192,37 @@ private fun SearchBar( } Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - .wrapContentHeight() + modifier = + Modifier + .fillMaxSize() + .wrapContentHeight(), ) { if (searchFocused) { IconButton(onClick = onClearQuery) { Icon( imageVector = Icons.AutoMirrored.Outlined.ArrowBack, tint = JetsnackTheme.colors.iconPrimary, - contentDescription = stringResource(R.string.label_back) + contentDescription = stringResource(R.string.label_back), ) } } BasicTextField( value = query, onValueChange = onQueryChange, - modifier = Modifier - .weight(1f) - .onFocusChanged { - onSearchFocusChange(it.isFocused) - } + modifier = + Modifier + .weight(1f) + .onFocusChanged { + onSearchFocusChange(it.isFocused) + }, ) if (searching) { CircularProgressIndicator( color = JetsnackTheme.colors.iconPrimary, - modifier = Modifier - .padding(horizontal = 6.dp) - .size(36.dp) + modifier = + Modifier + .padding(horizontal = 6.dp) + .size(36.dp), ) } else { Spacer(Modifier.width(IconSize)) // balance arrow icon @@ -229,19 +238,20 @@ private val IconSize = 48.dp private fun SearchHint() { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - .wrapContentSize() + modifier = + Modifier + .fillMaxSize() + .wrapContentSize(), ) { Icon( imageVector = Icons.Outlined.Search, tint = JetsnackTheme.colors.textHelp, - contentDescription = stringResource(R.string.label_search) + contentDescription = stringResource(R.string.label_search), ) Spacer(Modifier.width(8.dp)) Text( text = stringResource(R.string.search_jetsnack), - color = JetsnackTheme.colors.textHelp + color = JetsnackTheme.colors.textHelp, ) } } @@ -259,7 +269,7 @@ private fun SearchBarPreview() { searchFocused = false, onSearchFocusChange = { }, onClearQuery = { }, - searching = false + searching = false, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt index 638b70d686..fedaf79b6d 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt @@ -41,7 +41,7 @@ import com.example.jetsnack.ui.theme.JetsnackTheme @Composable fun SearchSuggestions( suggestions: List, - onSuggestionSelect: (String) -> Unit + onSuggestionSelect: (String) -> Unit, ) { LazyColumn { suggestions.forEach { suggestionGroup -> @@ -52,7 +52,7 @@ fun SearchSuggestions( Suggestion( suggestion = suggestion, onSuggestionSelect = onSuggestionSelect, - modifier = Modifier.fillParentMaxWidth() + modifier = Modifier.fillParentMaxWidth(), ) } item { @@ -65,16 +65,17 @@ fun SearchSuggestions( @Composable private fun SuggestionHeader( name: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Text( text = name, style = MaterialTheme.typography.titleLarge, color = JetsnackTheme.colors.textPrimary, - modifier = modifier - .heightIn(min = 56.dp) - .padding(horizontal = 24.dp, vertical = 4.dp) - .wrapContentHeight() + modifier = + modifier + .heightIn(min = 56.dp) + .padding(horizontal = 24.dp, vertical = 4.dp) + .wrapContentHeight(), ) } @@ -82,16 +83,17 @@ private fun SuggestionHeader( private fun Suggestion( suggestion: String, onSuggestionSelect: (String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Text( text = suggestion, style = MaterialTheme.typography.titleMedium, - modifier = modifier - .heightIn(min = 48.dp) - .clickable { onSuggestionSelect(suggestion) } - .padding(start = 24.dp) - .wrapContentSize(Alignment.CenterStart) + modifier = + modifier + .heightIn(min = 48.dp) + .clickable { onSuggestionSelect(suggestion) } + .padding(start = 24.dp) + .wrapContentSize(Alignment.CenterStart), ) } @@ -104,7 +106,7 @@ fun PreviewSuggestions() { JetsnackSurface { SearchSuggestions( suggestions = SearchRepo.getSuggestions(), - onSuggestionSelect = { } + onSuggestionSelect = { }, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/JetsnackNavController.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/JetsnackNavController.kt index dbd6c92f91..e717d26d59 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/JetsnackNavController.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/JetsnackNavController.kt @@ -40,11 +40,10 @@ object MainDestinations { * Remembers and creates an instance of [JetsnackNavController] */ @Composable -fun rememberJetsnackNavController( - navController: NavHostController = rememberNavController() -): JetsnackNavController = remember(navController) { - JetsnackNavController(navController) -} +fun rememberJetsnackNavController(navController: NavHostController = rememberNavController()): JetsnackNavController = + remember(navController) { + JetsnackNavController(navController) + } /** * Responsible for holding UI Navigation logic. @@ -53,7 +52,6 @@ fun rememberJetsnackNavController( class JetsnackNavController( val navController: NavHostController, ) { - // ---------------------------------------------------------- // Navigation state source of truth // ---------------------------------------------------------- @@ -76,7 +74,11 @@ class JetsnackNavController( } } - fun navigateToSnackDetail(snackId: Long, origin: String, from: NavBackStackEntry) { + fun navigateToSnackDetail( + snackId: Long, + origin: String, + from: NavBackStackEntry, + ) { // In order to discard duplicated navigation events, we check the Lifecycle if (from.lifecycleIsResumed()) { navController.navigate("${MainDestinations.SNACK_DETAIL_ROUTE}/$snackId?origin=$origin") @@ -89,8 +91,7 @@ class JetsnackNavController( * * This is used to de-duplicate navigation events. */ -private fun NavBackStackEntry.lifecycleIsResumed() = - this.lifecycle.currentState == Lifecycle.State.RESUMED +private fun NavBackStackEntry.lifecycleIsResumed() = this.lifecycle.currentState == Lifecycle.State.RESUMED private val NavGraph.startDestination: NavDestination? get() = findNode(startDestinationId) @@ -100,6 +101,5 @@ private val NavGraph.startDestination: NavDestination? * * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt */ -private tailrec fun findStartDestination(graph: NavDestination): NavDestination { - return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph -} +private tailrec fun findStartDestination(graph: NavDestination): NavDestination = + if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt index 043e7ecc74..8a4334d552 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt @@ -128,32 +128,37 @@ private val ExpandedImageSize = 300.dp private val CollapsedImageSize = 150.dp private val HzPadding = Modifier.padding(horizontal = 24.dp) -fun spatialExpressiveSpring() = spring( - dampingRatio = 0.8f, - stiffness = 380f -) - -fun nonSpatialExpressiveSpring() = spring( - dampingRatio = 1f, - stiffness = 1600f -) - -val snackDetailBoundsTransform = BoundsTransform { _, _ -> - spatialExpressiveSpring() -} +fun spatialExpressiveSpring() = + spring( + dampingRatio = 0.8f, + stiffness = 380f, + ) + +fun nonSpatialExpressiveSpring() = + spring( + dampingRatio = 1f, + stiffness = 1600f, + ) + +val snackDetailBoundsTransform = + BoundsTransform { _, _ -> + spatialExpressiveSpring() + } @Composable fun SnackDetail( snackId: Long, origin: String, - upPress: () -> Unit + upPress: () -> Unit, ) { val snack = remember(snackId) { SnackRepo.getSnack(snackId) } val related = remember(snackId) { SnackRepo.getRelated(snackId) } - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalStateException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalStateException("No Scope found") + val sharedTransitionScope = + LocalSharedTransitionScope.current + ?: throw IllegalStateException("No Scope found") + val animatedVisibilityScope = + LocalNavAnimatedVisibilityScope.current + ?: throw IllegalStateException("No Scope found") val roundedCornerAnim by animatedVisibilityScope.transition .animateDp(label = "rounded corner") { enterExit: EnterExitState -> when (enterExit) { @@ -168,21 +173,21 @@ fun SnackDetail( .clip(RoundedCornerShape(roundedCornerAnim)) .sharedBounds( rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = origin, - type = SnackSharedElementType.Bounds - ) + key = + SnackSharedElementKey( + snackId = snack.id, + origin = origin, + type = SnackSharedElementType.Bounds, + ), ), animatedVisibilityScope, clipInOverlayDuringTransition = - OverlayClip(RoundedCornerShape(roundedCornerAnim)), + OverlayClip(RoundedCornerShape(roundedCornerAnim)), boundsTransform = snackDetailBoundsTransform, exit = fadeOut(nonSpatialExpressiveSpring()), enter = fadeIn(nonSpatialExpressiveSpring()), - ) - .fillMaxSize() - .background(color = JetsnackTheme.colors.uiBackground) + ).fillMaxSize() + .background(color = JetsnackTheme.colors.uiBackground), ) { val scroll = rememberScrollState(0) Header(snack.id, origin = origin) @@ -196,83 +201,93 @@ fun SnackDetail( } @Composable -private fun Header(snackId: Long, origin: String) { - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalArgumentException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalArgumentException("No Scope found") +private fun Header( + snackId: Long, + origin: String, +) { + val sharedTransitionScope = + LocalSharedTransitionScope.current + ?: throw IllegalArgumentException("No Scope found") + val animatedVisibilityScope = + LocalNavAnimatedVisibilityScope.current + ?: throw IllegalArgumentException("No Scope found") with(sharedTransitionScope) { val brushColors = JetsnackTheme.colors.tornado1 val infiniteTransition = rememberInfiniteTransition(label = "background") - val targetOffset = with(LocalDensity.current) { - 1000.dp.toPx() - } + val targetOffset = + with(LocalDensity.current) { + 1000.dp.toPx() + } val offset by infiniteTransition.animateFloat( initialValue = 0f, targetValue = targetOffset, - animationSpec = infiniteRepeatable( - tween(50000, easing = LinearEasing), - repeatMode = RepeatMode.Reverse - ), - label = "offset" + animationSpec = + infiniteRepeatable( + tween(50000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "offset", ) Spacer( - modifier = Modifier - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snackId, - origin = origin, - type = SnackSharedElementType.Background - ) - ), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = snackDetailBoundsTransform, - enter = fadeIn(nonSpatialExpressiveSpring()), - exit = fadeOut(nonSpatialExpressiveSpring()), - resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() - ) - .height(280.dp) - .fillMaxWidth() - .blur(40.dp) - .drawWithCache { - val brushSize = 400f - val brush = Brush.linearGradient( - colors = brushColors, - start = Offset(offset, offset), - end = Offset(offset + brushSize, offset + brushSize), - tileMode = TileMode.Mirror - ) - onDrawBehind { - drawRect(brush) - } - } + modifier = + Modifier + .sharedBounds( + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snackId, + origin = origin, + type = SnackSharedElementType.Background, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = snackDetailBoundsTransform, + enter = fadeIn(nonSpatialExpressiveSpring()), + exit = fadeOut(nonSpatialExpressiveSpring()), + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds(), + ).height(280.dp) + .fillMaxWidth() + .blur(40.dp) + .drawWithCache { + val brushSize = 400f + val brush = + Brush.linearGradient( + colors = brushColors, + start = Offset(offset, offset), + end = Offset(offset + brushSize, offset + brushSize), + tileMode = TileMode.Mirror, + ) + onDrawBehind { + drawRect(brush) + } + }, ) } } @Composable private fun SharedTransitionScope.Up(upPress: () -> Unit) { - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalArgumentException("No Scope found") + val animatedVisibilityScope = + LocalNavAnimatedVisibilityScope.current + ?: throw IllegalArgumentException("No Scope found") with(animatedVisibilityScope) { IconButton( onClick = upPress, - modifier = Modifier - .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 3f) - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 10.dp) - .size(36.dp) - .animateEnterExit( - enter = scaleIn(tween(300, delayMillis = 300)), - exit = scaleOut(tween(20)) - ) - .background( - color = Neutral8.copy(alpha = 0.32f), - shape = CircleShape - ) + modifier = + Modifier + .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 3f) + .statusBarsPadding() + .padding(horizontal = 16.dp, vertical = 10.dp) + .size(36.dp) + .animateEnterExit( + enter = scaleIn(tween(300, delayMillis = 300)), + exit = scaleOut(tween(20)), + ).background( + color = Neutral8.copy(alpha = 0.32f), + shape = CircleShape, + ), ) { Icon( imageVector = Icons.AutoMirrored.Outlined.ArrowBack, @@ -286,21 +301,22 @@ private fun SharedTransitionScope.Up(upPress: () -> Unit) { @Composable private fun Body( related: List, - scroll: ScrollState + scroll: ScrollState, ) { val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No scope found") with(sharedTransitionScope) { Column(modifier = Modifier.skipToLookaheadSize()) { Spacer( - modifier = Modifier - .fillMaxWidth() - .statusBarsPadding() - .height(MinTitleOffset) + modifier = + Modifier + .fillMaxWidth() + .statusBarsPadding() + .height(MinTitleOffset), ) Column( - modifier = Modifier.verticalScroll(scroll) + modifier = Modifier.verticalScroll(scroll), ) { Spacer(Modifier.height(GradientScroll)) Spacer(Modifier.height(ImageOverlap)) @@ -315,7 +331,7 @@ private fun Body( text = stringResource(R.string.detail_header), style = MaterialTheme.typography.labelSmall, color = JetsnackTheme.colors.textHelp, - modifier = HzPadding + modifier = HzPadding, ) Spacer(Modifier.height(16.dp)) var seeMore by remember { mutableStateOf(true) } @@ -326,29 +342,29 @@ private fun Body( color = JetsnackTheme.colors.textHelp, maxLines = if (seeMore) 5 else Int.MAX_VALUE, overflow = TextOverflow.Ellipsis, - modifier = HzPadding.skipToLookaheadSize() - + modifier = HzPadding.skipToLookaheadSize(), ) } - val textButton = if (seeMore) { - stringResource(id = R.string.see_more) - } else { - stringResource(id = R.string.see_less) - } + val textButton = + if (seeMore) { + stringResource(id = R.string.see_more) + } else { + stringResource(id = R.string.see_less) + } Text( text = textButton, style = MaterialTheme.typography.labelLarge, textAlign = TextAlign.Center, color = JetsnackTheme.colors.textLink, - modifier = Modifier - .heightIn(20.dp) - .fillMaxWidth() - .padding(top = 15.dp) - .clickable { - seeMore = !seeMore - } - .skipToLookaheadSize() + modifier = + Modifier + .heightIn(20.dp) + .fillMaxWidth() + .padding(top = 15.dp) + .clickable { + seeMore = !seeMore + }.skipToLookaheadSize(), ) Spacer(Modifier.height(40.dp)) @@ -356,14 +372,14 @@ private fun Body( text = stringResource(R.string.ingredients), style = MaterialTheme.typography.labelSmall, color = JetsnackTheme.colors.textHelp, - modifier = HzPadding + modifier = HzPadding, ) Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.ingredients_list), style = MaterialTheme.typography.bodyLarge, color = JetsnackTheme.colors.textHelp, - modifier = HzPadding + modifier = HzPadding, ) Spacer(Modifier.height(16.dp)) @@ -374,16 +390,17 @@ private fun Body( SnackCollection( snackCollection = snackCollection, onSnackClick = { _, _ -> }, - highlight = false + highlight = false, ) } } Spacer( - modifier = Modifier - .padding(bottom = BottomBarHeight) - .navigationBarsPadding() - .height(8.dp) + modifier = + Modifier + .padding(bottom = BottomBarHeight) + .navigationBarsPadding() + .height(8.dp), ) } } @@ -393,27 +410,33 @@ private fun Body( } @Composable -private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) { +private fun Title( + snack: Snack, + origin: String, + scrollProvider: () -> Int, +) { val maxOffset = with(LocalDensity.current) { MaxTitleOffset.toPx() } val minOffset = with(LocalDensity.current) { MinTitleOffset.toPx() } - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalArgumentException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalArgumentException("No Scope found") + val sharedTransitionScope = + LocalSharedTransitionScope.current + ?: throw IllegalArgumentException("No Scope found") + val animatedVisibilityScope = + LocalNavAnimatedVisibilityScope.current + ?: throw IllegalArgumentException("No Scope found") with(sharedTransitionScope) { Column( verticalArrangement = Arrangement.Bottom, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = TitleHeight) - .statusBarsPadding() - .offset { - val scroll = scrollProvider() - val offset = (maxOffset - scroll).coerceAtLeast(minOffset) - IntOffset(x = 0, y = offset.toInt()) - } - .background(JetsnackTheme.colors.uiBackground) + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = TitleHeight) + .statusBarsPadding() + .offset { + val scroll = scrollProvider() + val offset = (maxOffset - scroll).coerceAtLeast(minOffset) + IntOffset(x = 0, y = offset.toInt()) + }.background(JetsnackTheme.colors.uiBackground), ) { Spacer(Modifier.height(16.dp)) Text( @@ -421,19 +444,20 @@ private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) { fontStyle = FontStyle.Italic, style = MaterialTheme.typography.headlineMedium, color = JetsnackTheme.colors.textSecondary, - modifier = HzPadding - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = origin, - type = SnackSharedElementType.Title - ) - ), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = snackDetailBoundsTransform - ) - .wrapContentWidth() + modifier = + HzPadding + .sharedBounds( + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snack.id, + origin = origin, + type = SnackSharedElementType.Title, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = snackDetailBoundsTransform, + ).wrapContentWidth(), ) Text( text = snack.tagline, @@ -441,19 +465,20 @@ private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) { style = MaterialTheme.typography.titleSmall, fontSize = 20.sp, color = JetsnackTheme.colors.textHelp, - modifier = HzPadding - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = origin, - type = SnackSharedElementType.Tagline - ) - ), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = snackDetailBoundsTransform - ) - .wrapContentWidth() + modifier = + HzPadding + .sharedBounds( + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snack.id, + origin = origin, + type = SnackSharedElementType.Tagline, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = snackDetailBoundsTransform, + ).wrapContentWidth(), ) Spacer(Modifier.height(4.dp)) with(animatedVisibilityScope) { @@ -461,12 +486,12 @@ private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) { text = formatPrice(snack.price), style = MaterialTheme.typography.titleLarge, color = JetsnackTheme.colors.textPrimary, - modifier = HzPadding - .animateEnterExit( - enter = fadeIn() + slideInVertically { -it / 3 }, - exit = fadeOut() + slideOutVertically { -it / 3 } - ) - .skipToLookaheadSize() + modifier = + HzPadding + .animateEnterExit( + enter = fadeIn() + slideInVertically { -it / 3 }, + exit = fadeOut() + slideOutVertically { -it / 3 }, + ).skipToLookaheadSize(), ) } Spacer(Modifier.height(8.dp)) @@ -481,7 +506,7 @@ private fun Image( origin: String, @DrawableRes imageRes: Int, - scrollProvider: () -> Int + scrollProvider: () -> Int, ) { val collapseRange = with(LocalDensity.current) { (MaxTitleOffset - MinTitleOffset).toPx() } val collapseFractionProvider = { @@ -490,33 +515,35 @@ private fun Image( CollapsingImageLayout( collapseFractionProvider = collapseFractionProvider, - modifier = HzPadding.statusBarsPadding() + modifier = HzPadding.statusBarsPadding(), ) { - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalStateException("No sharedTransitionScope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalStateException("No animatedVisibilityScope found") + val sharedTransitionScope = + LocalSharedTransitionScope.current + ?: throw IllegalStateException("No sharedTransitionScope found") + val animatedVisibilityScope = + LocalNavAnimatedVisibilityScope.current + ?: throw IllegalStateException("No animatedVisibilityScope found") with(sharedTransitionScope) { SnackImage( imageRes = imageRes, contentDescription = null, - modifier = Modifier - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snackId, - origin = origin, - type = SnackSharedElementType.Image - ) - ), - animatedVisibilityScope = animatedVisibilityScope, - exit = fadeOut(), - enter = fadeIn(), - boundsTransform = snackDetailBoundsTransform - ) - .fillMaxSize() - + modifier = + Modifier + .sharedBounds( + rememberSharedContentState( + key = + SnackSharedElementKey( + snackId = snackId, + origin = origin, + type = SnackSharedElementType.Image, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + exit = fadeOut(), + enter = fadeIn(), + boundsTransform = snackDetailBoundsTransform, + ).fillMaxSize(), ) } } @@ -526,11 +553,11 @@ private fun Image( private fun CollapsingImageLayout( collapseFractionProvider: () -> Float, modifier: Modifier = Modifier, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { Layout( modifier = modifier, - content = content + content = content, ) { measurables, constraints -> check(measurables.size == 1) @@ -542,14 +569,15 @@ private fun CollapsingImageLayout( val imagePlaceable = measurables[0].measure(Constraints.fixed(imageWidth, imageWidth)) val imageY = lerp(MinTitleOffset, MinImageOffset, collapseFraction).roundToPx() - val imageX = lerp( - (constraints.maxWidth - imageWidth) / 2, // centered when expanded - constraints.maxWidth - imageWidth, // right aligned when collapsed - collapseFraction - ) + val imageX = + lerp( + (constraints.maxWidth - imageWidth) / 2, // centered when expanded + constraints.maxWidth - imageWidth, // right aligned when collapsed + collapseFraction, + ) layout( width = constraints.maxWidth, - height = imageY + imageWidth + height = imageY + imageWidth, ) { imagePlaceable.placeRelative(imageX, imageY) } @@ -566,43 +594,47 @@ private fun CartBottomBar(modifier: Modifier = Modifier) { with(sharedTransitionScope) { with(animatedVisibilityScope) { JetsnackSurface( - modifier = modifier - .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 4f) - .animateEnterExit( - enter = slideInVertically( - tween( - 300, - delayMillis = 300 - ) - ) { it } + fadeIn(tween(300, delayMillis = 300)), - exit = slideOutVertically(tween(50)) { it } + - fadeOut(tween(50)) - ) + modifier = + modifier + .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 4f) + .animateEnterExit( + enter = + slideInVertically( + tween( + 300, + delayMillis = 300, + ), + ) { it } + fadeIn(tween(300, delayMillis = 300)), + exit = + slideOutVertically(tween(50)) { it } + + fadeOut(tween(50)), + ), ) { Column { JetsnackDivider() Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .navigationBarsPadding() - .then(HzPadding) - .heightIn(min = BottomBarHeight) + modifier = + Modifier + .navigationBarsPadding() + .then(HzPadding) + .heightIn(min = BottomBarHeight), ) { QuantitySelector( count = count, decreaseItemCount = { if (count > 0) updateCount(count - 1) }, - increaseItemCount = { updateCount(count + 1) } + increaseItemCount = { updateCount(count + 1) }, ) Spacer(Modifier.width(16.dp)) JetsnackButton( onClick = { /* todo */ }, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) { Text( text = stringResource(R.string.add_to_cart), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, - maxLines = 1 + maxLines = 1, ) } } @@ -621,7 +653,7 @@ private fun SnackDetailPreview() { SnackDetail( snackId = 1L, origin = "details", - upPress = { } + upPress = { }, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Shape.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Shape.kt index e9887fafe9..cd3dc85d8a 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Shape.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Shape.kt @@ -20,8 +20,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp -val Shapes = Shapes( - small = RoundedCornerShape(percent = 50), - medium = RoundedCornerShape(20.dp), - large = RoundedCornerShape(0.dp) -) +val Shapes = + Shapes( + small = RoundedCornerShape(percent = 50), + medium = RoundedCornerShape(20.dp), + large = RoundedCornerShape(0.dp), + ) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt index fe6ff88cce..3530c7f545 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt @@ -25,62 +25,64 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color -private val LightColorPalette = JetsnackColors( - brand = Shadow5, - brandSecondary = Ocean3, - uiBackground = Neutral0, - uiBorder = Neutral4, - uiFloated = FunctionalGrey, - textSecondary = Neutral7, - textHelp = Neutral6, - textInteractive = Neutral0, - textLink = Ocean11, - iconSecondary = Neutral7, - iconInteractive = Neutral0, - iconInteractiveInactive = Neutral1, - error = FunctionalRed, - gradient6_1 = listOf(Shadow4, Ocean3, Shadow2, Ocean3, Shadow4), - gradient6_2 = listOf(Rose4, Lavender3, Rose2, Lavender3, Rose4), - gradient3_1 = listOf(Shadow2, Ocean3, Shadow4), - gradient3_2 = listOf(Rose2, Lavender3, Rose4), - gradient2_1 = listOf(Shadow4, Shadow11), - gradient2_2 = listOf(Ocean3, Shadow3), - gradient2_3 = listOf(Lavender3, Rose2), - tornado1 = listOf(Shadow4, Ocean3), - isDark = false -) +private val LightColorPalette = + JetsnackColors( + brand = Shadow5, + brandSecondary = Ocean3, + uiBackground = Neutral0, + uiBorder = Neutral4, + uiFloated = FunctionalGrey, + textSecondary = Neutral7, + textHelp = Neutral6, + textInteractive = Neutral0, + textLink = Ocean11, + iconSecondary = Neutral7, + iconInteractive = Neutral0, + iconInteractiveInactive = Neutral1, + error = FunctionalRed, + gradient6_1 = listOf(Shadow4, Ocean3, Shadow2, Ocean3, Shadow4), + gradient6_2 = listOf(Rose4, Lavender3, Rose2, Lavender3, Rose4), + gradient3_1 = listOf(Shadow2, Ocean3, Shadow4), + gradient3_2 = listOf(Rose2, Lavender3, Rose4), + gradient2_1 = listOf(Shadow4, Shadow11), + gradient2_2 = listOf(Ocean3, Shadow3), + gradient2_3 = listOf(Lavender3, Rose2), + tornado1 = listOf(Shadow4, Ocean3), + isDark = false, + ) -private val DarkColorPalette = JetsnackColors( - brand = Shadow1, - brandSecondary = Ocean2, - uiBackground = Neutral8, - uiBorder = Neutral3, - uiFloated = FunctionalDarkGrey, - textPrimary = Shadow1, - textSecondary = Neutral0, - textHelp = Neutral1, - textInteractive = Neutral7, - textLink = Ocean2, - iconPrimary = Shadow1, - iconSecondary = Neutral0, - iconInteractive = Neutral7, - iconInteractiveInactive = Neutral6, - error = FunctionalRedDark, - gradient6_1 = listOf(Shadow5, Ocean7, Shadow9, Ocean7, Shadow5), - gradient6_2 = listOf(Rose11, Lavender7, Rose8, Lavender7, Rose11), - gradient3_1 = listOf(Shadow9, Ocean7, Shadow5), - gradient3_2 = listOf(Rose8, Lavender7, Rose11), - gradient2_1 = listOf(Ocean3, Shadow3), - gradient2_2 = listOf(Ocean4, Shadow2), - gradient2_3 = listOf(Lavender3, Rose3), - tornado1 = listOf(Shadow4, Ocean3), - isDark = true -) +private val DarkColorPalette = + JetsnackColors( + brand = Shadow1, + brandSecondary = Ocean2, + uiBackground = Neutral8, + uiBorder = Neutral3, + uiFloated = FunctionalDarkGrey, + textPrimary = Shadow1, + textSecondary = Neutral0, + textHelp = Neutral1, + textInteractive = Neutral7, + textLink = Ocean2, + iconPrimary = Shadow1, + iconSecondary = Neutral0, + iconInteractive = Neutral7, + iconInteractiveInactive = Neutral6, + error = FunctionalRedDark, + gradient6_1 = listOf(Shadow5, Ocean7, Shadow9, Ocean7, Shadow5), + gradient6_2 = listOf(Rose11, Lavender7, Rose8, Lavender7, Rose11), + gradient3_1 = listOf(Shadow9, Ocean7, Shadow5), + gradient3_2 = listOf(Rose8, Lavender7, Rose11), + gradient2_1 = listOf(Ocean3, Shadow3), + gradient2_2 = listOf(Ocean4, Shadow2), + gradient2_3 = listOf(Lavender3, Rose3), + tornado1 = listOf(Shadow4, Ocean3), + isDark = true, + ) @Composable fun JetsnackTheme( darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val colors = if (darkTheme) DarkColorPalette else LightColorPalette @@ -89,7 +91,7 @@ fun JetsnackTheme( colorScheme = debugColors(darkTheme), typography = Typography, shapes = Shapes, - content = content + content = content, ) } } @@ -132,20 +134,21 @@ data class JetsnackColors( val iconInteractiveInactive: Color, val error: Color, val notificationBadge: Color = error, - val isDark: Boolean + val isDark: Boolean, ) @Composable fun ProvideJetsnackColors( colors: JetsnackColors, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { CompositionLocalProvider(LocalJetsnackColors provides colors, content = content) } -private val LocalJetsnackColors = staticCompositionLocalOf { - error("No JetsnackColorPalette provided") -} +private val LocalJetsnackColors = + staticCompositionLocalOf { + error("No JetsnackColorPalette provided") + } /** * A Material [Colors] implementation which sets all colors to [debugColor] to discourage usage of @@ -153,7 +156,7 @@ private val LocalJetsnackColors = staticCompositionLocalOf { */ fun debugColors( darkTheme: Boolean, - debugColor: Color = Color.Magenta + debugColor: Color = Color.Magenta, ) = ColorScheme( primary = debugColor, onPrimary = debugColor, diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt index bda6b38704..cb0575f4b3 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt @@ -24,104 +24,120 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import com.example.jetsnack.R -private val Montserrat = FontFamily( - Font(R.font.montserrat_light, FontWeight.Light), - Font(R.font.montserrat_regular, FontWeight.Normal), - Font(R.font.montserrat_medium, FontWeight.Medium), - Font(R.font.montserrat_semibold, FontWeight.SemiBold) -) +private val Montserrat = + FontFamily( + Font(R.font.montserrat_light, FontWeight.Light), + Font(R.font.montserrat_regular, FontWeight.Normal), + Font(R.font.montserrat_medium, FontWeight.Medium), + Font(R.font.montserrat_semibold, FontWeight.SemiBold), + ) -private val Karla = FontFamily( - Font(R.font.karla_regular, FontWeight.Normal), - Font(R.font.karla_bold, FontWeight.Bold) -) +private val Karla = + FontFamily( + Font(R.font.karla_regular, FontWeight.Normal), + Font(R.font.karla_bold, FontWeight.Bold), + ) -val Typography = Typography( - displayLarge = TextStyle( - fontFamily = Montserrat, - fontSize = 96.sp, - fontWeight = FontWeight.Light, - lineHeight = 117.sp, - letterSpacing = (-1.5).sp - ), - displayMedium = TextStyle( - fontFamily = Montserrat, - fontSize = 60.sp, - fontWeight = FontWeight.Light, - lineHeight = 73.sp, - letterSpacing = (-0.5).sp - ), - displaySmall = TextStyle( - fontFamily = Montserrat, - fontSize = 48.sp, - fontWeight = FontWeight.Normal, - lineHeight = 59.sp - ), - headlineMedium = TextStyle( - fontFamily = Montserrat, - fontSize = 30.sp, - fontWeight = FontWeight.SemiBold, - lineHeight = 37.sp - ), - headlineSmall = TextStyle( - fontFamily = Montserrat, - fontSize = 24.sp, - fontWeight = FontWeight.SemiBold, - lineHeight = 29.sp - ), - titleLarge = TextStyle( - fontFamily = Montserrat, - fontSize = 20.sp, - fontWeight = FontWeight.SemiBold, - lineHeight = 24.sp - ), - titleMedium = TextStyle( - fontFamily = Montserrat, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - titleSmall = TextStyle( - fontFamily = Karla, - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - lineHeight = 24.sp, - letterSpacing = 0.1.sp - ), - bodyLarge = TextStyle( - fontFamily = Karla, - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - lineHeight = 28.sp, - letterSpacing = 0.15.sp - ), - bodyMedium = TextStyle( - fontFamily = Montserrat, - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - lineHeight = 20.sp, - letterSpacing = 0.25.sp - ), - labelLarge = TextStyle( - fontFamily = Montserrat, - fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, - lineHeight = 16.sp, - letterSpacing = 1.25.sp - ), - bodySmall = TextStyle( - fontFamily = Karla, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - lineHeight = 16.sp, - letterSpacing = 0.4.sp - ), - labelSmall = TextStyle( - fontFamily = Montserrat, - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold, - lineHeight = 16.sp, - letterSpacing = 1.sp +val Typography = + Typography( + displayLarge = + TextStyle( + fontFamily = Montserrat, + fontSize = 96.sp, + fontWeight = FontWeight.Light, + lineHeight = 117.sp, + letterSpacing = (-1.5).sp, + ), + displayMedium = + TextStyle( + fontFamily = Montserrat, + fontSize = 60.sp, + fontWeight = FontWeight.Light, + lineHeight = 73.sp, + letterSpacing = (-0.5).sp, + ), + displaySmall = + TextStyle( + fontFamily = Montserrat, + fontSize = 48.sp, + fontWeight = FontWeight.Normal, + lineHeight = 59.sp, + ), + headlineMedium = + TextStyle( + fontFamily = Montserrat, + fontSize = 30.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 37.sp, + ), + headlineSmall = + TextStyle( + fontFamily = Montserrat, + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 29.sp, + ), + titleLarge = + TextStyle( + fontFamily = Montserrat, + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 24.sp, + ), + titleMedium = + TextStyle( + fontFamily = Montserrat, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + ), + titleSmall = + TextStyle( + fontFamily = Karla, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + lineHeight = 24.sp, + letterSpacing = 0.1.sp, + ), + bodyLarge = + TextStyle( + fontFamily = Karla, + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + lineHeight = 28.sp, + letterSpacing = 0.15.sp, + ), + bodyMedium = + TextStyle( + fontFamily = Montserrat, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + labelLarge = + TextStyle( + fontFamily = Montserrat, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 16.sp, + letterSpacing = 1.25.sp, + ), + bodySmall = + TextStyle( + fontFamily = Karla, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + ), + labelSmall = + TextStyle( + fontFamily = Montserrat, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 16.sp, + letterSpacing = 1.sp, + ), ) -) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/Currency.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/Currency.kt index 10c243434d..da1927b724 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/Currency.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/Currency.kt @@ -19,8 +19,7 @@ package com.example.jetsnack.ui.utils import java.math.BigDecimal import java.text.NumberFormat -fun formatPrice(price: Long): String { - return NumberFormat.getCurrencyInstance().format( - BigDecimal(price).movePointLeft(2) +fun formatPrice(price: Long): String = + NumberFormat.getCurrencyInstance().format( + BigDecimal(price).movePointLeft(2), ) -} diff --git a/Jetsnack/build.gradle.kts b/Jetsnack/build.gradle.kts index 08ccea3e70..3d6825da11 100644 --- a/Jetsnack/build.gradle.kts +++ b/Jetsnack/build.gradle.kts @@ -1,3 +1,5 @@ +import com.diffplug.gradle.spotless.SpotlessExtension + /* * Copyright 2020 The Android Open Source Project * @@ -21,6 +23,30 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.compose) apply false + alias(libs.plugins.spotless) } apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") + +subprojects { + apply { + plugin(rootProject.libs.plugins.spotless.get().pluginId) + } + configure { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt") + ktlint(libs.versions.ktlint.get()) + .setEditorConfigPath("${project.rootDir}/.editorconfig") + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } + // Additional configuration for Kotlin Gradle scripts + kotlinGradle { + target("**/*.kts") + targetExclude("**/build/**/*.kts") + ktlint(libs.versions.ktlint.get()) // Apply ktlint to Gradle Kotlin scripts + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") + } + } +} \ No newline at end of file diff --git a/Jetsnack/gradle/libs.versions.toml b/Jetsnack/gradle/libs.versions.toml index 2c34e54e54..016f154a0d 100644 --- a/Jetsnack/gradle/libs.versions.toml +++ b/Jetsnack/gradle/libs.versions.toml @@ -47,6 +47,7 @@ junit = "4.13.2" kotlin = "2.0.0" kotlinx_immutable = "0.3.7" ksp = "2.0.0-1.0.21" +ktlint = "1.3.1" maps-compose = "3.1.1" # @keep minSdk = "21" @@ -57,6 +58,7 @@ roborazzi = "1.12.0" rome = "1.18.0" room = "2.6.0" secrets = "2.0.1" +spotless = "6.25.0" # @keep targetSdk = "33" version-catalog-update = "0.8.4" @@ -179,4 +181,5 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } diff --git a/Reply/.editorconfig b/Reply/.editorconfig new file mode 100644 index 0000000000..43f0af8237 --- /dev/null +++ b/Reply/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{kt,kts}] +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL + +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ktlint_standard_property-naming = disabled +ktlint_function_naming_ignore_when_annotated_with = Composable diff --git a/Reply/app/build.gradle.kts b/Reply/app/build.gradle.kts index e9ca09a541..6c48e79a95 100644 --- a/Reply/app/build.gradle.kts +++ b/Reply/app/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ * limitations under the License. */ + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -21,13 +22,22 @@ plugins { } android { - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() namespace = "com.example.reply" defaultConfig { applicationId = "com.example.reply" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() + targetSdk = + libs.versions.targetSdk + .get() + .toInt() versionCode = 1 versionName = "1.0" vectorDrawables.useSupportLibrary = true @@ -50,14 +60,15 @@ android { buildTypes { getByName("debug") { - } getByName("release") { isMinifyEnabled = true signingConfig = signingConfigs.getByName("release") - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) } } diff --git a/Reply/app/src/main/java/com/example/reply/data/Account.kt b/Reply/app/src/main/java/com/example/reply/data/Account.kt index a9b742277e..60b1cb9aab 100644 --- a/Reply/app/src/main/java/com/example/reply/data/Account.kt +++ b/Reply/app/src/main/java/com/example/reply/data/Account.kt @@ -30,7 +30,7 @@ data class Account( val email: String, val altEmail: String, @DrawableRes val avatar: Int, - var isCurrentAccount: Boolean = false + var isCurrentAccount: Boolean = false, ) { val fullName: String = "$firstName $lastName" } diff --git a/Reply/app/src/main/java/com/example/reply/data/AccountsRepository.kt b/Reply/app/src/main/java/com/example/reply/data/AccountsRepository.kt index 6cd255f4a2..34713231ef 100644 --- a/Reply/app/src/main/java/com/example/reply/data/AccountsRepository.kt +++ b/Reply/app/src/main/java/com/example/reply/data/AccountsRepository.kt @@ -23,6 +23,8 @@ import kotlinx.coroutines.flow.Flow */ interface AccountsRepository { fun getDefaultUserAccount(): Flow + fun getAllUserAccounts(): Flow> + fun getContactAccountByUid(uid: Long): Flow } diff --git a/Reply/app/src/main/java/com/example/reply/data/AccountsRepositoryImpl.kt b/Reply/app/src/main/java/com/example/reply/data/AccountsRepositoryImpl.kt index 577f6f765d..4809078714 100644 --- a/Reply/app/src/main/java/com/example/reply/data/AccountsRepositoryImpl.kt +++ b/Reply/app/src/main/java/com/example/reply/data/AccountsRepositoryImpl.kt @@ -21,16 +21,18 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class AccountsRepositoryImpl : AccountsRepository { + override fun getDefaultUserAccount(): Flow = + flow { + emit(LocalAccountsDataProvider.getDefaultUserAccount()) + } - override fun getDefaultUserAccount(): Flow = flow { - emit(LocalAccountsDataProvider.getDefaultUserAccount()) - } + override fun getAllUserAccounts(): Flow> = + flow { + emit(LocalAccountsDataProvider.allUserAccounts) + } - override fun getAllUserAccounts(): Flow> = flow { - emit(LocalAccountsDataProvider.allUserAccounts) - } - - override fun getContactAccountByUid(uid: Long): Flow = flow { - emit(LocalAccountsDataProvider.getContactAccountByUid(uid)) - } + override fun getContactAccountByUid(uid: Long): Flow = + flow { + emit(LocalAccountsDataProvider.getContactAccountByUid(uid)) + } } diff --git a/Reply/app/src/main/java/com/example/reply/data/Email.kt b/Reply/app/src/main/java/com/example/reply/data/Email.kt index f1e6f3ee49..8a1ec24276 100644 --- a/Reply/app/src/main/java/com/example/reply/data/Email.kt +++ b/Reply/app/src/main/java/com/example/reply/data/Email.kt @@ -30,5 +30,5 @@ data class Email( var isStarred: Boolean = false, var mailbox: MailboxType = MailboxType.INBOX, val createdAt: String, - val threads: List = emptyList() + val threads: List = emptyList(), ) diff --git a/Reply/app/src/main/java/com/example/reply/data/EmailAttachment.kt b/Reply/app/src/main/java/com/example/reply/data/EmailAttachment.kt index d0f6f89d45..9745371665 100644 --- a/Reply/app/src/main/java/com/example/reply/data/EmailAttachment.kt +++ b/Reply/app/src/main/java/com/example/reply/data/EmailAttachment.kt @@ -23,5 +23,5 @@ import androidx.annotation.DrawableRes */ data class EmailAttachment( @DrawableRes val resId: Int, - val contentDesc: String + val contentDesc: String, ) diff --git a/Reply/app/src/main/java/com/example/reply/data/EmailsRepository.kt b/Reply/app/src/main/java/com/example/reply/data/EmailsRepository.kt index 9b2684a33e..a78d24cece 100644 --- a/Reply/app/src/main/java/com/example/reply/data/EmailsRepository.kt +++ b/Reply/app/src/main/java/com/example/reply/data/EmailsRepository.kt @@ -23,7 +23,10 @@ import kotlinx.coroutines.flow.Flow */ interface EmailsRepository { fun getAllEmails(): Flow> + fun getCategoryEmails(category: MailboxType): Flow> + fun getAllFolders(): List + fun getEmailFromId(id: Long): Flow } diff --git a/Reply/app/src/main/java/com/example/reply/data/EmailsRepositoryImpl.kt b/Reply/app/src/main/java/com/example/reply/data/EmailsRepositoryImpl.kt index 58b118ff4e..3b2207ec39 100644 --- a/Reply/app/src/main/java/com/example/reply/data/EmailsRepositoryImpl.kt +++ b/Reply/app/src/main/java/com/example/reply/data/EmailsRepositoryImpl.kt @@ -21,21 +21,21 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class EmailsRepositoryImpl : EmailsRepository { + override fun getAllEmails(): Flow> = + flow { + emit(LocalEmailsDataProvider.allEmails) + } - override fun getAllEmails(): Flow> = flow { - emit(LocalEmailsDataProvider.allEmails) - } + override fun getCategoryEmails(category: MailboxType): Flow> = + flow { + val categoryEmails = LocalEmailsDataProvider.allEmails.filter { it.mailbox == category } + emit(categoryEmails) + } - override fun getCategoryEmails(category: MailboxType): Flow> = flow { - val categoryEmails = LocalEmailsDataProvider.allEmails.filter { it.mailbox == category } - emit(categoryEmails) - } + override fun getAllFolders(): List = LocalEmailsDataProvider.getAllFolders() - override fun getAllFolders(): List { - return LocalEmailsDataProvider.getAllFolders() - } - - override fun getEmailFromId(id: Long): Flow = flow { - val categoryEmails = LocalEmailsDataProvider.allEmails.firstOrNull { it.id == id } - } + override fun getEmailFromId(id: Long): Flow = + flow { + val categoryEmails = LocalEmailsDataProvider.allEmails.firstOrNull { it.id == id } + } } diff --git a/Reply/app/src/main/java/com/example/reply/data/MailboxType.kt b/Reply/app/src/main/java/com/example/reply/data/MailboxType.kt index a5a275e6e8..9c711b9859 100644 --- a/Reply/app/src/main/java/com/example/reply/data/MailboxType.kt +++ b/Reply/app/src/main/java/com/example/reply/data/MailboxType.kt @@ -20,5 +20,9 @@ package com.example.reply.data * An enum class to define different types of email folders or categories. */ enum class MailboxType { - INBOX, DRAFTS, SENT, SPAM, TRASH + INBOX, + DRAFTS, + SENT, + SPAM, + TRASH, } diff --git a/Reply/app/src/main/java/com/example/reply/data/local/LocalAccountsDataProvider.kt b/Reply/app/src/main/java/com/example/reply/data/local/LocalAccountsDataProvider.kt index 63b9569c39..7a3d797f9d 100644 --- a/Reply/app/src/main/java/com/example/reply/data/local/LocalAccountsDataProvider.kt +++ b/Reply/app/src/main/java/com/example/reply/data/local/LocalAccountsDataProvider.kt @@ -24,130 +24,131 @@ import com.example.reply.data.Account * all [Account]s of the current user's contacts. */ object LocalAccountsDataProvider { - - val allUserAccounts = listOf( - Account( - id = 1L, - uid = 0L, - firstName = "Jeff", - lastName = "Hansen", - email = "hikingfan@gmail.com", - altEmail = "hkngfan@outside.com", - avatar = R.drawable.avatar_10, - isCurrentAccount = true - ), - Account( - id = 2L, - uid = 0L, - firstName = "Jeff", - lastName = "H", - email = "jeffersonloveshiking@gmail.com", - altEmail = "jeffersonloveshiking@work.com", - avatar = R.drawable.avatar_2 - ), - Account( - id = 3L, - uid = 0L, - firstName = "Jeff", - lastName = "Hansen", - email = "jeffersonc@google.com", - altEmail = "jeffersonc@gmail.com", - avatar = R.drawable.avatar_9 + val allUserAccounts = + listOf( + Account( + id = 1L, + uid = 0L, + firstName = "Jeff", + lastName = "Hansen", + email = "hikingfan@gmail.com", + altEmail = "hkngfan@outside.com", + avatar = R.drawable.avatar_10, + isCurrentAccount = true, + ), + Account( + id = 2L, + uid = 0L, + firstName = "Jeff", + lastName = "H", + email = "jeffersonloveshiking@gmail.com", + altEmail = "jeffersonloveshiking@work.com", + avatar = R.drawable.avatar_2, + ), + Account( + id = 3L, + uid = 0L, + firstName = "Jeff", + lastName = "Hansen", + email = "jeffersonc@google.com", + altEmail = "jeffersonc@gmail.com", + avatar = R.drawable.avatar_9, + ), ) - ) - private val allUserContactAccounts = listOf( - Account( - id = 4L, - uid = 1L, - firstName = "Tracy", - lastName = "Alvarez", - email = "tracealvie@gmail.com", - altEmail = "tracealvie@gravity.com", - avatar = R.drawable.avatar_1 - ), - Account( - id = 5L, - uid = 2L, - firstName = "Allison", - lastName = "Trabucco", - email = "atrabucco222@gmail.com", - altEmail = "atrabucco222@work.com", - avatar = R.drawable.avatar_3 - ), - Account( - id = 6L, - uid = 3L, - firstName = "Ali", - lastName = "Connors", - email = "aliconnors@gmail.com", - altEmail = "aliconnors@android.com", - avatar = R.drawable.avatar_5 - ), - Account( - id = 7L, - uid = 4L, - firstName = "Alberto", - lastName = "Williams", - email = "albertowilliams124@gmail.com", - altEmail = "albertowilliams124@chromeos.com", - avatar = R.drawable.avatar_0 - ), - Account( - id = 8L, - uid = 5L, - firstName = "Kim", - lastName = "Alen", - email = "alen13@gmail.com", - altEmail = "alen13@mountainview.gov", - avatar = R.drawable.avatar_7 - ), - Account( - id = 9L, - uid = 6L, - firstName = "Google", - lastName = "Express", - email = "express@google.com", - altEmail = "express@gmail.com", - avatar = R.drawable.avatar_express - ), - Account( - id = 10L, - uid = 7L, - firstName = "Sandra", - lastName = "Adams", - email = "sandraadams@gmail.com", - altEmail = "sandraadams@textera.com", - avatar = R.drawable.avatar_2 - ), - Account( - id = 11L, - uid = 8L, - firstName = "Trevor", - lastName = "Hansen", - email = "trevorhandsen@gmail.com", - altEmail = "trevorhandsen@express.com", - avatar = R.drawable.avatar_8 - ), - Account( - id = 12L, - uid = 9L, - firstName = "Sean", - lastName = "Holt", - email = "sholt@gmail.com", - altEmail = "sholt@art.com", - avatar = R.drawable.avatar_6 - ), - Account( - id = 13L, - uid = 10L, - firstName = "Frank", - lastName = "Hawkins", - email = "fhawkank@gmail.com", - altEmail = "fhawkank@thisisme.com", - avatar = R.drawable.avatar_4 + private val allUserContactAccounts = + listOf( + Account( + id = 4L, + uid = 1L, + firstName = "Tracy", + lastName = "Alvarez", + email = "tracealvie@gmail.com", + altEmail = "tracealvie@gravity.com", + avatar = R.drawable.avatar_1, + ), + Account( + id = 5L, + uid = 2L, + firstName = "Allison", + lastName = "Trabucco", + email = "atrabucco222@gmail.com", + altEmail = "atrabucco222@work.com", + avatar = R.drawable.avatar_3, + ), + Account( + id = 6L, + uid = 3L, + firstName = "Ali", + lastName = "Connors", + email = "aliconnors@gmail.com", + altEmail = "aliconnors@android.com", + avatar = R.drawable.avatar_5, + ), + Account( + id = 7L, + uid = 4L, + firstName = "Alberto", + lastName = "Williams", + email = "albertowilliams124@gmail.com", + altEmail = "albertowilliams124@chromeos.com", + avatar = R.drawable.avatar_0, + ), + Account( + id = 8L, + uid = 5L, + firstName = "Kim", + lastName = "Alen", + email = "alen13@gmail.com", + altEmail = "alen13@mountainview.gov", + avatar = R.drawable.avatar_7, + ), + Account( + id = 9L, + uid = 6L, + firstName = "Google", + lastName = "Express", + email = "express@google.com", + altEmail = "express@gmail.com", + avatar = R.drawable.avatar_express, + ), + Account( + id = 10L, + uid = 7L, + firstName = "Sandra", + lastName = "Adams", + email = "sandraadams@gmail.com", + altEmail = "sandraadams@textera.com", + avatar = R.drawable.avatar_2, + ), + Account( + id = 11L, + uid = 8L, + firstName = "Trevor", + lastName = "Hansen", + email = "trevorhandsen@gmail.com", + altEmail = "trevorhandsen@express.com", + avatar = R.drawable.avatar_8, + ), + Account( + id = 12L, + uid = 9L, + firstName = "Sean", + lastName = "Holt", + email = "sholt@gmail.com", + altEmail = "sholt@art.com", + avatar = R.drawable.avatar_6, + ), + Account( + id = 13L, + uid = 10L, + firstName = "Frank", + lastName = "Hawkins", + email = "fhawkank@gmail.com", + altEmail = "fhawkank@thisisme.com", + avatar = R.drawable.avatar_4, + ), ) - ) /** * Get the current user's default account. @@ -162,7 +163,5 @@ object LocalAccountsDataProvider { /** * Get the contact of the current user with the given [accountId]. */ - fun getContactAccountByUid(accountId: Long): Account { - return allUserContactAccounts.first { it.id == accountId } - } + fun getContactAccountByUid(accountId: Long): Account = allUserContactAccounts.first { it.id == accountId } } diff --git a/Reply/app/src/main/java/com/example/reply/data/local/LocalEmailsDataProvider.kt b/Reply/app/src/main/java/com/example/reply/data/local/LocalEmailsDataProvider.kt index a8b7c6750a..c806501eba 100644 --- a/Reply/app/src/main/java/com/example/reply/data/local/LocalEmailsDataProvider.kt +++ b/Reply/app/src/main/java/com/example/reply/data/local/LocalEmailsDataProvider.kt @@ -26,283 +26,293 @@ import com.example.reply.data.MailboxType */ object LocalEmailsDataProvider { + private val threads = + listOf( + Email( + id = 8L, + sender = LocalAccountsDataProvider.getContactAccountByUid(13L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Your update on Google Play Store is live!", + body = + """ + Your update, 0.1.1, is now live on the Play Store and available for your alpha users to start testing. + + Your alpha testers will be automatically notified. If you'd rather send them a link directly, go to your Google Play Console and follow the instructions for obtaining an open alpha testing link. + """.trimIndent(), + mailbox = MailboxType.TRASH, + createdAt = "3 hours ago", + ), + Email( + id = 5L, + sender = LocalAccountsDataProvider.getContactAccountByUid(13L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Update to Your Itinerary", + body = "", + createdAt = "2 hours ago", + ), + Email( + id = 6L, + sender = LocalAccountsDataProvider.getContactAccountByUid(10L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Recipe to try", + "Raspberry Pie: We should make this pie recipe tonight! The filling is " + + "very quick to put together.", + createdAt = "2 hours ago", + mailbox = MailboxType.SENT, + ), + Email( + id = 7L, + sender = LocalAccountsDataProvider.getContactAccountByUid(9L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Delivered", + body = "Your shoes should be waiting for you at home!", + createdAt = "2 hours ago", + ), + Email( + id = 9L, + sender = LocalAccountsDataProvider.getContactAccountByUid(10L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "(No subject)", + body = + """ + Hey, + + Wanted to email and see what you thought of + """.trimIndent(), + createdAt = "3 hours ago", + mailbox = MailboxType.DRAFTS, + ), + Email( + id = 1L, + sender = LocalAccountsDataProvider.getContactAccountByUid(6L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Brunch this weekend?", + body = + """ + I'll be in your neighborhood doing errands and was hoping to catch you for a coffee this Saturday. If you don't have anything scheduled, it would be great to see you! It feels like its been forever. - private val threads = listOf( - Email( - id = 8L, - sender = LocalAccountsDataProvider.getContactAccountByUid(13L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Your update on Google Play Store is live!", - body = """ - Your update, 0.1.1, is now live on the Play Store and available for your alpha users to start testing. - - Your alpha testers will be automatically notified. If you'd rather send them a link directly, go to your Google Play Console and follow the instructions for obtaining an open alpha testing link. - """.trimIndent(), - mailbox = MailboxType.TRASH, - createdAt = "3 hours ago", - ), - Email( - id = 5L, - sender = LocalAccountsDataProvider.getContactAccountByUid(13L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Update to Your Itinerary", - body = "", - createdAt = "2 hours ago", - ), - Email( - id = 6L, - sender = LocalAccountsDataProvider.getContactAccountByUid(10L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Recipe to try", - "Raspberry Pie: We should make this pie recipe tonight! The filling is " + - "very quick to put together.", - createdAt = "2 hours ago", - mailbox = MailboxType.SENT - ), - Email( - id = 7L, - sender = LocalAccountsDataProvider.getContactAccountByUid(9L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Delivered", - body = "Your shoes should be waiting for you at home!", - createdAt = "2 hours ago", - ), - Email( - id = 9L, - sender = LocalAccountsDataProvider.getContactAccountByUid(10L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "(No subject)", - body = """ - Hey, - - Wanted to email and see what you thought of - """.trimIndent(), - createdAt = "3 hours ago", - mailbox = MailboxType.DRAFTS - ), - Email( - id = 1L, - sender = LocalAccountsDataProvider.getContactAccountByUid(6L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Brunch this weekend?", - body = """ - I'll be in your neighborhood doing errands and was hoping to catch you for a coffee this Saturday. If you don't have anything scheduled, it would be great to see you! It feels like its been forever. - - If we do get a chance to get together, remind me to tell you about Kim. She stopped over at the house to say hey to the kids and told me all about her trip to Mexico. + If we do get a chance to get together, remind me to tell you about Kim. She stopped over at the house to say hey to the kids and told me all about her trip to Mexico. - Talk to you soon, + Talk to you soon, - Ali - """.trimIndent(), - createdAt = "40 mins ago", - ), - Email( - id = 2L, - sender = LocalAccountsDataProvider.getContactAccountByUid(5L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Bonjour from Paris", - body = "Here are some great shots from my trip...", - attachments = listOf( - EmailAttachment(R.drawable.paris_1, "Bridge in Paris"), - EmailAttachment(R.drawable.paris_2, "Bridge in Paris at night"), - EmailAttachment(R.drawable.paris_3, "City street in Paris"), - EmailAttachment(R.drawable.paris_4, "Street with bike in Paris") + Ali + """.trimIndent(), + createdAt = "40 mins ago", + ), + Email( + id = 2L, + sender = LocalAccountsDataProvider.getContactAccountByUid(5L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Bonjour from Paris", + body = "Here are some great shots from my trip...", + attachments = + listOf( + EmailAttachment(R.drawable.paris_1, "Bridge in Paris"), + EmailAttachment(R.drawable.paris_2, "Bridge in Paris at night"), + EmailAttachment(R.drawable.paris_3, "City street in Paris"), + EmailAttachment(R.drawable.paris_4, "Street with bike in Paris"), + ), + isImportant = true, + createdAt = "1 hour ago", ), - isImportant = true, - createdAt = "1 hour ago", - ), - ) + ) - val allEmails = listOf( - Email( - id = 0L, - sender = LocalAccountsDataProvider.getContactAccountByUid(9L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Package shipped!", - body = """ - Cucumber Mask Facial has shipped. + val allEmails = + listOf( + Email( + id = 0L, + sender = LocalAccountsDataProvider.getContactAccountByUid(9L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Package shipped!", + body = + """ + Cucumber Mask Facial has shipped. - Keep an eye out for a package to arrive between this Thursday and next Tuesday. If for any reason you don't receive your package before the end of next week, please reach out to us for details on your shipment. + Keep an eye out for a package to arrive between this Thursday and next Tuesday. If for any reason you don't receive your package before the end of next week, please reach out to us for details on your shipment. - As always, thank you for shopping with us and we hope you love our specially formulated Cucumber Mask! - """.trimIndent(), - createdAt = "20 mins ago", - isStarred = true, - threads = threads, - ), - Email( - id = 1L, - sender = LocalAccountsDataProvider.getContactAccountByUid(6L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Brunch this weekend?", - body = """ - I'll be in your neighborhood doing errands and was hoping to catch you for a coffee this Saturday. If you don't have anything scheduled, it would be great to see you! It feels like its been forever. + As always, thank you for shopping with us and we hope you love our specially formulated Cucumber Mask! + """.trimIndent(), + createdAt = "20 mins ago", + isStarred = true, + threads = threads, + ), + Email( + id = 1L, + sender = LocalAccountsDataProvider.getContactAccountByUid(6L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Brunch this weekend?", + body = + """ + I'll be in your neighborhood doing errands and was hoping to catch you for a coffee this Saturday. If you don't have anything scheduled, it would be great to see you! It feels like its been forever. - If we do get a chance to get together, remind me to tell you about Kim. She stopped over at the house to say hey to the kids and told me all about her trip to Mexico. + If we do get a chance to get together, remind me to tell you about Kim. She stopped over at the house to say hey to the kids and told me all about her trip to Mexico. - Talk to you soon, + Talk to you soon, - Ali - """.trimIndent(), - createdAt = "40 mins ago", - threads = threads.shuffled(), - ), - Email( - 2L, - LocalAccountsDataProvider.getContactAccountByUid(5L), - listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - "Bonjour from Paris", - "Here are some great shots from my trip...", - listOf( - EmailAttachment(R.drawable.paris_1, "Bridge in Paris"), - EmailAttachment(R.drawable.paris_2, "Bridge in Paris at night"), - EmailAttachment(R.drawable.paris_3, "City street in Paris"), - EmailAttachment(R.drawable.paris_4, "Street with bike in Paris") + Ali + """.trimIndent(), + createdAt = "40 mins ago", + threads = threads.shuffled(), ), - true, - createdAt = "1 hour ago", - threads = threads.shuffled(), - ), - Email( - 3L, - LocalAccountsDataProvider.getContactAccountByUid(8L), - listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - "High school reunion?", - """ + Email( + 2L, + LocalAccountsDataProvider.getContactAccountByUid(5L), + listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + "Bonjour from Paris", + "Here are some great shots from my trip...", + listOf( + EmailAttachment(R.drawable.paris_1, "Bridge in Paris"), + EmailAttachment(R.drawable.paris_2, "Bridge in Paris at night"), + EmailAttachment(R.drawable.paris_3, "City street in Paris"), + EmailAttachment(R.drawable.paris_4, "Street with bike in Paris"), + ), + true, + createdAt = "1 hour ago", + threads = threads.shuffled(), + ), + Email( + 3L, + LocalAccountsDataProvider.getContactAccountByUid(8L), + listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + "High school reunion?", + """ Hi friends, I was at the grocery store on Sunday night.. when I ran into Genie Williams! I almost didn't recognize her afer 20 years! Anyway, it turns out she is on the organizing committee for the high school reunion this fall. I don't know if you were planning on going or not, but she could definitely use our help in trying to track down lots of missing alums. If you can make it, we're doing a little phone-tree party at her place next Saturday, hoping that if we can find one person, thee more will... - """.trimIndent(), - createdAt = "2 hours ago", - mailbox = MailboxType.SENT, - threads = threads.shuffled(), - ), - Email( - id = 4L, - sender = LocalAccountsDataProvider.getContactAccountByUid(11L), - recipients = listOf( - LocalAccountsDataProvider.getDefaultUserAccount(), - LocalAccountsDataProvider.getContactAccountByUid(8L), - LocalAccountsDataProvider.getContactAccountByUid(5L) + """.trimIndent(), + createdAt = "2 hours ago", + mailbox = MailboxType.SENT, + threads = threads.shuffled(), ), - subject = "Brazil trip", - body = """ - Thought we might be able to go over some details about our upcoming vacation. + Email( + id = 4L, + sender = LocalAccountsDataProvider.getContactAccountByUid(11L), + recipients = + listOf( + LocalAccountsDataProvider.getDefaultUserAccount(), + LocalAccountsDataProvider.getContactAccountByUid(8L), + LocalAccountsDataProvider.getContactAccountByUid(5L), + ), + subject = "Brazil trip", + body = + """ + Thought we might be able to go over some details about our upcoming vacation. - I've been doing a bit of research and have come across a few paces in Northern Brazil that I think we should check out. One, the north has some of the most predictable wind on the planet. I'd love to get out on the ocean and kitesurf for a couple of days if we're going to be anywhere near or around Taiba. I hear it's beautiful there and if you're up for it, I'd love to go. Other than that, I haven't spent too much time looking into places along our road trip route. I'm assuming we can find places to stay and things to do as we drive and find places we think look interesting. But... I know you're more of a planner, so if you have ideas or places in mind, lets jot some ideas down! + I've been doing a bit of research and have come across a few paces in Northern Brazil that I think we should check out. One, the north has some of the most predictable wind on the planet. I'd love to get out on the ocean and kitesurf for a couple of days if we're going to be anywhere near or around Taiba. I hear it's beautiful there and if you're up for it, I'd love to go. Other than that, I haven't spent too much time looking into places along our road trip route. I'm assuming we can find places to stay and things to do as we drive and find places we think look interesting. But... I know you're more of a planner, so if you have ideas or places in mind, lets jot some ideas down! - Maybe we can jump on the phone later today if you have a second. - """.trimIndent(), - createdAt = "2 hours ago", - isStarred = true, - threads = threads.shuffled(), - ), - Email( - id = 5L, - sender = LocalAccountsDataProvider.getContactAccountByUid(13L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Update to Your Itinerary", - body = "", - createdAt = "2 hours ago", - threads = threads.shuffled() - ), - Email( - id = 6L, - sender = LocalAccountsDataProvider.getContactAccountByUid(10L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Recipe to try", - "Raspberry Pie: We should make this pie recipe tonight! The filling is " + - "very quick to put together.", - createdAt = "2 hours ago", - mailbox = MailboxType.SENT, - threads = threads.shuffled() - ), - Email( - id = 7L, - sender = LocalAccountsDataProvider.getContactAccountByUid(9L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Delivered", - body = "Your shoes should be waiting for you at home!", - createdAt = "2 hours ago", - threads = threads.shuffled() - ), - Email( - id = 8L, - sender = LocalAccountsDataProvider.getContactAccountByUid(13L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Your update on Google Play Store is live!", - body = """ - Your update, 0.1.1, is now live on the Play Store and available for your alpha users to start testing. - - Your alpha testers will be automatically notified. If you'd rather send them a link directly, go to your Google Play Console and follow the instructions for obtaining an open alpha testing link. - """.trimIndent(), - mailbox = MailboxType.TRASH, - createdAt = "3 hours ago", - threads = threads.shuffled(), - ), - Email( - id = 9L, - sender = LocalAccountsDataProvider.getContactAccountByUid(10L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "(No subject)", - body = """ - Hey, - - Wanted to email and see what you thought of - """.trimIndent(), - createdAt = "3 hours ago", - mailbox = MailboxType.DRAFTS, - threads = threads.shuffled(), - ), - Email( - id = 10L, - sender = LocalAccountsDataProvider.getContactAccountByUid(5L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Try a free TrailGo account", - body = """ - Looking for the best hiking trails in your area? TrailGo gets you on the path to the outdoors faster than you can pack a sandwich. - - Whether you're an experienced hiker or just looking to get outside for the afternoon, there's a segment that suits you. - """.trimIndent(), - createdAt = "3 hours ago", - mailbox = MailboxType.TRASH, - threads = threads.shuffled(), - ), - Email( - id = 11L, - sender = LocalAccountsDataProvider.getContactAccountByUid(5L), - recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), - subject = "Free money", - body = """ - You've been selected as a winner in our latest raffle! To claim your prize, click on the link. - """.trimIndent(), - createdAt = "3 hours ago", - mailbox = MailboxType.SPAM, - threads = threads.shuffled(), + Maybe we can jump on the phone later today if you have a second. + """.trimIndent(), + createdAt = "2 hours ago", + isStarred = true, + threads = threads.shuffled(), + ), + Email( + id = 5L, + sender = LocalAccountsDataProvider.getContactAccountByUid(13L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Update to Your Itinerary", + body = "", + createdAt = "2 hours ago", + threads = threads.shuffled(), + ), + Email( + id = 6L, + sender = LocalAccountsDataProvider.getContactAccountByUid(10L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Recipe to try", + "Raspberry Pie: We should make this pie recipe tonight! The filling is " + + "very quick to put together.", + createdAt = "2 hours ago", + mailbox = MailboxType.SENT, + threads = threads.shuffled(), + ), + Email( + id = 7L, + sender = LocalAccountsDataProvider.getContactAccountByUid(9L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Delivered", + body = "Your shoes should be waiting for you at home!", + createdAt = "2 hours ago", + threads = threads.shuffled(), + ), + Email( + id = 8L, + sender = LocalAccountsDataProvider.getContactAccountByUid(13L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Your update on Google Play Store is live!", + body = + """ + Your update, 0.1.1, is now live on the Play Store and available for your alpha users to start testing. + + Your alpha testers will be automatically notified. If you'd rather send them a link directly, go to your Google Play Console and follow the instructions for obtaining an open alpha testing link. + """.trimIndent(), + mailbox = MailboxType.TRASH, + createdAt = "3 hours ago", + threads = threads.shuffled(), + ), + Email( + id = 9L, + sender = LocalAccountsDataProvider.getContactAccountByUid(10L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "(No subject)", + body = + """ + Hey, + + Wanted to email and see what you thought of + """.trimIndent(), + createdAt = "3 hours ago", + mailbox = MailboxType.DRAFTS, + threads = threads.shuffled(), + ), + Email( + id = 10L, + sender = LocalAccountsDataProvider.getContactAccountByUid(5L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Try a free TrailGo account", + body = + """ + Looking for the best hiking trails in your area? TrailGo gets you on the path to the outdoors faster than you can pack a sandwich. + + Whether you're an experienced hiker or just looking to get outside for the afternoon, there's a segment that suits you. + """.trimIndent(), + createdAt = "3 hours ago", + mailbox = MailboxType.TRASH, + threads = threads.shuffled(), + ), + Email( + id = 11L, + sender = LocalAccountsDataProvider.getContactAccountByUid(5L), + recipients = listOf(LocalAccountsDataProvider.getDefaultUserAccount()), + subject = "Free money", + body = + """ + You've been selected as a winner in our latest raffle! To claim your prize, click on the link. + """.trimIndent(), + createdAt = "3 hours ago", + mailbox = MailboxType.SPAM, + threads = threads.shuffled(), + ), ) - ) /** * Get an [Email] with the given [id]. */ - fun get(id: Long): Email? { - return allEmails.firstOrNull { it.id == id } - } + fun get(id: Long): Email? = allEmails.firstOrNull { it.id == id } /** * Create a new, blank [Email]. */ - fun create(): Email { - return Email( + fun create(): Email = + Email( System.nanoTime(), // Unique ID generation. LocalAccountsDataProvider.getDefaultUserAccount(), createdAt = "Just now", subject = "Monthly hosting party", - body = "I would like to invite everyone to our monthly event hosting party" + body = "I would like to invite everyone to our monthly event hosting party", ) - } /** * Create a new [Email] that is a reply to the email with the given [replyToId]. @@ -311,26 +321,28 @@ object LocalEmailsDataProvider { val replyTo = get(replyToId) ?: return create() return Email( id = System.nanoTime(), - sender = replyTo.recipients.firstOrNull() - ?: LocalAccountsDataProvider.getDefaultUserAccount(), + sender = + replyTo.recipients.firstOrNull() + ?: LocalAccountsDataProvider.getDefaultUserAccount(), recipients = listOf(replyTo.sender) + replyTo.recipients, subject = replyTo.subject, isStarred = replyTo.isStarred, isImportant = replyTo.isImportant, createdAt = "Just now", - body = "Responding to the above conversation." + body = "Responding to the above conversation.", ) } /** * Get a list of [EmailFolder]s by which [Email]s can be categorized. */ - fun getAllFolders() = listOf( - "Receipts", - "Pine Elementary", - "Taxes", - "Vacation", - "Mortgage", - "Grocery coupons" - ) + fun getAllFolders() = + listOf( + "Receipts", + "Pine Elementary", + "Taxes", + "Vacation", + "Mortgage", + "Grocery coupons", + ) } diff --git a/Reply/app/src/main/java/com/example/reply/ui/EmptyComingSoon.kt b/Reply/app/src/main/java/com/example/reply/ui/EmptyComingSoon.kt index a4f0d222ff..45739d1580 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/EmptyComingSoon.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/EmptyComingSoon.kt @@ -32,27 +32,25 @@ import androidx.compose.ui.unit.dp import com.example.reply.R @Composable -fun EmptyComingSoon( - modifier: Modifier = Modifier -) { +fun EmptyComingSoon(modifier: Modifier = Modifier) { Column( modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Text( modifier = Modifier.padding(8.dp), text = stringResource(id = R.string.empty_screen_title), style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, ) Text( modifier = Modifier.padding(horizontal = 8.dp), text = stringResource(id = R.string.empty_screen_subtitle), style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.outline + color = MaterialTheme.colorScheme.outline, ) } } diff --git a/Reply/app/src/main/java/com/example/reply/ui/MainActivity.kt b/Reply/app/src/main/java/com/example/reply/ui/MainActivity.kt index c3cd2e5709..421ecb1c9a 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/MainActivity.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/MainActivity.kt @@ -35,7 +35,6 @@ import com.example.reply.ui.theme.ContrastAwareReplyTheme import com.google.accompanist.adaptive.calculateDisplayFeatures class MainActivity : ComponentActivity() { - private val viewModel: ReplyHomeViewModel by viewModels() @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @@ -61,7 +60,7 @@ class MainActivity : ComponentActivity() { }, toggleSelectedEmail = { emailId -> viewModel.toggleSelectedEmail(emailId) - } + }, ) } } diff --git a/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt b/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt index 15398f821d..bf8eb48f4d 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt @@ -40,12 +40,13 @@ import com.example.reply.ui.utils.ReplyNavigationType import com.example.reply.ui.utils.isBookPosture import com.example.reply.ui.utils.isSeparating -private fun NavigationSuiteType.toReplyNavType() = when (this) { - NavigationSuiteType.NavigationBar -> ReplyNavigationType.BOTTOM_NAVIGATION - NavigationSuiteType.NavigationRail -> ReplyNavigationType.NAVIGATION_RAIL - NavigationSuiteType.NavigationDrawer -> ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER - else -> ReplyNavigationType.BOTTOM_NAVIGATION -} +private fun NavigationSuiteType.toReplyNavType() = + when (this) { + NavigationSuiteType.NavigationBar -> ReplyNavigationType.BOTTOM_NAVIGATION + NavigationSuiteType.NavigationRail -> ReplyNavigationType.NAVIGATION_RAIL + NavigationSuiteType.NavigationDrawer -> ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER + else -> ReplyNavigationType.BOTTOM_NAVIGATION + } @Composable fun ReplyApp( @@ -54,7 +55,7 @@ fun ReplyApp( replyHomeUIState: ReplyHomeUIState, closeDetailScreen: () -> Unit = {}, navigateToDetail: (Long, ReplyContentType) -> Unit = { _, _ -> }, - toggleSelectedEmail: (Long) -> Unit = { } + toggleSelectedEmail: (Long) -> Unit = { }, ) { /** * We are using display's folding features to map the device postures a fold is in. @@ -63,31 +64,35 @@ fun ReplyApp( */ val foldingFeature = displayFeatures.filterIsInstance().firstOrNull() - val foldingDevicePosture = when { - isBookPosture(foldingFeature) -> - DevicePosture.BookPosture(foldingFeature.bounds) + val foldingDevicePosture = + when { + isBookPosture(foldingFeature) -> + DevicePosture.BookPosture(foldingFeature.bounds) - isSeparating(foldingFeature) -> - DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation) + isSeparating(foldingFeature) -> + DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation) - else -> DevicePosture.NormalPosture - } + else -> DevicePosture.NormalPosture + } - val contentType = when (windowSize.widthSizeClass) { - WindowWidthSizeClass.Compact -> ReplyContentType.SINGLE_PANE - WindowWidthSizeClass.Medium -> if (foldingDevicePosture != DevicePosture.NormalPosture) { - ReplyContentType.DUAL_PANE - } else { - ReplyContentType.SINGLE_PANE + val contentType = + when (windowSize.widthSizeClass) { + WindowWidthSizeClass.Compact -> ReplyContentType.SINGLE_PANE + WindowWidthSizeClass.Medium -> + if (foldingDevicePosture != DevicePosture.NormalPosture) { + ReplyContentType.DUAL_PANE + } else { + ReplyContentType.SINGLE_PANE + } + WindowWidthSizeClass.Expanded -> ReplyContentType.DUAL_PANE + else -> ReplyContentType.SINGLE_PANE } - WindowWidthSizeClass.Expanded -> ReplyContentType.DUAL_PANE - else -> ReplyContentType.SINGLE_PANE - } val navController = rememberNavController() - val navigationActions = remember(navController) { - ReplyNavigationActions(navController) - } + val navigationActions = + remember(navController) { + ReplyNavigationActions(navController) + } val navBackStackEntry by navController.currentBackStackEntryAsState() val selectedDestination = navBackStackEntry?.destination?.route ?: ReplyRoute.INBOX @@ -95,7 +100,7 @@ fun ReplyApp( Surface { ReplyNavigationWrapper( selectedDestination = selectedDestination, - navigateToTopLevelDestination = navigationActions::navigateTo + navigateToTopLevelDestination = navigationActions::navigateTo, ) { ReplyNavHost( navController = navController, @@ -136,7 +141,7 @@ private fun ReplyNavHost( displayFeatures = displayFeatures, closeDetailScreen = closeDetailScreen, navigateToDetail = navigateToDetail, - toggleSelectedEmail = toggleSelectedEmail + toggleSelectedEmail = toggleSelectedEmail, ) } composable(ReplyRoute.DM) { diff --git a/Reply/app/src/main/java/com/example/reply/ui/ReplyHomeViewModel.kt b/Reply/app/src/main/java/com/example/reply/ui/ReplyHomeViewModel.kt index c470a60cf4..1930147fba 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/ReplyHomeViewModel.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/ReplyHomeViewModel.kt @@ -27,9 +27,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch -class ReplyHomeViewModel(private val emailsRepository: EmailsRepository = EmailsRepositoryImpl()) : - ViewModel() { - +class ReplyHomeViewModel( + private val emailsRepository: EmailsRepository = EmailsRepositoryImpl(), +) : ViewModel() { // UI state exposed to the UI private val _uiState = MutableStateFlow(ReplyHomeUIState(loading = true)) val uiState: StateFlow = _uiState @@ -40,47 +40,59 @@ class ReplyHomeViewModel(private val emailsRepository: EmailsRepository = Emails private fun observeEmails() { viewModelScope.launch { - emailsRepository.getAllEmails() + emailsRepository + .getAllEmails() .catch { ex -> _uiState.value = ReplyHomeUIState(error = ex.message) - } - .collect { emails -> + }.collect { emails -> /** * We set first email selected by default for first App launch in large-screens */ - _uiState.value = ReplyHomeUIState( - emails = emails, - openedEmail = emails.first() - ) + _uiState.value = + ReplyHomeUIState( + emails = emails, + openedEmail = emails.first(), + ) } } } - fun setOpenedEmail(emailId: Long, contentType: ReplyContentType) { + fun setOpenedEmail( + emailId: Long, + contentType: ReplyContentType, + ) { /** * We only set isDetailOnlyOpen to true when it's only single pane layout */ val email = uiState.value.emails.find { it.id == emailId } - _uiState.value = _uiState.value.copy( - openedEmail = email, - isDetailOnlyOpen = contentType == ReplyContentType.SINGLE_PANE - ) + _uiState.value = + _uiState.value.copy( + openedEmail = email, + isDetailOnlyOpen = contentType == ReplyContentType.SINGLE_PANE, + ) } fun toggleSelectedEmail(emailId: Long) { val currentSelection = uiState.value.selectedEmails - _uiState.value = _uiState.value.copy( - selectedEmails = if (currentSelection.contains(emailId)) - currentSelection.minus(emailId) else currentSelection.plus(emailId) - ) + _uiState.value = + _uiState.value.copy( + selectedEmails = + if (currentSelection.contains(emailId)) { + currentSelection.minus(emailId) + } else { + currentSelection.plus(emailId) + }, + ) } fun closeDetailScreen() { - _uiState.value = _uiState - .value.copy( - isDetailOnlyOpen = false, - openedEmail = _uiState.value.emails.first() - ) + _uiState.value = + _uiState + .value + .copy( + isDetailOnlyOpen = false, + openedEmail = _uiState.value.emails.first(), + ) } } @@ -90,5 +102,5 @@ data class ReplyHomeUIState( val openedEmail: Email? = null, val isDetailOnlyOpen: Boolean = false, val loading: Boolean = false, - val error: String? = null + val error: String? = null, ) diff --git a/Reply/app/src/main/java/com/example/reply/ui/ReplyListContent.kt b/Reply/app/src/main/java/com/example/reply/ui/ReplyListContent.kt index 56758184e0..c5998f32f0 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/ReplyListContent.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/ReplyListContent.kt @@ -65,7 +65,7 @@ fun ReplyInboxScreen( closeDetailScreen: () -> Unit, navigateToDetail: (Long, ReplyContentType) -> Unit, toggleSelectedEmail: (Long) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { /** * When moving from LIST_AND_DETAIL page to LIST page clear the selection and user should see LIST screen. @@ -89,17 +89,17 @@ fun ReplyInboxScreen( selectedEmailIds = replyHomeUIState.selectedEmails, toggleEmailSelection = toggleSelectedEmail, emailLazyListState = emailLazyListState, - navigateToDetail = navigateToDetail + navigateToDetail = navigateToDetail, ) }, second = { ReplyEmailDetail( email = replyHomeUIState.openedEmail ?: replyHomeUIState.emails.first(), - isFullScreen = false + isFullScreen = false, ) }, strategy = HorizontalTwoPaneStrategy(splitFraction = 0.5f, gapWidth = 16.dp), - displayFeatures = displayFeatures + displayFeatures = displayFeatures, ) } else { Box(modifier = modifier.fillMaxSize()) { @@ -109,22 +109,23 @@ fun ReplyInboxScreen( emailLazyListState = emailLazyListState, modifier = Modifier.fillMaxSize(), closeDetailScreen = closeDetailScreen, - navigateToDetail = navigateToDetail + navigateToDetail = navigateToDetail, ) // When we have bottom navigation we show FAB at the bottom end. if (navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) { LargeFloatingActionButton( onClick = { /*TODO*/ }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), + modifier = + Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, ) { Icon( imageVector = Icons.Default.Edit, contentDescription = stringResource(id = R.string.compose), - modifier = Modifier.size(28.dp) + modifier = Modifier.size(28.dp), ) } } @@ -139,7 +140,7 @@ fun ReplySinglePaneContent( emailLazyListState: LazyListState, modifier: Modifier = Modifier, closeDetailScreen: () -> Unit, - navigateToDetail: (Long, ReplyContentType) -> Unit + navigateToDetail: (Long, ReplyContentType) -> Unit, ) { if (replyHomeUIState.openedEmail != null && replyHomeUIState.isDetailOnlyOpen) { BackHandler { @@ -156,7 +157,7 @@ fun ReplySinglePaneContent( toggleEmailSelection = toggleEmailSelection, emailLazyListState = emailLazyListState, modifier = modifier, - navigateToDetail = navigateToDetail + navigateToDetail = navigateToDetail, ) } } @@ -169,7 +170,7 @@ fun ReplyEmailList( toggleEmailSelection: (Long) -> Unit, emailLazyListState: LazyListState, modifier: Modifier = Modifier, - navigateToDetail: (Long, ReplyContentType) -> Unit + navigateToDetail: (Long, ReplyContentType) -> Unit, ) { Box(modifier = modifier.windowInsetsPadding(WindowInsets.statusBars)) { ReplyDockedSearchBar( @@ -177,16 +178,18 @@ fun ReplyEmailList( onSearchItemSelected = { searchedEmail -> navigateToDetail(searchedEmail.id, ReplyContentType.SINGLE_PANE) }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), ) LazyColumn( - modifier = modifier - .fillMaxWidth() - .padding(top = 80.dp), - state = emailLazyListState + modifier = + modifier + .fillMaxWidth() + .padding(top = 80.dp), + state = emailLazyListState, ) { items(items = emails, key = { it.id }) { email -> ReplyEmailListItem( @@ -196,7 +199,7 @@ fun ReplyEmailList( }, toggleSelection = toggleEmailSelection, isOpened = openedEmail?.id == email.id, - isSelected = selectedEmailIds.contains(email.id) + isSelected = selectedEmailIds.contains(email.id), ) } // Add extra spacing at the bottom if @@ -212,11 +215,12 @@ fun ReplyEmailDetail( email: Email, modifier: Modifier = Modifier, isFullScreen: Boolean = true, - onBackPressed: () -> Unit = {} + onBackPressed: () -> Unit = {}, ) { LazyColumn( - modifier = modifier - .background(MaterialTheme.colorScheme.inverseOnSurface) + modifier = + modifier + .background(MaterialTheme.colorScheme.inverseOnSurface), ) { item { EmailDetailAppBar(email, isFullScreen) { diff --git a/Reply/app/src/main/java/com/example/reply/ui/components/ReplyAppBars.kt b/Reply/app/src/main/java/com/example/reply/ui/components/ReplyAppBars.kt index 6974a9c8d9..cf47a7c87e 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/components/ReplyAppBars.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/components/ReplyAppBars.kt @@ -60,7 +60,7 @@ import com.example.reply.data.Email fun ReplyDockedSearchBar( emails: List, onSearchItemSelected: (Email) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { var query by remember { mutableStateOf("") } var expanded by remember { mutableStateOf(false) } @@ -76,13 +76,14 @@ fun ReplyDockedSearchBar( emails.filter { it.subject.startsWith( prefix = query, - ignoreCase = true - ) || it.sender.fullName.startsWith( - prefix = - query, - ignoreCase = true - ) - } + ignoreCase = true, + ) || + it.sender.fullName.startsWith( + prefix = + query, + ignoreCase = true, + ) + }, ) } } @@ -104,12 +105,13 @@ fun ReplyDockedSearchBar( Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.back_button), - modifier = Modifier - .padding(start = 16.dp) - .clickable { - expanded = false - query = "" - }, + modifier = + Modifier + .padding(start = 16.dp) + .clickable { + expanded = false + query = "" + }, ) } else { Icon( @@ -123,9 +125,10 @@ fun ReplyDockedSearchBar( ReplyProfileImage( drawableResource = R.drawable.avatar_6, description = stringResource(id = R.string.profile), - modifier = Modifier - .padding(12.dp) - .size(32.dp) + modifier = + Modifier + .padding(12.dp) + .size(32.dp), ) }, ) @@ -138,7 +141,7 @@ fun ReplyDockedSearchBar( LazyColumn( modifier = Modifier.fillMaxWidth(), contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(4.dp), ) { items(items = searchResults, key = { it.id }) { email -> ListItem( @@ -148,29 +151,32 @@ fun ReplyDockedSearchBar( ReplyProfileImage( drawableResource = email.sender.avatar, description = stringResource(id = R.string.profile), - modifier = Modifier - .size(32.dp) + modifier = + Modifier + .size(32.dp), ) }, - modifier = Modifier.clickable { - onSearchItemSelected.invoke(email) - query = "" - expanded = false - } + modifier = + Modifier.clickable { + onSearchItemSelected.invoke(email) + query = "" + expanded = false + }, ) } } } else if (query.isNotEmpty()) { Text( text = stringResource(id = R.string.no_item_found), - modifier = Modifier.padding(16.dp) + modifier = Modifier.padding(16.dp), ) - } else + } else { Text( text = stringResource(id = R.string.no_search_history), - modifier = Modifier.padding(16.dp) + modifier = Modifier.padding(16.dp), ) - } + } + }, ) } @@ -180,29 +186,34 @@ fun EmailDetailAppBar( email: Email, isFullScreen: Boolean, modifier: Modifier = Modifier, - onBackPressed: () -> Unit + onBackPressed: () -> Unit, ) { TopAppBar( modifier = modifier, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.inverseOnSurface - ), + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.inverseOnSurface, + ), title = { Column( modifier = Modifier.fillMaxWidth(), - horizontalAlignment = if (isFullScreen) Alignment.CenterHorizontally - else Alignment.Start + horizontalAlignment = + if (isFullScreen) { + Alignment.CenterHorizontally + } else { + Alignment.Start + }, ) { Text( text = email.subject, style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( modifier = Modifier.padding(top = 4.dp), text = "${email.threads.size} ${stringResource(id = R.string.messages)}", style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.outline + color = MaterialTheme.colorScheme.outline, ) } }, @@ -211,15 +222,16 @@ fun EmailDetailAppBar( FilledIconButton( onClick = onBackPressed, modifier = Modifier.padding(8.dp), - colors = IconButtonDefaults.filledIconButtonColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ) + colors = + IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ), ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.back_button), - modifier = Modifier.size(14.dp) + modifier = Modifier.size(14.dp), ) } } @@ -231,9 +243,9 @@ fun EmailDetailAppBar( Icon( imageVector = Icons.Default.MoreVert, contentDescription = stringResource(id = R.string.more_options_button), - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - } + }, ) } diff --git a/Reply/app/src/main/java/com/example/reply/ui/components/ReplyEmailListItem.kt b/Reply/app/src/main/java/com/example/reply/ui/components/ReplyEmailListItem.kt index ba2c6299fe..e0125da88d 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/components/ReplyEmailListItem.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/components/ReplyEmailListItem.kt @@ -61,31 +61,39 @@ fun ReplyEmailListItem( isSelected: Boolean = false, ) { Card( - modifier = modifier - .padding(horizontal = 16.dp, vertical = 4.dp) - .semantics { selected = isSelected } - .clip(CardDefaults.shape) - .combinedClickable( - onClick = { navigateToDetail(email.id) }, - onLongClick = { toggleSelection(email.id) } - ) - .clip(CardDefaults.shape), - colors = CardDefaults.cardColors( - containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer - else if (isOpened) MaterialTheme.colorScheme.secondaryContainer - else MaterialTheme.colorScheme.surfaceVariant - ) + modifier = + modifier + .padding(horizontal = 16.dp, vertical = 4.dp) + .semantics { selected = isSelected } + .clip(CardDefaults.shape) + .combinedClickable( + onClick = { navigateToDetail(email.id) }, + onLongClick = { toggleSelection(email.id) }, + ).clip(CardDefaults.shape), + colors = + CardDefaults.cardColors( + containerColor = + if (isSelected) { + MaterialTheme.colorScheme.primaryContainer + } else if (isOpened) { + MaterialTheme.colorScheme.secondaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant + }, + ), ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(20.dp), ) { Row(modifier = Modifier.fillMaxWidth()) { - val clickModifier = Modifier.clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { toggleSelection(email.id) } + val clickModifier = + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { toggleSelection(email.id) } AnimatedContent(targetState = isSelected, label = "avatar") { selected -> if (selected) { SelectedProfileImage(clickModifier) @@ -93,20 +101,21 @@ fun ReplyEmailListItem( ReplyProfileImage( email.sender.avatar, email.sender.fullName, - clickModifier + clickModifier, ) } } Column( - modifier = Modifier - .weight(1f) - .padding(horizontal = 12.dp, vertical = 4.dp), - verticalArrangement = Arrangement.Center + modifier = + Modifier + .weight(1f) + .padding(horizontal = 12.dp, vertical = 4.dp), + verticalArrangement = Arrangement.Center, ) { Text( text = email.sender.firstName, - style = MaterialTheme.typography.labelMedium + style = MaterialTheme.typography.labelMedium, ) Text( text = email.createdAt, @@ -115,14 +124,15 @@ fun ReplyEmailListItem( } IconButton( onClick = { /*TODO*/ }, - modifier = Modifier - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) + modifier = + Modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), ) { Icon( imageVector = Icons.Default.StarBorder, contentDescription = "Favorite", - tint = MaterialTheme.colorScheme.outline + tint = MaterialTheme.colorScheme.outline, ) } } @@ -136,7 +146,7 @@ fun ReplyEmailListItem( text = email.body, style = MaterialTheme.typography.bodyMedium, maxLines = 2, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) } } @@ -148,15 +158,16 @@ fun SelectedProfileImage(modifier: Modifier = Modifier) { modifier .size(40.dp) .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary) + .background(MaterialTheme.colorScheme.primary), ) { Icon( Icons.Default.Check, contentDescription = null, - modifier = Modifier - .size(24.dp) - .align(Alignment.Center), - tint = MaterialTheme.colorScheme.onPrimary + modifier = + Modifier + .size(24.dp) + .align(Alignment.Center), + tint = MaterialTheme.colorScheme.onPrimary, ) } } diff --git a/Reply/app/src/main/java/com/example/reply/ui/components/ReplyEmailThreadItem.kt b/Reply/app/src/main/java/com/example/reply/ui/components/ReplyEmailThreadItem.kt index a3fbac594d..9d3d7b9420 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/components/ReplyEmailThreadItem.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/components/ReplyEmailThreadItem.kt @@ -44,18 +44,20 @@ import com.example.reply.data.Email @Composable fun ReplyEmailThreadItem( email: Email, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Card( modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh - ) + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ), ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(20.dp), ) { Row(modifier = Modifier.fillMaxWidth()) { ReplyProfileImage( @@ -63,31 +65,33 @@ fun ReplyEmailThreadItem( description = email.sender.fullName, ) Column( - modifier = Modifier - .weight(1f) - .padding(horizontal = 12.dp, vertical = 4.dp), - verticalArrangement = Arrangement.Center + modifier = + Modifier + .weight(1f) + .padding(horizontal = 12.dp, vertical = 4.dp), + verticalArrangement = Arrangement.Center, ) { Text( text = email.sender.firstName, - style = MaterialTheme.typography.labelMedium + style = MaterialTheme.typography.labelMedium, ) Text( text = "20 mins ago", style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.outline + color = MaterialTheme.colorScheme.outline, ) } IconButton( onClick = { /*TODO*/ }, - modifier = Modifier - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainer) + modifier = + Modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainer), ) { Icon( imageVector = Icons.Default.StarBorder, contentDescription = "Favorite", - tint = MaterialTheme.colorScheme.outline + tint = MaterialTheme.colorScheme.outline, ) } } @@ -105,33 +109,36 @@ fun ReplyEmailThreadItem( color = MaterialTheme.colorScheme.onSurfaceVariant, ) Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp, bottom = 8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(top = 20.dp, bottom = 8.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Button( onClick = { /*TODO*/ }, modifier = Modifier.weight(1f), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.surfaceBright - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceBright, + ), ) { Text( text = stringResource(id = R.string.reply), - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) } Button( onClick = { /*TODO*/ }, modifier = Modifier.weight(1f), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.surfaceBright - ) + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceBright, + ), ) { Text( text = stringResource(id = R.string.reply_all), - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) } } diff --git a/Reply/app/src/main/java/com/example/reply/ui/components/ReplyProfileImage.kt b/Reply/app/src/main/java/com/example/reply/ui/components/ReplyProfileImage.kt index de83690939..49f4fcb189 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/components/ReplyProfileImage.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/components/ReplyProfileImage.kt @@ -29,12 +29,13 @@ import androidx.compose.ui.unit.dp fun ReplyProfileImage( drawableResource: Int, description: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Image( - modifier = modifier - .size(40.dp) - .clip(CircleShape), + modifier = + modifier + .size(40.dp) + .clip(CircleShape), painter = painterResource(id = drawableResource), contentDescription = description, ) diff --git a/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationActions.kt b/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationActions.kt index 407f1ac407..e5006deec1 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationActions.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationActions.kt @@ -37,11 +37,12 @@ data class ReplyTopLevelDestination( val route: String, val selectedIcon: ImageVector, val unselectedIcon: ImageVector, - val iconTextId: Int + val iconTextId: Int, ) -class ReplyNavigationActions(private val navController: NavHostController) { - +class ReplyNavigationActions( + private val navController: NavHostController, +) { fun navigateTo(destination: ReplyTopLevelDestination) { navController.navigate(destination.route) { // Pop up to the start destination of the graph to @@ -59,30 +60,30 @@ class ReplyNavigationActions(private val navController: NavHostController) { } } -val TOP_LEVEL_DESTINATIONS = listOf( - ReplyTopLevelDestination( - route = ReplyRoute.INBOX, - selectedIcon = Icons.Default.Inbox, - unselectedIcon = Icons.Default.Inbox, - iconTextId = R.string.tab_inbox - ), - ReplyTopLevelDestination( - route = ReplyRoute.ARTICLES, - selectedIcon = Icons.AutoMirrored.Filled.Article, - unselectedIcon = Icons.AutoMirrored.Filled.Article, - iconTextId = R.string.tab_article - ), - ReplyTopLevelDestination( - route = ReplyRoute.DM, - selectedIcon = Icons.Outlined.ChatBubbleOutline, - unselectedIcon = Icons.Outlined.ChatBubbleOutline, - iconTextId = R.string.tab_inbox - ), - ReplyTopLevelDestination( - route = ReplyRoute.GROUPS, - selectedIcon = Icons.Default.People, - unselectedIcon = Icons.Default.People, - iconTextId = R.string.tab_article +val TOP_LEVEL_DESTINATIONS = + listOf( + ReplyTopLevelDestination( + route = ReplyRoute.INBOX, + selectedIcon = Icons.Default.Inbox, + unselectedIcon = Icons.Default.Inbox, + iconTextId = R.string.tab_inbox, + ), + ReplyTopLevelDestination( + route = ReplyRoute.ARTICLES, + selectedIcon = Icons.AutoMirrored.Filled.Article, + unselectedIcon = Icons.AutoMirrored.Filled.Article, + iconTextId = R.string.tab_article, + ), + ReplyTopLevelDestination( + route = ReplyRoute.DM, + selectedIcon = Icons.Outlined.ChatBubbleOutline, + unselectedIcon = Icons.Outlined.ChatBubbleOutline, + iconTextId = R.string.tab_inbox, + ), + ReplyTopLevelDestination( + route = ReplyRoute.GROUPS, + selectedIcon = Icons.Default.People, + unselectedIcon = Icons.Default.People, + iconTextId = R.string.tab_article, + ), ) - -) diff --git a/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationComponents.kt b/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationComponents.kt index ed87d31f1c..f95c49f9a4 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationComponents.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationComponents.kt @@ -82,33 +82,37 @@ private fun WindowSizeClass.isCompact() = windowHeightSizeClass == WindowHeightSizeClass.COMPACT class ReplyNavSuiteScope( - val navSuiteType: NavigationSuiteType + val navSuiteType: NavigationSuiteType, ) @Composable fun ReplyNavigationWrapper( selectedDestination: String, navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit, - content: @Composable ReplyNavSuiteScope.() -> Unit + content: @Composable ReplyNavSuiteScope.() -> Unit, ) { val adaptiveInfo = currentWindowAdaptiveInfo() - val windowSize = with(LocalDensity.current) { - currentWindowSize().toSize().toDpSize() - } + val windowSize = + with(LocalDensity.current) { + currentWindowSize().toSize().toDpSize() + } - val navLayoutType = when { - adaptiveInfo.windowPosture.isTabletop -> NavigationSuiteType.NavigationBar - adaptiveInfo.windowSizeClass.isCompact() -> NavigationSuiteType.NavigationBar - adaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED && - windowSize.width >= 1200.dp -> NavigationSuiteType.NavigationDrawer - else -> NavigationSuiteType.NavigationRail - } - val navContentPosition = when (adaptiveInfo.windowSizeClass.windowHeightSizeClass) { - WindowHeightSizeClass.COMPACT -> ReplyNavigationContentPosition.TOP - WindowHeightSizeClass.MEDIUM, - WindowHeightSizeClass.EXPANDED -> ReplyNavigationContentPosition.CENTER - else -> ReplyNavigationContentPosition.TOP - } + val navLayoutType = + when { + adaptiveInfo.windowPosture.isTabletop -> NavigationSuiteType.NavigationBar + adaptiveInfo.windowSizeClass.isCompact() -> NavigationSuiteType.NavigationBar + adaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED && + windowSize.width >= 1200.dp -> NavigationSuiteType.NavigationDrawer + else -> NavigationSuiteType.NavigationRail + } + val navContentPosition = + when (adaptiveInfo.windowSizeClass.windowHeightSizeClass) { + WindowHeightSizeClass.COMPACT -> ReplyNavigationContentPosition.TOP + WindowHeightSizeClass.MEDIUM, + WindowHeightSizeClass.EXPANDED, + -> ReplyNavigationContentPosition.CENTER + else -> ReplyNavigationContentPosition.TOP + } val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val coroutineScope = rememberCoroutineScope() @@ -135,7 +139,7 @@ fun ReplyNavigationWrapper( coroutineScope.launch { drawerState.close() } - } + }, ) }, ) { @@ -143,27 +147,30 @@ fun ReplyNavigationWrapper( layoutType = navLayoutType, navigationSuite = { when (navLayoutType) { - NavigationSuiteType.NavigationBar -> ReplyBottomNavigationBar( - selectedDestination = selectedDestination, - navigateToTopLevelDestination = navigateToTopLevelDestination - ) - NavigationSuiteType.NavigationRail -> ReplyNavigationRail( - selectedDestination = selectedDestination, - navigationContentPosition = navContentPosition, - navigateToTopLevelDestination = navigateToTopLevelDestination, - onDrawerClicked = { - coroutineScope.launch { - drawerState.open() - } - } - ) - NavigationSuiteType.NavigationDrawer -> PermanentNavigationDrawerContent( - selectedDestination = selectedDestination, - navigationContentPosition = navContentPosition, - navigateToTopLevelDestination = navigateToTopLevelDestination - ) + NavigationSuiteType.NavigationBar -> + ReplyBottomNavigationBar( + selectedDestination = selectedDestination, + navigateToTopLevelDestination = navigateToTopLevelDestination, + ) + NavigationSuiteType.NavigationRail -> + ReplyNavigationRail( + selectedDestination = selectedDestination, + navigationContentPosition = navContentPosition, + navigateToTopLevelDestination = navigateToTopLevelDestination, + onDrawerClicked = { + coroutineScope.launch { + drawerState.open() + } + }, + ) + NavigationSuiteType.NavigationDrawer -> + PermanentNavigationDrawerContent( + selectedDestination = selectedDestination, + navigationContentPosition = navContentPosition, + navigateToTopLevelDestination = navigateToTopLevelDestination, + ) } - } + }, ) { ReplyNavSuiteScope(navLayoutType).content() } @@ -179,12 +186,12 @@ fun ReplyNavigationRail( ) { NavigationRail( modifier = Modifier.fillMaxHeight(), - containerColor = MaterialTheme.colorScheme.inverseOnSurface + containerColor = MaterialTheme.colorScheme.inverseOnSurface, ) { Column( modifier = Modifier.layoutId(LayoutType.HEADER), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(4.dp), ) { NavigationRailItem( selected = false, @@ -192,20 +199,20 @@ fun ReplyNavigationRail( icon = { Icon( imageVector = Icons.Default.Menu, - contentDescription = stringResource(id = R.string.navigation_drawer) + contentDescription = stringResource(id = R.string.navigation_drawer), ) - } + }, ) FloatingActionButton( onClick = { /*TODO*/ }, modifier = Modifier.padding(top = 8.dp, bottom = 32.dp), containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, ) { Icon( imageVector = Icons.Default.Edit, contentDescription = stringResource(id = R.string.compose), - modifier = Modifier.size(18.dp) + modifier = Modifier.size(18.dp), ) } Spacer(Modifier.height(8.dp)) // NavigationRailHeaderPadding @@ -215,7 +222,7 @@ fun ReplyNavigationRail( Column( modifier = Modifier.layoutId(LayoutType.CONTENT), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(4.dp), ) { TOP_LEVEL_DESTINATIONS.forEach { replyDestination -> NavigationRailItem( @@ -224,11 +231,12 @@ fun ReplyNavigationRail( icon = { Icon( imageVector = replyDestination.selectedIcon, - contentDescription = stringResource( - id = replyDestination.iconTextId - ) + contentDescription = + stringResource( + id = replyDestination.iconTextId, + ), ) - } + }, ) } } @@ -238,7 +246,7 @@ fun ReplyNavigationRail( @Composable fun ReplyBottomNavigationBar( selectedDestination: String, - navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit + navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit, ) { NavigationBar(modifier = Modifier.fillMaxWidth()) { TOP_LEVEL_DESTINATIONS.forEach { replyDestination -> @@ -248,9 +256,9 @@ fun ReplyBottomNavigationBar( icon = { Icon( imageVector = replyDestination.selectedIcon, - contentDescription = stringResource(id = replyDestination.iconTextId) + contentDescription = stringResource(id = replyDestination.iconTextId), ) - } + }, ) } } @@ -268,47 +276,51 @@ fun PermanentNavigationDrawerContent( ) { // TODO remove custom nav drawer content positioning when NavDrawer component supports it. ticket : b/232495216 Layout( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .padding(16.dp), + modifier = + Modifier + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(16.dp), content = { Column( modifier = Modifier.layoutId(LayoutType.HEADER), horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( - modifier = Modifier - .padding(16.dp), + modifier = + Modifier + .padding(16.dp), text = stringResource(id = R.string.app_name).uppercase(), style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, ) ExtendedFloatingActionButton( onClick = { /*TODO*/ }, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 40.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 40.dp), containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, ) { Icon( imageVector = Icons.Default.Edit, contentDescription = stringResource(id = R.string.compose), - modifier = Modifier.size(24.dp) + modifier = Modifier.size(24.dp), ) Text( text = stringResource(id = R.string.compose), modifier = Modifier.weight(1f), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } Column( - modifier = Modifier - .layoutId(LayoutType.CONTENT) - .verticalScroll(rememberScrollState()), + modifier = + Modifier + .layoutId(LayoutType.CONTENT) + .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { TOP_LEVEL_DESTINATIONS.forEach { replyDestination -> @@ -317,26 +329,28 @@ fun PermanentNavigationDrawerContent( label = { Text( text = stringResource(id = replyDestination.iconTextId), - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier.padding(horizontal = 16.dp), ) }, icon = { Icon( imageVector = replyDestination.selectedIcon, - contentDescription = stringResource( - id = replyDestination.iconTextId - ) + contentDescription = + stringResource( + id = replyDestination.iconTextId, + ), ) }, - colors = NavigationDrawerItemDefaults.colors( - unselectedContainerColor = Color.Transparent - ), - onClick = { navigateToTopLevelDestination(replyDestination) } + colors = + NavigationDrawerItemDefaults.colors( + unselectedContainerColor = Color.Transparent, + ), + onClick = { navigateToTopLevelDestination(replyDestination) }, ) } } }, - measurePolicy = navigationMeasurePolicy(navigationContentPosition) + measurePolicy = navigationMeasurePolicy(navigationContentPosition), ) } } @@ -346,65 +360,69 @@ fun ModalNavigationDrawerContent( selectedDestination: String, navigationContentPosition: ReplyNavigationContentPosition, navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit, - onDrawerClicked: () -> Unit = {} + onDrawerClicked: () -> Unit = {}, ) { ModalDrawerSheet { // TODO remove custom nav drawer content positioning when NavDrawer component supports it. ticket : b/232495216 Layout( - modifier = Modifier - .background(MaterialTheme.colorScheme.inverseOnSurface) - .padding(16.dp), + modifier = + Modifier + .background(MaterialTheme.colorScheme.inverseOnSurface) + .padding(16.dp), content = { Column( modifier = Modifier.layoutId(LayoutType.HEADER), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(4.dp), ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = stringResource(id = R.string.app_name).uppercase(), style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, ) IconButton(onClick = onDrawerClicked) { Icon( imageVector = Icons.AutoMirrored.Filled.MenuOpen, - contentDescription = stringResource(id = R.string.close_drawer) + contentDescription = stringResource(id = R.string.close_drawer), ) } } ExtendedFloatingActionButton( onClick = { /*TODO*/ }, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 40.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 40.dp), containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, ) { Icon( imageVector = Icons.Default.Edit, contentDescription = stringResource(id = R.string.compose), - modifier = Modifier.size(18.dp) + modifier = Modifier.size(18.dp), ) Text( text = stringResource(id = R.string.compose), modifier = Modifier.weight(1f), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } Column( - modifier = Modifier - .layoutId(LayoutType.CONTENT) - .verticalScroll(rememberScrollState()), + modifier = + Modifier + .layoutId(LayoutType.CONTENT) + .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { TOP_LEVEL_DESTINATIONS.forEach { replyDestination -> @@ -413,34 +431,34 @@ fun ModalNavigationDrawerContent( label = { Text( text = stringResource(id = replyDestination.iconTextId), - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier.padding(horizontal = 16.dp), ) }, icon = { Icon( imageVector = replyDestination.selectedIcon, - contentDescription = stringResource( - id = replyDestination.iconTextId - ) + contentDescription = + stringResource( + id = replyDestination.iconTextId, + ), ) }, - colors = NavigationDrawerItemDefaults.colors( - unselectedContainerColor = Color.Transparent - ), - onClick = { navigateToTopLevelDestination(replyDestination) } + colors = + NavigationDrawerItemDefaults.colors( + unselectedContainerColor = Color.Transparent, + ), + onClick = { navigateToTopLevelDestination(replyDestination) }, ) } } }, - measurePolicy = navigationMeasurePolicy(navigationContentPosition) + measurePolicy = navigationMeasurePolicy(navigationContentPosition), ) } } -fun navigationMeasurePolicy( - navigationContentPosition: ReplyNavigationContentPosition, -): MeasurePolicy { - return MeasurePolicy { measurables, constraints -> +fun navigationMeasurePolicy(navigationContentPosition: ReplyNavigationContentPosition): MeasurePolicy = + MeasurePolicy { measurables, constraints -> lateinit var headerMeasurable: Measurable lateinit var contentMeasurable: Measurable measurables.forEach { @@ -452,9 +470,10 @@ fun navigationMeasurePolicy( } val headerPlaceable = headerMeasurable.measure(constraints) - val contentPlaceable = contentMeasurable.measure( - constraints.offset(vertical = -headerPlaceable.height) - ) + val contentPlaceable = + contentMeasurable.measure( + constraints.offset(vertical = -headerPlaceable.height), + ) layout(constraints.maxWidth, constraints.maxHeight) { // Place the header, this goes at the top headerPlaceable.placeRelative(0, 0) @@ -462,20 +481,21 @@ fun navigationMeasurePolicy( // Determine how much space is not taken up by the content val nonContentVerticalSpace = constraints.maxHeight - contentPlaceable.height - val contentPlaceableY = when (navigationContentPosition) { - // Figure out the place we want to place the content, with respect to the - // parent (ignoring the header for now) - ReplyNavigationContentPosition.TOP -> 0 - ReplyNavigationContentPosition.CENTER -> nonContentVerticalSpace / 2 - } - // And finally, make sure we don't overlap with the header. - .coerceAtLeast(headerPlaceable.height) + val contentPlaceableY = + when (navigationContentPosition) { + // Figure out the place we want to place the content, with respect to the + // parent (ignoring the header for now) + ReplyNavigationContentPosition.TOP -> 0 + ReplyNavigationContentPosition.CENTER -> nonContentVerticalSpace / 2 + } + // And finally, make sure we don't overlap with the header. + .coerceAtLeast(headerPlaceable.height) contentPlaceable.placeRelative(0, contentPlaceableY) } } -} enum class LayoutType { - HEADER, CONTENT + HEADER, + CONTENT, } diff --git a/Reply/app/src/main/java/com/example/reply/ui/theme/Shapes.kt b/Reply/app/src/main/java/com/example/reply/ui/theme/Shapes.kt index 0c11182f2b..7023b4ab40 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/theme/Shapes.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/theme/Shapes.kt @@ -20,10 +20,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp -val shapes = Shapes( - extraSmall = RoundedCornerShape(4.dp), - small = RoundedCornerShape(8.dp), - medium = RoundedCornerShape(16.dp), - large = RoundedCornerShape(24.dp), - extraLarge = RoundedCornerShape(32.dp) -) +val shapes = + Shapes( + extraSmall = RoundedCornerShape(4.dp), + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(16.dp), + large = RoundedCornerShape(24.dp), + extraLarge = RoundedCornerShape(32.dp), + ) diff --git a/Reply/app/src/main/java/com/example/reply/ui/theme/Theme.kt b/Reply/app/src/main/java/com/example/reply/ui/theme/Theme.kt index 5118734c24..2cd1152689 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/theme/Theme.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/theme/Theme.kt @@ -34,240 +34,244 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat -private val lightScheme = lightColorScheme( - primary = primaryLight, - onPrimary = onPrimaryLight, - primaryContainer = primaryContainerLight, - onPrimaryContainer = onPrimaryContainerLight, - secondary = secondaryLight, - onSecondary = onSecondaryLight, - secondaryContainer = secondaryContainerLight, - onSecondaryContainer = onSecondaryContainerLight, - tertiary = tertiaryLight, - onTertiary = onTertiaryLight, - tertiaryContainer = tertiaryContainerLight, - onTertiaryContainer = onTertiaryContainerLight, - error = errorLight, - onError = onErrorLight, - errorContainer = errorContainerLight, - onErrorContainer = onErrorContainerLight, - background = backgroundLight, - onBackground = onBackgroundLight, - surface = surfaceLight, - onSurface = onSurfaceLight, - surfaceVariant = surfaceVariantLight, - onSurfaceVariant = onSurfaceVariantLight, - outline = outlineLight, - outlineVariant = outlineVariantLight, - scrim = scrimLight, - inverseSurface = inverseSurfaceLight, - inverseOnSurface = inverseOnSurfaceLight, - inversePrimary = inversePrimaryLight, - surfaceDim = surfaceDimLight, - surfaceBright = surfaceBrightLight, - surfaceContainerLowest = surfaceContainerLowestLight, - surfaceContainerLow = surfaceContainerLowLight, - surfaceContainer = surfaceContainerLight, - surfaceContainerHigh = surfaceContainerHighLight, - surfaceContainerHighest = surfaceContainerHighestLight, -) +private val lightScheme = + lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, + ) -private val darkScheme = darkColorScheme( - primary = primaryDark, - onPrimary = onPrimaryDark, - primaryContainer = primaryContainerDark, - onPrimaryContainer = onPrimaryContainerDark, - secondary = secondaryDark, - onSecondary = onSecondaryDark, - secondaryContainer = secondaryContainerDark, - onSecondaryContainer = onSecondaryContainerDark, - tertiary = tertiaryDark, - onTertiary = onTertiaryDark, - tertiaryContainer = tertiaryContainerDark, - onTertiaryContainer = onTertiaryContainerDark, - error = errorDark, - onError = onErrorDark, - errorContainer = errorContainerDark, - onErrorContainer = onErrorContainerDark, - background = backgroundDark, - onBackground = onBackgroundDark, - surface = surfaceDark, - onSurface = onSurfaceDark, - surfaceVariant = surfaceVariantDark, - onSurfaceVariant = onSurfaceVariantDark, - outline = outlineDark, - outlineVariant = outlineVariantDark, - scrim = scrimDark, - inverseSurface = inverseSurfaceDark, - inverseOnSurface = inverseOnSurfaceDark, - inversePrimary = inversePrimaryDark, - surfaceDim = surfaceDimDark, - surfaceBright = surfaceBrightDark, - surfaceContainerLowest = surfaceContainerLowestDark, - surfaceContainerLow = surfaceContainerLowDark, - surfaceContainer = surfaceContainerDark, - surfaceContainerHigh = surfaceContainerHighDark, - surfaceContainerHighest = surfaceContainerHighestDark, -) +private val darkScheme = + darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, + ) -private val mediumContrastLightColorScheme = lightColorScheme( - primary = primaryLightMediumContrast, - onPrimary = onPrimaryLightMediumContrast, - primaryContainer = primaryContainerLightMediumContrast, - onPrimaryContainer = onPrimaryContainerLightMediumContrast, - secondary = secondaryLightMediumContrast, - onSecondary = onSecondaryLightMediumContrast, - secondaryContainer = secondaryContainerLightMediumContrast, - onSecondaryContainer = onSecondaryContainerLightMediumContrast, - tertiary = tertiaryLightMediumContrast, - onTertiary = onTertiaryLightMediumContrast, - tertiaryContainer = tertiaryContainerLightMediumContrast, - onTertiaryContainer = onTertiaryContainerLightMediumContrast, - error = errorLightMediumContrast, - onError = onErrorLightMediumContrast, - errorContainer = errorContainerLightMediumContrast, - onErrorContainer = onErrorContainerLightMediumContrast, - background = backgroundLightMediumContrast, - onBackground = onBackgroundLightMediumContrast, - surface = surfaceLightMediumContrast, - onSurface = onSurfaceLightMediumContrast, - surfaceVariant = surfaceVariantLightMediumContrast, - onSurfaceVariant = onSurfaceVariantLightMediumContrast, - outline = outlineLightMediumContrast, - outlineVariant = outlineVariantLightMediumContrast, - scrim = scrimLightMediumContrast, - inverseSurface = inverseSurfaceLightMediumContrast, - inverseOnSurface = inverseOnSurfaceLightMediumContrast, - inversePrimary = inversePrimaryLightMediumContrast, - surfaceDim = surfaceDimLightMediumContrast, - surfaceBright = surfaceBrightLightMediumContrast, - surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, - surfaceContainerLow = surfaceContainerLowLightMediumContrast, - surfaceContainer = surfaceContainerLightMediumContrast, - surfaceContainerHigh = surfaceContainerHighLightMediumContrast, - surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, -) +private val mediumContrastLightColorScheme = + lightColorScheme( + primary = primaryLightMediumContrast, + onPrimary = onPrimaryLightMediumContrast, + primaryContainer = primaryContainerLightMediumContrast, + onPrimaryContainer = onPrimaryContainerLightMediumContrast, + secondary = secondaryLightMediumContrast, + onSecondary = onSecondaryLightMediumContrast, + secondaryContainer = secondaryContainerLightMediumContrast, + onSecondaryContainer = onSecondaryContainerLightMediumContrast, + tertiary = tertiaryLightMediumContrast, + onTertiary = onTertiaryLightMediumContrast, + tertiaryContainer = tertiaryContainerLightMediumContrast, + onTertiaryContainer = onTertiaryContainerLightMediumContrast, + error = errorLightMediumContrast, + onError = onErrorLightMediumContrast, + errorContainer = errorContainerLightMediumContrast, + onErrorContainer = onErrorContainerLightMediumContrast, + background = backgroundLightMediumContrast, + onBackground = onBackgroundLightMediumContrast, + surface = surfaceLightMediumContrast, + onSurface = onSurfaceLightMediumContrast, + surfaceVariant = surfaceVariantLightMediumContrast, + onSurfaceVariant = onSurfaceVariantLightMediumContrast, + outline = outlineLightMediumContrast, + outlineVariant = outlineVariantLightMediumContrast, + scrim = scrimLightMediumContrast, + inverseSurface = inverseSurfaceLightMediumContrast, + inverseOnSurface = inverseOnSurfaceLightMediumContrast, + inversePrimary = inversePrimaryLightMediumContrast, + surfaceDim = surfaceDimLightMediumContrast, + surfaceBright = surfaceBrightLightMediumContrast, + surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, + surfaceContainerLow = surfaceContainerLowLightMediumContrast, + surfaceContainer = surfaceContainerLightMediumContrast, + surfaceContainerHigh = surfaceContainerHighLightMediumContrast, + surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, + ) -private val highContrastLightColorScheme = lightColorScheme( - primary = primaryLightHighContrast, - onPrimary = onPrimaryLightHighContrast, - primaryContainer = primaryContainerLightHighContrast, - onPrimaryContainer = onPrimaryContainerLightHighContrast, - secondary = secondaryLightHighContrast, - onSecondary = onSecondaryLightHighContrast, - secondaryContainer = secondaryContainerLightHighContrast, - onSecondaryContainer = onSecondaryContainerLightHighContrast, - tertiary = tertiaryLightHighContrast, - onTertiary = onTertiaryLightHighContrast, - tertiaryContainer = tertiaryContainerLightHighContrast, - onTertiaryContainer = onTertiaryContainerLightHighContrast, - error = errorLightHighContrast, - onError = onErrorLightHighContrast, - errorContainer = errorContainerLightHighContrast, - onErrorContainer = onErrorContainerLightHighContrast, - background = backgroundLightHighContrast, - onBackground = onBackgroundLightHighContrast, - surface = surfaceLightHighContrast, - onSurface = onSurfaceLightHighContrast, - surfaceVariant = surfaceVariantLightHighContrast, - onSurfaceVariant = onSurfaceVariantLightHighContrast, - outline = outlineLightHighContrast, - outlineVariant = outlineVariantLightHighContrast, - scrim = scrimLightHighContrast, - inverseSurface = inverseSurfaceLightHighContrast, - inverseOnSurface = inverseOnSurfaceLightHighContrast, - inversePrimary = inversePrimaryLightHighContrast, - surfaceDim = surfaceDimLightHighContrast, - surfaceBright = surfaceBrightLightHighContrast, - surfaceContainerLowest = surfaceContainerLowestLightHighContrast, - surfaceContainerLow = surfaceContainerLowLightHighContrast, - surfaceContainer = surfaceContainerLightHighContrast, - surfaceContainerHigh = surfaceContainerHighLightHighContrast, - surfaceContainerHighest = surfaceContainerHighestLightHighContrast, -) +private val highContrastLightColorScheme = + lightColorScheme( + primary = primaryLightHighContrast, + onPrimary = onPrimaryLightHighContrast, + primaryContainer = primaryContainerLightHighContrast, + onPrimaryContainer = onPrimaryContainerLightHighContrast, + secondary = secondaryLightHighContrast, + onSecondary = onSecondaryLightHighContrast, + secondaryContainer = secondaryContainerLightHighContrast, + onSecondaryContainer = onSecondaryContainerLightHighContrast, + tertiary = tertiaryLightHighContrast, + onTertiary = onTertiaryLightHighContrast, + tertiaryContainer = tertiaryContainerLightHighContrast, + onTertiaryContainer = onTertiaryContainerLightHighContrast, + error = errorLightHighContrast, + onError = onErrorLightHighContrast, + errorContainer = errorContainerLightHighContrast, + onErrorContainer = onErrorContainerLightHighContrast, + background = backgroundLightHighContrast, + onBackground = onBackgroundLightHighContrast, + surface = surfaceLightHighContrast, + onSurface = onSurfaceLightHighContrast, + surfaceVariant = surfaceVariantLightHighContrast, + onSurfaceVariant = onSurfaceVariantLightHighContrast, + outline = outlineLightHighContrast, + outlineVariant = outlineVariantLightHighContrast, + scrim = scrimLightHighContrast, + inverseSurface = inverseSurfaceLightHighContrast, + inverseOnSurface = inverseOnSurfaceLightHighContrast, + inversePrimary = inversePrimaryLightHighContrast, + surfaceDim = surfaceDimLightHighContrast, + surfaceBright = surfaceBrightLightHighContrast, + surfaceContainerLowest = surfaceContainerLowestLightHighContrast, + surfaceContainerLow = surfaceContainerLowLightHighContrast, + surfaceContainer = surfaceContainerLightHighContrast, + surfaceContainerHigh = surfaceContainerHighLightHighContrast, + surfaceContainerHighest = surfaceContainerHighestLightHighContrast, + ) -private val mediumContrastDarkColorScheme = darkColorScheme( - primary = primaryDarkMediumContrast, - onPrimary = onPrimaryDarkMediumContrast, - primaryContainer = primaryContainerDarkMediumContrast, - onPrimaryContainer = onPrimaryContainerDarkMediumContrast, - secondary = secondaryDarkMediumContrast, - onSecondary = onSecondaryDarkMediumContrast, - secondaryContainer = secondaryContainerDarkMediumContrast, - onSecondaryContainer = onSecondaryContainerDarkMediumContrast, - tertiary = tertiaryDarkMediumContrast, - onTertiary = onTertiaryDarkMediumContrast, - tertiaryContainer = tertiaryContainerDarkMediumContrast, - onTertiaryContainer = onTertiaryContainerDarkMediumContrast, - error = errorDarkMediumContrast, - onError = onErrorDarkMediumContrast, - errorContainer = errorContainerDarkMediumContrast, - onErrorContainer = onErrorContainerDarkMediumContrast, - background = backgroundDarkMediumContrast, - onBackground = onBackgroundDarkMediumContrast, - surface = surfaceDarkMediumContrast, - onSurface = onSurfaceDarkMediumContrast, - surfaceVariant = surfaceVariantDarkMediumContrast, - onSurfaceVariant = onSurfaceVariantDarkMediumContrast, - outline = outlineDarkMediumContrast, - outlineVariant = outlineVariantDarkMediumContrast, - scrim = scrimDarkMediumContrast, - inverseSurface = inverseSurfaceDarkMediumContrast, - inverseOnSurface = inverseOnSurfaceDarkMediumContrast, - inversePrimary = inversePrimaryDarkMediumContrast, - surfaceDim = surfaceDimDarkMediumContrast, - surfaceBright = surfaceBrightDarkMediumContrast, - surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, - surfaceContainerLow = surfaceContainerLowDarkMediumContrast, - surfaceContainer = surfaceContainerDarkMediumContrast, - surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, - surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, -) +private val mediumContrastDarkColorScheme = + darkColorScheme( + primary = primaryDarkMediumContrast, + onPrimary = onPrimaryDarkMediumContrast, + primaryContainer = primaryContainerDarkMediumContrast, + onPrimaryContainer = onPrimaryContainerDarkMediumContrast, + secondary = secondaryDarkMediumContrast, + onSecondary = onSecondaryDarkMediumContrast, + secondaryContainer = secondaryContainerDarkMediumContrast, + onSecondaryContainer = onSecondaryContainerDarkMediumContrast, + tertiary = tertiaryDarkMediumContrast, + onTertiary = onTertiaryDarkMediumContrast, + tertiaryContainer = tertiaryContainerDarkMediumContrast, + onTertiaryContainer = onTertiaryContainerDarkMediumContrast, + error = errorDarkMediumContrast, + onError = onErrorDarkMediumContrast, + errorContainer = errorContainerDarkMediumContrast, + onErrorContainer = onErrorContainerDarkMediumContrast, + background = backgroundDarkMediumContrast, + onBackground = onBackgroundDarkMediumContrast, + surface = surfaceDarkMediumContrast, + onSurface = onSurfaceDarkMediumContrast, + surfaceVariant = surfaceVariantDarkMediumContrast, + onSurfaceVariant = onSurfaceVariantDarkMediumContrast, + outline = outlineDarkMediumContrast, + outlineVariant = outlineVariantDarkMediumContrast, + scrim = scrimDarkMediumContrast, + inverseSurface = inverseSurfaceDarkMediumContrast, + inverseOnSurface = inverseOnSurfaceDarkMediumContrast, + inversePrimary = inversePrimaryDarkMediumContrast, + surfaceDim = surfaceDimDarkMediumContrast, + surfaceBright = surfaceBrightDarkMediumContrast, + surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, + surfaceContainerLow = surfaceContainerLowDarkMediumContrast, + surfaceContainer = surfaceContainerDarkMediumContrast, + surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, + surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, + ) -private val highContrastDarkColorScheme = darkColorScheme( - primary = primaryDarkHighContrast, - onPrimary = onPrimaryDarkHighContrast, - primaryContainer = primaryContainerDarkHighContrast, - onPrimaryContainer = onPrimaryContainerDarkHighContrast, - secondary = secondaryDarkHighContrast, - onSecondary = onSecondaryDarkHighContrast, - secondaryContainer = secondaryContainerDarkHighContrast, - onSecondaryContainer = onSecondaryContainerDarkHighContrast, - tertiary = tertiaryDarkHighContrast, - onTertiary = onTertiaryDarkHighContrast, - tertiaryContainer = tertiaryContainerDarkHighContrast, - onTertiaryContainer = onTertiaryContainerDarkHighContrast, - error = errorDarkHighContrast, - onError = onErrorDarkHighContrast, - errorContainer = errorContainerDarkHighContrast, - onErrorContainer = onErrorContainerDarkHighContrast, - background = backgroundDarkHighContrast, - onBackground = onBackgroundDarkHighContrast, - surface = surfaceDarkHighContrast, - onSurface = onSurfaceDarkHighContrast, - surfaceVariant = surfaceVariantDarkHighContrast, - onSurfaceVariant = onSurfaceVariantDarkHighContrast, - outline = outlineDarkHighContrast, - outlineVariant = outlineVariantDarkHighContrast, - scrim = scrimDarkHighContrast, - inverseSurface = inverseSurfaceDarkHighContrast, - inverseOnSurface = inverseOnSurfaceDarkHighContrast, - inversePrimary = inversePrimaryDarkHighContrast, - surfaceDim = surfaceDimDarkHighContrast, - surfaceBright = surfaceBrightDarkHighContrast, - surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, - surfaceContainerLow = surfaceContainerLowDarkHighContrast, - surfaceContainer = surfaceContainerDarkHighContrast, - surfaceContainerHigh = surfaceContainerHighDarkHighContrast, - surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, -) +private val highContrastDarkColorScheme = + darkColorScheme( + primary = primaryDarkHighContrast, + onPrimary = onPrimaryDarkHighContrast, + primaryContainer = primaryContainerDarkHighContrast, + onPrimaryContainer = onPrimaryContainerDarkHighContrast, + secondary = secondaryDarkHighContrast, + onSecondary = onSecondaryDarkHighContrast, + secondaryContainer = secondaryContainerDarkHighContrast, + onSecondaryContainer = onSecondaryContainerDarkHighContrast, + tertiary = tertiaryDarkHighContrast, + onTertiary = onTertiaryDarkHighContrast, + tertiaryContainer = tertiaryContainerDarkHighContrast, + onTertiaryContainer = onTertiaryContainerDarkHighContrast, + error = errorDarkHighContrast, + onError = onErrorDarkHighContrast, + errorContainer = errorContainerDarkHighContrast, + onErrorContainer = onErrorContainerDarkHighContrast, + background = backgroundDarkHighContrast, + onBackground = onBackgroundDarkHighContrast, + surface = surfaceDarkHighContrast, + onSurface = onSurfaceDarkHighContrast, + surfaceVariant = surfaceVariantDarkHighContrast, + onSurfaceVariant = onSurfaceVariantDarkHighContrast, + outline = outlineDarkHighContrast, + outlineVariant = outlineVariantDarkHighContrast, + scrim = scrimDarkHighContrast, + inverseSurface = inverseSurfaceDarkHighContrast, + inverseOnSurface = inverseOnSurfaceDarkHighContrast, + inversePrimary = inversePrimaryDarkHighContrast, + surfaceDim = surfaceDimDarkHighContrast, + surfaceBright = surfaceBrightDarkHighContrast, + surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, + surfaceContainerLow = surfaceContainerLowDarkHighContrast, + surfaceContainer = surfaceContainerDarkHighContrast, + surfaceContainerHigh = surfaceContainerHighDarkHighContrast, + surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, + ) -fun isContrastAvailable(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -} +fun isContrastAvailable(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE @Composable -fun selectSchemeForContrast(isDark: Boolean,): ColorScheme { +fun selectSchemeForContrast(isDark: Boolean): ColorScheme { val context = LocalContext.current var colorScheme = if (isDark) darkScheme else lightScheme val isPreview = LocalInspectionMode.current @@ -276,36 +280,55 @@ fun selectSchemeForContrast(isDark: Boolean,): ColorScheme { val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager val contrastLevel = uiModeManager.contrast - colorScheme = when (contrastLevel) { - in 0.0f..0.33f -> if (isDark) - darkScheme else lightScheme + colorScheme = + when (contrastLevel) { + in 0.0f..0.33f -> + if (isDark) { + darkScheme + } else { + lightScheme + } - in 0.34f..0.66f -> if (isDark) - mediumContrastDarkColorScheme else mediumContrastLightColorScheme + in 0.34f..0.66f -> + if (isDark) { + mediumContrastDarkColorScheme + } else { + mediumContrastLightColorScheme + } - in 0.67f..1.0f -> if (isDark) - highContrastDarkColorScheme else highContrastLightColorScheme + in 0.67f..1.0f -> + if (isDark) { + highContrastDarkColorScheme + } else { + highContrastLightColorScheme + } - else -> if (isDark) darkScheme else lightScheme - } + else -> if (isDark) darkScheme else lightScheme + } + return colorScheme + } else { return colorScheme - } else return colorScheme + } } + @Composable fun ContrastAwareReplyTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = false, - content: @Composable() () -> Unit + content: + @Composable() + () -> Unit, ) { - val replyColorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } + val replyColorScheme = + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } - else -> selectSchemeForContrast(darkTheme) - } + else -> selectSchemeForContrast(darkTheme) + } val view = LocalView.current if (!view.isInEditMode) { SideEffect { @@ -319,6 +342,6 @@ fun ContrastAwareReplyTheme( colorScheme = replyColorScheme, typography = replyTypography, shapes = shapes, - content = content + content = content, ) } diff --git a/Reply/app/src/main/java/com/example/reply/ui/theme/Type.kt b/Reply/app/src/main/java/com/example/reply/ui/theme/Type.kt index a180cb8731..c00cd51526 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/theme/Type.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/theme/Type.kt @@ -22,77 +22,90 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp // Material 3 typography -val replyTypography = Typography( - headlineLarge = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp - ), - headlineMedium = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp - ), - headlineSmall = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.sp - ), - titleLarge = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - titleMedium = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - titleSmall = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - bodyLarge = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - bodyMedium = TextStyle( - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp - ), - bodySmall = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp - ), - labelLarge = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - labelMedium = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ), - labelSmall = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp +val replyTypography = + Typography( + headlineLarge = + TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + ), + headlineMedium = + TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp, + ), + headlineSmall = + TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp, + ), + titleLarge = + TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp, + ), + titleMedium = + TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + ), + titleSmall = + TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + bodyLarge = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + ), + bodyMedium = + TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + bodySmall = + TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + ), + labelLarge = + TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + labelMedium = + TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), + labelSmall = + TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), ) -) diff --git a/Reply/app/src/main/java/com/example/reply/ui/utils/WindowStateUtils.kt b/Reply/app/src/main/java/com/example/reply/ui/utils/WindowStateUtils.kt index f212254689..bba4412ab9 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/utils/WindowStateUtils.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/utils/WindowStateUtils.kt @@ -28,12 +28,12 @@ sealed interface DevicePosture { object NormalPosture : DevicePosture data class BookPosture( - val hingePosition: Rect + val hingePosition: Rect, ) : DevicePosture data class Separating( val hingePosition: Rect, - var orientation: FoldingFeature.Orientation + var orientation: FoldingFeature.Orientation, ) : DevicePosture } @@ -54,19 +54,23 @@ fun isSeparating(foldFeature: FoldingFeature?): Boolean { * Different type of navigation supported by app depending on device size and state. */ enum class ReplyNavigationType { - BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER + BOTTOM_NAVIGATION, + NAVIGATION_RAIL, + PERMANENT_NAVIGATION_DRAWER, } /** * Different position of navigation content inside Navigation Rail, Navigation Drawer depending on device size and state. */ enum class ReplyNavigationContentPosition { - TOP, CENTER + TOP, + CENTER, } /** * App Content shown depending on device size and state. */ enum class ReplyContentType { - SINGLE_PANE, DUAL_PANE + SINGLE_PANE, + DUAL_PANE, } diff --git a/Reply/build.gradle.kts b/Reply/build.gradle.kts index 0a68895495..dcbafdf2ae 100644 --- a/Reply/build.gradle.kts +++ b/Reply/build.gradle.kts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import com.diffplug.gradle.spotless.SpotlessExtension plugins { alias(libs.plugins.gradle.versions) alias(libs.plugins.version.catalog.update) @@ -21,6 +21,30 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.compose) apply false + alias(libs.plugins.spotless) } apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") + +subprojects { + apply { + plugin(rootProject.libs.plugins.spotless.get().pluginId) + } + configure { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt") + ktlint(libs.versions.ktlint.get()) + .setEditorConfigPath("${project.rootDir}/.editorconfig") + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } + // Additional configuration for Kotlin Gradle scripts + kotlinGradle { + target("**/*.kts") + targetExclude("**/build/**/*.kts") + ktlint(libs.versions.ktlint.get()) // Apply ktlint to Gradle Kotlin scripts + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") + } + } +} diff --git a/Reply/buildscripts/init.gradle.kts b/Reply/buildscripts/init.gradle.kts deleted file mode 100644 index 1b7a54264c..0000000000 --- a/Reply/buildscripts/init.gradle.kts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -val ktlintVersion = "0.46.1" - -initscript { - val spotlessVersion = "6.10.0" - - repositories { - mavenCentral() - } - - dependencies { - classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion") - } -} - -allprojects { - if (this == rootProject) { - return@allprojects - } - apply() - extensions.configure { - kotlin { - target("**/*.kt") - targetExclude("**/build/**/*.kt") - ktlint(ktlintVersion).editorConfigOverride( - mapOf( - "ktlint_code_style" to "android", - "ij_kotlin_allow_trailing_comma" to true, - // These rules were introduced in ktlint 0.46.0 and should not be - // enabled without further discussion. They are disabled for now. - // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 - "disabled_rules" to - "filename," + - "annotation,annotation-spacing," + - "argument-list-wrapping," + - "double-colon-spacing," + - "enum-entry-name-case," + - "multiline-if-else," + - "no-empty-first-line-in-method-block," + - "package-name," + - "trailing-comma," + - "spacing-around-angle-brackets," + - "spacing-between-declarations-with-annotations," + - "spacing-between-declarations-with-comments," + - "unary-op-spacing" - ) - ) - licenseHeaderFile(rootProject.file("spotless/copyright.kt")) - } - format("kts") { - target("**/*.kts") - targetExclude("**/build/**/*.kts") - // Look for the first line that doesn't have a block comment (assumed to be the license) - licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") - } - } -} \ No newline at end of file diff --git a/Reply/gradle/libs.versions.toml b/Reply/gradle/libs.versions.toml index 2c34e54e54..016f154a0d 100644 --- a/Reply/gradle/libs.versions.toml +++ b/Reply/gradle/libs.versions.toml @@ -47,6 +47,7 @@ junit = "4.13.2" kotlin = "2.0.0" kotlinx_immutable = "0.3.7" ksp = "2.0.0-1.0.21" +ktlint = "1.3.1" maps-compose = "3.1.1" # @keep minSdk = "21" @@ -57,6 +58,7 @@ roborazzi = "1.12.0" rome = "1.18.0" room = "2.6.0" secrets = "2.0.1" +spotless = "6.25.0" # @keep targetSdk = "33" version-catalog-update = "0.8.4" @@ -179,4 +181,5 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } diff --git a/scripts/libs.versions.toml b/scripts/libs.versions.toml index 2c34e54e54..016f154a0d 100644 --- a/scripts/libs.versions.toml +++ b/scripts/libs.versions.toml @@ -47,6 +47,7 @@ junit = "4.13.2" kotlin = "2.0.0" kotlinx_immutable = "0.3.7" ksp = "2.0.0-1.0.21" +ktlint = "1.3.1" maps-compose = "3.1.1" # @keep minSdk = "21" @@ -57,6 +58,7 @@ roborazzi = "1.12.0" rome = "1.18.0" room = "2.6.0" secrets = "2.0.1" +spotless = "6.25.0" # @keep targetSdk = "33" version-catalog-update = "0.8.4" @@ -179,4 +181,5 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } From a93ac9adb9fa054f2fc8eb7392a2247e7f467201 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Mon, 12 Aug 2024 12:52:33 +0100 Subject: [PATCH 2/4] Fix WearOS imports. --- Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt index 352bd5e7e6..6e8cb780aa 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt @@ -41,7 +41,7 @@ import com.example.jetcaster.ui.PodcastDetails import com.example.jetcaster.ui.UpNext import com.example.jetcaster.ui.YourPodcasts import com.example.jetcaster.ui.episode.EpisodeScreen -import com.example.jetcaster.ui.latest_episodes.LatestEpisodesScreen +import com.example.jetcaster.ui.latestepisodes.LatestEpisodesScreen import com.example.jetcaster.ui.library.LibraryScreen import com.example.jetcaster.ui.player.PlayerScreen import com.example.jetcaster.ui.podcast.PodcastDetailsScreen From e4e3c956908f164102c0ccbaf65bbd02f71e20b4 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Mon, 12 Aug 2024 13:07:55 +0100 Subject: [PATCH 3/4] Fix spotlessCheck --- .github/workflows/Release.yml | 32 +----------------------------- .github/workflows/build-sample.yml | 2 +- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 21bc6a993a..0eb96871e6 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -38,27 +38,7 @@ jobs: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} draft: true - prerelease: false - - - name: Upload Crane - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: Crane/app/build/outputs/apk/debug/app-debug.apk - asset_name: crane-debug.apk - asset_content_type: application/vnd.android.package-archive - - - name: Upload Owl - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: Owl/app/build/outputs/apk/debug/app-debug.apk - asset_name: owl-debug.apk - asset_content_type: application/vnd.android.package-archive + prerelease: false - name: Upload Jetcaster uses: actions/upload-release-asset@v1 @@ -99,13 +79,3 @@ jobs: asset_path: Jetsnack/app/build/outputs/apk/debug/app-debug.apk asset_name: jetsnack-debug.apk asset_content_type: application/vnd.android.package-archive - - - name: Upload Jetsurvey - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: Jetsurvey/app/build/outputs/apk/debug/app-debug.apk - asset_name: jetsurvey-debug.apk - asset_content_type: application/vnd.android.package-archive diff --git a/.github/workflows/build-sample.yml b/.github/workflows/build-sample.yml index d4d65a9984..b06f5465e4 100644 --- a/.github/workflows/build-sample.yml +++ b/.github/workflows/build-sample.yml @@ -59,7 +59,7 @@ jobs: - name: Check formatting working-directory: ${{ inputs.path }} - run: ./gradlew --init-script buildscripts/init.gradle.kts spotlessCheck --stacktrace + run: ./gradlew spotlessCheck --stacktrace - name: Check lint working-directory: ${{ inputs.path }} From 694f8388a707b0219a7774563aa3f3afda25039a Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Mon, 12 Aug 2024 13:14:24 +0100 Subject: [PATCH 4/4] Fix formatting script --- .github/workflows/main.yml | 41 ------------------- ...Placeholder.kt => ThumbnailPlaceholder.kt} | 0 scripts/format.sh | 2 +- 3 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 .github/workflows/main.yml rename Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/{thumbnailPlaceholder.kt => ThumbnailPlaceholder.kt} (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index e8fdcae311..0000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: Apply Spotless - -on: - pull_request: - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - spotlessApply: - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - uses: actions/checkout@v4 - - - name: set up Java 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: 17 - - - name: Apply spotless formatting - run: ./scripts/format.sh - - - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: 🤖 Apply Spotless diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/ThumbnailPlaceholder.kt similarity index 100% rename from Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt rename to Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/ThumbnailPlaceholder.kt diff --git a/scripts/format.sh b/scripts/format.sh index c0c2497191..255236ec0f 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -24,4 +24,4 @@ set -xe -./scripts/gradlew_recursive.sh --init-script buildscripts/init.gradle.kts spotlessApply +./scripts/gradlew_recursive.sh spotlessApply