From 4dd911eabe5b067b4adc19ffb78a4d5592b9b478 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Sun, 11 Jun 2023 15:38:49 +0100 Subject: [PATCH 01/23] Migrate to Compose Multiplatform --- .idea/dictionaries/chris.xml | 7 + .idea/kotlinc.xml | 6 +- android-app/app/build.gradle.kts | 28 +--- .../app/tivi/ContentViewSetter.kt | 0 .../{java => kotlin}/app/tivi/TiviActivity.kt | 0 .../app/tivi/TiviApplication.kt | 0 .../tivi/appinitializers/EmojiInitializer.kt | 0 .../{java => kotlin}/app/tivi/home/Home.kt | 10 +- .../app/tivi/home/MainActivity.kt | 100 +++++++------ .../app/tivi/home/MainActivityViewModel.kt | 0 .../app/tivi/inject/ActivityComponent.kt | 0 .../inject/AndroidApplicationComponent.kt | 4 + .../app/tivi/inject/UiComponent.kt | 0 .../benchmark/BaselineProfileGenerator.kt | 0 .../app/tivi/benchmark/StartupBenchmark.kt | 0 .../app/tivi/app/test/AppScenarios.kt | 0 build.gradle.kts | 2 +- common/imageloading/build.gradle.kts | 37 +++-- .../imageloading/AndroidImageLoaderFactory.kt | 39 +++++ .../ImageLoadingPlatformComponent.kt | 25 ++++ .../imageloading/EpisodeCoilInterceptor.kt | 33 ++--- .../common/imageloading/ImageLoaderFactory.kt | 38 +++++ .../imageloading/ImageLoadingComponent.kt | 28 ++++ .../imageloading/ShowCoilInterceptor.kt | 22 +-- .../TmdbImageEntityCoilInterceptor.kt | 23 +-- .../ImageLoadingPlatformComponent.kt | 22 +++ .../imageloading/IosImageLoaderFactory.kt | 48 ++++++ .../imageloading/DesktopImageLoaderFactory.kt | 58 ++++++++ .../ImageLoadingPlatformComponent.kt | 22 +++ .../common/imageloading/CoilAppInitializer.kt | 33 ----- .../imageloading/ImageLoadingComponent.kt | 14 -- .../TrimTransparentEdgesTransformation.kt | 90 ------------ common/ui/circuit-overlay/build.gradle.kts | 29 ++-- .../app/tivi/overlays/BottomSheetOverlay.kt | 62 ++++++++ .../kotlin/app/tivi/overlays/DialogOverlay.kt | 23 +-- .../app/tivi/overlays/LocalNavigator.kt | 0 .../app/tivi/overlays/BottomSheetOverlay.kt | 82 ----------- common/ui/compose/build.gradle.kts | 72 ++++----- .../tivi/common/compose/ReportDrawnWhen.kt | 11 ++ .../app/tivi/common/compose/EntryGrid.kt | 33 ++--- .../kotlin}/app/tivi/common/compose/Layout.kt | 2 +- .../app/tivi/common/compose/LazyList.kt | 21 +-- .../common/compose/LazyPagingExtensions.kt | 0 .../tivi/common/compose/ReportDrawnWhen.kt | 9 ++ .../common/compose/StableCoroutineScope.kt | 0 .../common/compose/TiviCompositionLocals.kt | 0 .../compose/TiviPreferenceExtensions.kt | 0 .../app/tivi/common/compose/UiMessage.kt | 0 .../tivi/common/compose/WindowSizeClass.kt | 11 ++ .../app/tivi/common/compose/theme/Color.kt | 0 .../app/tivi/common/compose/theme/Shape.kt | 0 .../app/tivi/common/compose/theme/Theme.kt | 0 .../app/tivi/common/compose/theme/Type.kt | 0 .../app/tivi/common/compose/ui/AppBar.kt | 2 +- .../ui/AutoSizedCircularProgressIndicator.kt | 32 ---- .../app/tivi/common/compose/ui/Backdrop.kt | 1 - .../common/compose/ui/DateTimeTextFields.kt | 138 +++++++++--------- .../app/tivi/common/compose/ui/Empty.kt | 0 .../common/compose/ui/ExpandingSummary.kt | 0 .../tivi/common/compose/ui/GradientScrim.kt | 0 .../tivi/common/compose/ui/IconButtonScrim.kt | 0 .../app/tivi/common/compose/ui/Image.kt | 115 +++++++++++++++ .../tivi/common/compose/ui/LoadingButton.kt | 13 -- .../tivi/common/compose/ui/PaddingValues.kt | 0 .../app/tivi/common/compose/ui/Position.kt | 0 .../app/tivi/common/compose/ui/PosterCard.kt | 1 - .../tivi/common/compose/ui/RefreshButton.kt | 0 .../tivi/common/compose/ui/SearchTextField.kt | 0 .../app/tivi/common/compose/ui/SortChip.kt | 0 .../tivi/common/compose/ui/SortMenuPopup.kt | 0 .../common/compose/ui/TimePickerDialog.kt | 41 ++++++ .../tivi/common/compose/ui/TiviAlertDialog.kt | 0 .../common/compose/ui/UserProfileButton.kt | 1 - .../tivi/common/compose/ui/WindowInsets.kt | 0 .../tivi/common/compose/ReportDrawnWhen.kt | 10 ++ .../tivi/common/compose/ReportDrawnWhen.kt | 10 ++ .../java/app/tivi/common/compose/Debug.kt | 33 ----- .../tivi/common/compose/WindowSizeClass.kt | 14 -- .../tivi/common/compose/ui/AndroidDialog.kt | 58 -------- .../java/app/tivi/common/compose/ui/Image.kt | 49 ------- .../common/compose/ui/TimePickerDialog.kt | 57 -------- common/ui/resources-compose/build.gradle.kts | 24 --- .../src/main/AndroidManifest.xml | 2 - .../moko/resources/compose/AssetResource.kt | 19 --- .../moko/resources/compose/ColorResource.kt | 16 -- .../moko/resources/compose/FileResource.kt | 19 --- .../moko/resources/compose/FontResource.kt | 24 --- .../moko/resources/compose/ImageResource.kt | 13 -- .../moko/resources/compose/StringDescExt.kt | 15 -- .../moko/resources/compose/StringResource.kt | 30 ---- gradle.properties | 2 + .../build-logic/convention/build.gradle.kts | 5 - .../java/app/tivi/gradle/AndroidCompose.kt | 23 --- .../gradle/AndroidComposeConventionPlugin.kt | 15 -- .../app/tivi/gradle/Android.kt | 0 .../AndroidApplicationConventionPlugin.kt | 0 .../tivi/gradle/AndroidApplicationLauncher.kt | 0 .../gradle/AndroidLibraryConventionPlugin.kt | 0 .../gradle/AndroidTestConventionPlugin.kt | 0 .../{java => kotlin}/app/tivi/gradle/Java.kt | 0 .../app/tivi/gradle/Kotlin.kt | 0 .../gradle/KotlinAndroidConventionPlugin.kt | 0 .../KotlinMultiplatformConventionPlugin.kt | 0 .../app/tivi/gradle/RootConventionPlugin.kt | 0 .../app/tivi/gradle/Spotless.kt | 0 .../app/tivi/gradle/VersionCatalog.kt | 0 .../app/tivi/gradle/Versions.kt | 0 gradle/libs.versions.toml | 7 +- settings.gradle.kts | 1 - shared/build.gradle.kts | 18 ++- ui/account/build.gradle.kts | 15 +- .../app/tivi/account/AccountComponent.kt | 0 .../app/tivi/account/AccountPresenter.kt | 0 .../app/tivi/account/AccountUi.kt | 5 +- .../app/tivi/account/AccountUiState.kt | 0 ui/discover/build.gradle.kts | 38 ++--- .../app/tivi/home/discover/Discover.kt | 29 ++-- .../tivi/home/discover/DiscoverComponent.kt | 0 .../tivi/home/discover/DiscoverPresenter.kt | 0 .../app/tivi/home/discover/DiscoverUiState.kt | 0 ui/episode/details/build.gradle.kts | 40 ++--- .../app/tivi/episodedetails/EpisodeDetails.kt | 58 ++++---- .../episodedetails/EpisodeDetailsComponent.kt | 0 .../episodedetails/EpisodeDetailsPresenter.kt | 0 .../episodedetails/EpisodeDetailsUiState.kt | 0 ui/episode/track/build.gradle.kts | 38 ++--- .../app/tivi/episode/track/EpisodeTrack.kt | 29 ++-- .../episode/track/EpisodeTrackComponent.kt | 0 .../episode/track/EpisodeTrackPresenter.kt | 0 .../tivi/episode/track/EpisodeTrackUiState.kt | 0 ui/library/build.gradle.kts | 48 +++--- .../kotlin}/app/tivi/home/library/Library.kt | 24 ++- .../app/tivi/home/library/LibraryComponent.kt | 0 .../app/tivi/home/library/LibraryPresenter.kt | 0 .../app/tivi/home/library/LibraryUiState.kt | 0 ui/popular/build.gradle.kts | 32 ++-- .../app/tivi/home/popular/PopularShows.kt | 0 .../home/popular/PopularShowsComponent.kt | 0 .../home/popular/PopularShowsPresenter.kt | 0 .../tivi/home/popular/PopularShowsUiState.kt | 0 ui/recommended/build.gradle.kts | 32 ++-- .../app/tivi/home/recommended/Recommended.kt | 0 .../recommended/RecommendedShowsComponent.kt | 0 .../recommended/RecommendedShowsPresenter.kt | 0 .../recommended/RecommendedShowsUiState.kt | 0 ui/search/build.gradle.kts | 34 +++-- .../kotlin}/app/tivi/home/search/Search.kt | 27 ++-- .../app/tivi/home/search/SearchComponent.kt | 0 .../app/tivi/home/search/SearchPresenter.kt | 0 .../app/tivi/home/search/SearchUiState.kt | 0 .../app/tivi/settings/SettingsActivity.kt | 0 .../settings/SettingsPreferenceFragment.kt | 0 ui/show/details/build.gradle.kts | 36 ++--- .../tivi/showdetails/details/ShowDetails.kt | 78 +++------- .../details/ShowDetailsComponent.kt | 0 .../details/ShowDetailsPresenter.kt | 0 .../showdetails/details/ShowDetailsUiState.kt | 0 ui/show/seasons/build.gradle.kts | 38 ++--- .../tivi/showdetails/seasons/ShowSeasons.kt | 27 ++-- .../seasons/ShowSeasonsComponent.kt | 0 .../seasons/ShowSeasonsPresenter.kt | 0 .../showdetails/seasons/ShowSeasonsUiState.kt | 0 ui/trending/build.gradle.kts | 32 ++-- .../app/tivi/home/trending/Trending.kt | 0 .../home/trending/TrendingShowsComponent.kt | 0 .../home/trending/TrendingShowsPresenter.kt | 0 .../home/trending/TrendingShowsUiState.kt | 0 ui/upnext/build.gradle.kts | 52 +++---- .../kotlin}/app/tivi/home/upnext/UpNext.kt | 35 ++--- .../app/tivi/home/upnext/UpNextComponent.kt | 0 .../app/tivi/home/upnext/UpNextPresenter.kt | 0 .../app/tivi/home/upnext/UpNextUiState.kt | 0 172 files changed, 1211 insertions(+), 1388 deletions(-) create mode 100644 .idea/dictionaries/chris.xml rename android-app/app/src/main/{java => kotlin}/app/tivi/ContentViewSetter.kt (100%) rename android-app/app/src/main/{java => kotlin}/app/tivi/TiviActivity.kt (100%) rename android-app/app/src/main/{java => kotlin}/app/tivi/TiviApplication.kt (100%) rename android-app/app/src/main/{java => kotlin}/app/tivi/appinitializers/EmojiInitializer.kt (100%) rename android-app/app/src/main/{java => kotlin}/app/tivi/home/Home.kt (97%) rename android-app/app/src/main/{java => kotlin}/app/tivi/home/MainActivity.kt (70%) rename android-app/app/src/main/{java => kotlin}/app/tivi/home/MainActivityViewModel.kt (100%) rename android-app/app/src/main/{java => kotlin}/app/tivi/inject/ActivityComponent.kt (100%) rename android-app/app/src/main/{java => kotlin}/app/tivi/inject/AndroidApplicationComponent.kt (95%) rename android-app/app/src/main/{java => kotlin}/app/tivi/inject/UiComponent.kt (100%) rename android-app/benchmark/src/main/{java => kotlin}/app/tivi/benchmark/BaselineProfileGenerator.kt (100%) rename android-app/benchmark/src/main/{java => kotlin}/app/tivi/benchmark/StartupBenchmark.kt (100%) rename android-app/common-test/src/main/{java => kotlin}/app/tivi/app/test/AppScenarios.kt (100%) create mode 100644 common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/AndroidImageLoaderFactory.kt create mode 100644 common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt rename common/imageloading/src/{main/java => commonMain/kotlin}/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt (67%) create mode 100644 common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoaderFactory.kt create mode 100644 common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoadingComponent.kt rename common/imageloading/src/{main/java => commonMain/kotlin}/app/tivi/common/imageloading/ShowCoilInterceptor.kt (77%) rename common/imageloading/src/{main/java => commonMain/kotlin}/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt (67%) create mode 100644 common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt create mode 100644 common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/IosImageLoaderFactory.kt create mode 100644 common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/DesktopImageLoaderFactory.kt create mode 100644 common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt delete mode 100644 common/imageloading/src/main/java/app/tivi/common/imageloading/CoilAppInitializer.kt delete mode 100644 common/imageloading/src/main/java/app/tivi/common/imageloading/ImageLoadingComponent.kt delete mode 100644 common/imageloading/src/main/java/app/tivi/common/imageloading/TrimTransparentEdgesTransformation.kt create mode 100644 common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/BottomSheetOverlay.kt rename common/ui/circuit-overlay/src/{main => commonMain}/kotlin/app/tivi/overlays/DialogOverlay.kt (58%) rename common/ui/circuit-overlay/src/{main => commonMain}/kotlin/app/tivi/overlays/LocalNavigator.kt (100%) delete mode 100644 common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/BottomSheetOverlay.kt create mode 100644 common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/EntryGrid.kt (94%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/Layout.kt (96%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/LazyList.kt (88%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/LazyPagingExtensions.kt (100%) create mode 100644 common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/StableCoroutineScope.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/TiviCompositionLocals.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/TiviPreferenceExtensions.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/UiMessage.kt (100%) create mode 100644 common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/WindowSizeClass.kt rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/theme/Color.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/theme/Shape.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/theme/Theme.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/theme/Type.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/AppBar.kt (98%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt (64%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/Backdrop.kt (98%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/DateTimeTextFields.kt (61%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/Empty.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/ExpandingSummary.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/GradientScrim.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/IconButtonScrim.kt (100%) create mode 100644 common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/LoadingButton.kt (86%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/PaddingValues.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/Position.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/PosterCard.kt (97%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/RefreshButton.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/SearchTextField.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/SortChip.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/SortMenuPopup.kt (100%) create mode 100644 common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TimePickerDialog.kt rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/TiviAlertDialog.kt (100%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/UserProfileButton.kt (96%) rename common/ui/compose/src/{main/java => commonMain/kotlin}/app/tivi/common/compose/ui/WindowInsets.kt (100%) create mode 100644 common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt create mode 100644 common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt delete mode 100644 common/ui/compose/src/main/java/app/tivi/common/compose/Debug.kt delete mode 100644 common/ui/compose/src/main/java/app/tivi/common/compose/WindowSizeClass.kt delete mode 100644 common/ui/compose/src/main/java/app/tivi/common/compose/ui/AndroidDialog.kt delete mode 100644 common/ui/compose/src/main/java/app/tivi/common/compose/ui/Image.kt delete mode 100644 common/ui/compose/src/main/java/app/tivi/common/compose/ui/TimePickerDialog.kt delete mode 100644 common/ui/resources-compose/build.gradle.kts delete mode 100644 common/ui/resources-compose/src/main/AndroidManifest.xml delete mode 100644 common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/AssetResource.kt delete mode 100644 common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ColorResource.kt delete mode 100644 common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FileResource.kt delete mode 100644 common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FontResource.kt delete mode 100644 common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt delete mode 100644 common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringDescExt.kt delete mode 100644 common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringResource.kt delete mode 100644 gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidCompose.kt delete mode 100644 gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidComposeConventionPlugin.kt rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/Android.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/AndroidApplicationConventionPlugin.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/AndroidApplicationLauncher.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/AndroidLibraryConventionPlugin.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/AndroidTestConventionPlugin.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/Java.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/Kotlin.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/KotlinAndroidConventionPlugin.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/KotlinMultiplatformConventionPlugin.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/RootConventionPlugin.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/Spotless.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/VersionCatalog.kt (100%) rename gradle/build-logic/convention/src/main/{java => kotlin}/app/tivi/gradle/Versions.kt (100%) rename ui/account/src/main/{java => kotlin}/app/tivi/account/AccountComponent.kt (100%) rename ui/account/src/main/{java => kotlin}/app/tivi/account/AccountPresenter.kt (100%) rename ui/account/src/main/{java => kotlin}/app/tivi/account/AccountUi.kt (98%) rename ui/account/src/main/{java => kotlin}/app/tivi/account/AccountUiState.kt (100%) rename ui/discover/src/{main/java => commonMain/kotlin}/app/tivi/home/discover/Discover.kt (96%) rename ui/discover/src/{main/java => commonMain/kotlin}/app/tivi/home/discover/DiscoverComponent.kt (100%) rename ui/discover/src/{main/java => commonMain/kotlin}/app/tivi/home/discover/DiscoverPresenter.kt (100%) rename ui/discover/src/{main/java => commonMain/kotlin}/app/tivi/home/discover/DiscoverUiState.kt (100%) rename ui/episode/details/src/{main/java => commonMain/kotlin}/app/tivi/episodedetails/EpisodeDetails.kt (93%) rename ui/episode/details/src/{main/java => commonMain/kotlin}/app/tivi/episodedetails/EpisodeDetailsComponent.kt (100%) rename ui/episode/details/src/{main/java => commonMain/kotlin}/app/tivi/episodedetails/EpisodeDetailsPresenter.kt (100%) rename ui/episode/details/src/{main/java => commonMain/kotlin}/app/tivi/episodedetails/EpisodeDetailsUiState.kt (100%) rename ui/episode/track/src/{main/java => commonMain/kotlin}/app/tivi/episode/track/EpisodeTrack.kt (94%) rename ui/episode/track/src/{main/java => commonMain/kotlin}/app/tivi/episode/track/EpisodeTrackComponent.kt (100%) rename ui/episode/track/src/{main/java => commonMain/kotlin}/app/tivi/episode/track/EpisodeTrackPresenter.kt (100%) rename ui/episode/track/src/{main/java => commonMain/kotlin}/app/tivi/episode/track/EpisodeTrackUiState.kt (100%) rename ui/library/src/{main/java => commonMain/kotlin}/app/tivi/home/library/Library.kt (97%) rename ui/library/src/{main/java => commonMain/kotlin}/app/tivi/home/library/LibraryComponent.kt (100%) rename ui/library/src/{main/java => commonMain/kotlin}/app/tivi/home/library/LibraryPresenter.kt (100%) rename ui/library/src/{main/java => commonMain/kotlin}/app/tivi/home/library/LibraryUiState.kt (100%) rename ui/popular/src/{main/java => commonMain/kotlin}/app/tivi/home/popular/PopularShows.kt (100%) rename ui/popular/src/{main/java => commonMain/kotlin}/app/tivi/home/popular/PopularShowsComponent.kt (100%) rename ui/popular/src/{main/java => commonMain/kotlin}/app/tivi/home/popular/PopularShowsPresenter.kt (100%) rename ui/popular/src/{main/java => commonMain/kotlin}/app/tivi/home/popular/PopularShowsUiState.kt (100%) rename ui/recommended/src/{main/java => commonMain/kotlin}/app/tivi/home/recommended/Recommended.kt (100%) rename ui/recommended/src/{main/java => commonMain/kotlin}/app/tivi/home/recommended/RecommendedShowsComponent.kt (100%) rename ui/recommended/src/{main/java => commonMain/kotlin}/app/tivi/home/recommended/RecommendedShowsPresenter.kt (100%) rename ui/recommended/src/{main/java => commonMain/kotlin}/app/tivi/home/recommended/RecommendedShowsUiState.kt (100%) rename ui/search/src/{main/java => commonMain/kotlin}/app/tivi/home/search/Search.kt (94%) rename ui/search/src/{main/java => commonMain/kotlin}/app/tivi/home/search/SearchComponent.kt (100%) rename ui/search/src/{main/java => commonMain/kotlin}/app/tivi/home/search/SearchPresenter.kt (100%) rename ui/search/src/{main/java => commonMain/kotlin}/app/tivi/home/search/SearchUiState.kt (100%) rename ui/settings/src/main/{java => kotlin}/app/tivi/settings/SettingsActivity.kt (100%) rename ui/settings/src/main/{java => kotlin}/app/tivi/settings/SettingsPreferenceFragment.kt (100%) rename ui/show/details/src/{main/java => commonMain/kotlin}/app/tivi/showdetails/details/ShowDetails.kt (93%) rename ui/show/details/src/{main/java => commonMain/kotlin}/app/tivi/showdetails/details/ShowDetailsComponent.kt (100%) rename ui/show/details/src/{main/java => commonMain/kotlin}/app/tivi/showdetails/details/ShowDetailsPresenter.kt (100%) rename ui/show/details/src/{main/java => commonMain/kotlin}/app/tivi/showdetails/details/ShowDetailsUiState.kt (100%) rename ui/show/seasons/src/{main/java => commonMain/kotlin}/app/tivi/showdetails/seasons/ShowSeasons.kt (96%) rename ui/show/seasons/src/{main/java => commonMain/kotlin}/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt (100%) rename ui/show/seasons/src/{main/java => commonMain/kotlin}/app/tivi/showdetails/seasons/ShowSeasonsPresenter.kt (100%) rename ui/show/seasons/src/{main/java => commonMain/kotlin}/app/tivi/showdetails/seasons/ShowSeasonsUiState.kt (100%) rename ui/trending/src/{main/java => commonMain/kotlin}/app/tivi/home/trending/Trending.kt (100%) rename ui/trending/src/{main/java => commonMain/kotlin}/app/tivi/home/trending/TrendingShowsComponent.kt (100%) rename ui/trending/src/{main/java => commonMain/kotlin}/app/tivi/home/trending/TrendingShowsPresenter.kt (100%) rename ui/trending/src/{main/java => commonMain/kotlin}/app/tivi/home/trending/TrendingShowsUiState.kt (100%) rename ui/upnext/src/{main/java => commonMain/kotlin}/app/tivi/home/upnext/UpNext.kt (94%) rename ui/upnext/src/{main/java => commonMain/kotlin}/app/tivi/home/upnext/UpNextComponent.kt (100%) rename ui/upnext/src/{main/java => commonMain/kotlin}/app/tivi/home/upnext/UpNextPresenter.kt (100%) rename ui/upnext/src/{main/java => commonMain/kotlin}/app/tivi/home/upnext/UpNextUiState.kt (100%) diff --git a/.idea/dictionaries/chris.xml b/.idea/dictionaries/chris.xml new file mode 100644 index 0000000000..06d5fdf03b --- /dev/null +++ b/.idea/dictionaries/chris.xml @@ -0,0 +1,7 @@ + + + + spdx + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 9fee720a8d..69e86158ba 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,10 +1,6 @@ - - - \ No newline at end of file diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts index 92e4f0b3a5..8254756123 100644 --- a/android-app/app/build.gradle.kts +++ b/android-app/app/build.gradle.kts @@ -4,9 +4,9 @@ plugins { id("app.tivi.android.application") - id("app.tivi.android.compose") id("app.tivi.kotlin.android") alias(libs.plugins.ksp) + alias(libs.plugins.composeMultiplatform) } val appVersionCode = propOrDef("TIVI_VERSIONCODE", "1000").toInt() @@ -150,18 +150,7 @@ dependencies { implementation(projects.shared) implementation(projects.ui.account) - implementation(projects.ui.discover) - implementation(projects.ui.episode.details) - implementation(projects.ui.episode.track) - implementation(projects.ui.library) - implementation(projects.ui.popular) - implementation(projects.ui.trending) - implementation(projects.ui.recommended) - implementation(projects.ui.search) - implementation(projects.ui.show.details) - implementation(projects.ui.show.seasons) implementation(projects.ui.settings) - implementation(projects.ui.upnext) implementation(libs.circuit.overlay) @@ -172,16 +161,13 @@ dependencies { implementation(libs.androidx.emoji) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material.iconsext) - implementation(libs.compose.material3.material3) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.materialIconsExtended) + implementation(compose.material3) implementation(libs.compose.material3.windowsizeclass) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) - - implementation(libs.timber) + implementation(compose.animation) + implementation(compose.uiTooling) implementation(libs.kotlin.coroutines.android) diff --git a/android-app/app/src/main/java/app/tivi/ContentViewSetter.kt b/android-app/app/src/main/kotlin/app/tivi/ContentViewSetter.kt similarity index 100% rename from android-app/app/src/main/java/app/tivi/ContentViewSetter.kt rename to android-app/app/src/main/kotlin/app/tivi/ContentViewSetter.kt diff --git a/android-app/app/src/main/java/app/tivi/TiviActivity.kt b/android-app/app/src/main/kotlin/app/tivi/TiviActivity.kt similarity index 100% rename from android-app/app/src/main/java/app/tivi/TiviActivity.kt rename to android-app/app/src/main/kotlin/app/tivi/TiviActivity.kt diff --git a/android-app/app/src/main/java/app/tivi/TiviApplication.kt b/android-app/app/src/main/kotlin/app/tivi/TiviApplication.kt similarity index 100% rename from android-app/app/src/main/java/app/tivi/TiviApplication.kt rename to android-app/app/src/main/kotlin/app/tivi/TiviApplication.kt diff --git a/android-app/app/src/main/java/app/tivi/appinitializers/EmojiInitializer.kt b/android-app/app/src/main/kotlin/app/tivi/appinitializers/EmojiInitializer.kt similarity index 100% rename from android-app/app/src/main/java/app/tivi/appinitializers/EmojiInitializer.kt rename to android-app/app/src/main/kotlin/app/tivi/appinitializers/EmojiInitializer.kt diff --git a/android-app/app/src/main/java/app/tivi/home/Home.kt b/android-app/app/src/main/kotlin/app/tivi/home/Home.kt similarity index 97% rename from android-app/app/src/main/java/app/tivi/home/Home.kt rename to android-app/app/src/main/kotlin/app/tivi/home/Home.kt index 35e286ecf1..3664a024d4 100644 --- a/android-app/app/src/main/java/app/tivi/home/Home.kt +++ b/android-app/app/src/main/kotlin/app/tivi/home/Home.kt @@ -12,10 +12,7 @@ import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeContentPadding -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsBottomHeight @@ -27,6 +24,7 @@ import androidx.compose.material.icons.filled.Weekend import androidx.compose.material.icons.outlined.VideoLibrary import androidx.compose.material.icons.outlined.Weekend import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem @@ -56,6 +54,9 @@ import app.tivi.screens.DiscoverScreen import app.tivi.screens.LibraryScreen import app.tivi.screens.SearchScreen import app.tivi.screens.UpNextScreen +import com.moriatsushi.insetsx.navigationBars +import com.moriatsushi.insetsx.safeContentPadding +import com.moriatsushi.insetsx.statusBars import com.slack.circuit.backstack.SaveableBackStack import com.slack.circuit.foundation.NavigableCircuitContent import com.slack.circuit.foundation.screen @@ -65,7 +66,7 @@ import com.slack.circuit.runtime.Screen import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.compose.stringResource -@OptIn(ExperimentalComposeUiApi::class) +@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) @Composable internal fun Home( backstack: SaveableBackStack, @@ -202,6 +203,7 @@ internal fun HomeNavigationDrawer( .widthIn(max = 280.dp), ) { for (item in HomeNavigationItems) { + @OptIn(ExperimentalMaterial3Api::class) NavigationDrawerItem( icon = { Icon( diff --git a/android-app/app/src/main/java/app/tivi/home/MainActivity.kt b/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt similarity index 70% rename from android-app/app/src/main/java/app/tivi/home/MainActivity.kt rename to android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt index 6436cac535..20e33e95c8 100644 --- a/android-app/app/src/main/java/app/tivi/home/MainActivity.kt +++ b/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt @@ -4,7 +4,6 @@ package app.tivi.home import android.app.Activity -import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity @@ -35,7 +34,6 @@ import app.tivi.common.compose.theme.TiviTheme import app.tivi.core.analytics.Analytics import app.tivi.data.traktauth.LoginToTraktInteractor import app.tivi.data.traktauth.TraktAuthActivityComponent -import app.tivi.extensions.unsafeLazy import app.tivi.inject.ActivityComponent import app.tivi.inject.ActivityScope import app.tivi.inject.AndroidApplicationComponent @@ -47,6 +45,8 @@ import app.tivi.settings.SettingsActivity import app.tivi.settings.TiviPreferences import app.tivi.util.TiviDateFormatter import app.tivi.util.TiviTextCreator +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.LocalImageLoader import com.slack.circuit.backstack.SaveableBackStack import com.slack.circuit.backstack.rememberSaveableBackStack import com.slack.circuit.foundation.CircuitCompositionLocals @@ -69,9 +69,6 @@ class MainActivity : TiviActivity() { } } - private val preferences: TiviPreferences by unsafeLazy { component.preferences } - private val analytics: Analytics by unsafeLazy { component.analytics } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) component = MainActivityComponent::class.create(this) @@ -83,7 +80,21 @@ class MainActivity : TiviActivity() { val composeView = ComposeView(this).apply { setContent { - TiviContent() + CompositionLocalProvider( + LocalImageLoader provides component.imageLoader, + LocalTiviDateFormatter provides component.tiviDateFormatter, + LocalTiviTextCreator provides component.textCreator, + ) { + CircuitCompositionLocals(component.circuitConfig) { + TiviContent( + analytics = component.analytics, + preferences = component.preferences, + onOpenSettings = { + context.startActivity(Intent(context, SettingsActivity::class.java)) + }, + ) + } + } } } @@ -95,58 +106,52 @@ class MainActivity : TiviActivity() { component.contentViewSetter.setContentView(this, composeView) } +} - @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) - @Composable - private fun TiviContent() { - CircuitCompositionLocals(component.circuitConfig) { - val backstack: SaveableBackStack = rememberSaveableBackStack { push(DiscoverScreen) } - val circuitNavigator = rememberCircuitNavigator(backstack) - - val navigator: Navigator = remember(circuitNavigator) { - TiviNavigator(context = this, navigator = circuitNavigator) - } +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +private fun TiviContent( + onOpenSettings: () -> Unit, + analytics: Analytics, + preferences: TiviPreferences, +) { + val backstack: SaveableBackStack = rememberSaveableBackStack { push(DiscoverScreen) } + val circuitNavigator = rememberCircuitNavigator(backstack) + + val navigator: Navigator = remember(circuitNavigator) { + TiviNavigator(circuitNavigator, onOpenSettings) + } - // Launch an effect to track changes to the current back stack entry, and push them - // as a screen views to analytics - LaunchedEffect(backstack.topRecord) { - val topScreen = backstack.topRecord?.screen as? TiviScreen - analytics.trackScreenView( - name = topScreen?.name ?: "unknown screen", - arguments = topScreen?.arguments, - ) - } + // Launch an effect to track changes to the current back stack entry, and push them + // as a screen views to analytics + LaunchedEffect(backstack.topRecord) { + val topScreen = backstack.topRecord?.screen as? TiviScreen + analytics.trackScreenView( + name = topScreen?.name ?: "unknown screen", + arguments = topScreen?.arguments, + ) + } - CompositionLocalProvider( - LocalTiviDateFormatter provides component.tiviDateFormatter, - LocalTiviTextCreator provides component.textCreator, - LocalNavigator provides navigator, - LocalWindowSizeClass provides calculateWindowSizeClass(this@MainActivity), - ) { - TiviTheme( - useDarkColors = preferences.shouldUseDarkColors(), - useDynamicColors = preferences.shouldUseDynamicColors(), - ) { - Home( - backstack = backstack, - navigator = navigator, - ) - } - } + CompositionLocalProvider( + LocalNavigator provides navigator, + LocalWindowSizeClass provides calculateWindowSizeClass(), + ) { + TiviTheme( + useDarkColors = preferences.shouldUseDarkColors(), + useDynamicColors = preferences.shouldUseDynamicColors(), + ) { + Home(backstack = backstack, navigator = navigator) } } } -internal class TiviNavigator( - private val context: Context, +private class TiviNavigator( private val navigator: Navigator, + private val onOpenSettings: () -> Unit, ) : Navigator { override fun goTo(screen: Screen) { when (screen) { - is SettingsScreen -> { - // We need to 'escape' out of Compose here and launch an activity - context.startActivity(Intent(context, SettingsActivity::class.java)) - } + is SettingsScreen -> onOpenSettings() else -> navigator.goTo(screen) } } @@ -174,6 +179,7 @@ abstract class MainActivityComponent( abstract val contentViewSetter: ContentViewSetter abstract val login: LoginToTraktInteractor abstract val circuitConfig: CircuitConfig + abstract val imageLoader: ImageLoader abstract val viewModel: () -> MainActivityViewModel } diff --git a/android-app/app/src/main/java/app/tivi/home/MainActivityViewModel.kt b/android-app/app/src/main/kotlin/app/tivi/home/MainActivityViewModel.kt similarity index 100% rename from android-app/app/src/main/java/app/tivi/home/MainActivityViewModel.kt rename to android-app/app/src/main/kotlin/app/tivi/home/MainActivityViewModel.kt diff --git a/android-app/app/src/main/java/app/tivi/inject/ActivityComponent.kt b/android-app/app/src/main/kotlin/app/tivi/inject/ActivityComponent.kt similarity index 100% rename from android-app/app/src/main/java/app/tivi/inject/ActivityComponent.kt rename to android-app/app/src/main/kotlin/app/tivi/inject/ActivityComponent.kt diff --git a/android-app/app/src/main/java/app/tivi/inject/AndroidApplicationComponent.kt b/android-app/app/src/main/kotlin/app/tivi/inject/AndroidApplicationComponent.kt similarity index 95% rename from android-app/app/src/main/java/app/tivi/inject/AndroidApplicationComponent.kt rename to android-app/app/src/main/kotlin/app/tivi/inject/AndroidApplicationComponent.kt index adef63802e..98967b569a 100644 --- a/android-app/app/src/main/java/app/tivi/inject/AndroidApplicationComponent.kt +++ b/android-app/app/src/main/kotlin/app/tivi/inject/AndroidApplicationComponent.kt @@ -5,6 +5,7 @@ package app.tivi.inject import android.app.Application import android.content.Context +import androidx.compose.ui.unit.Density import app.tivi.BuildConfig import app.tivi.TiviApplication import app.tivi.app.ApplicationInfo @@ -78,6 +79,9 @@ abstract class AndroidApplicationComponent( .writeTimeout(20, TimeUnit.SECONDS) .build() + @Provides + fun provideDensity(application: Application): Density = Density(application) + companion object { fun from(context: Context): AndroidApplicationComponent { return (context.applicationContext as TiviApplication).component diff --git a/android-app/app/src/main/java/app/tivi/inject/UiComponent.kt b/android-app/app/src/main/kotlin/app/tivi/inject/UiComponent.kt similarity index 100% rename from android-app/app/src/main/java/app/tivi/inject/UiComponent.kt rename to android-app/app/src/main/kotlin/app/tivi/inject/UiComponent.kt diff --git a/android-app/benchmark/src/main/java/app/tivi/benchmark/BaselineProfileGenerator.kt b/android-app/benchmark/src/main/kotlin/app/tivi/benchmark/BaselineProfileGenerator.kt similarity index 100% rename from android-app/benchmark/src/main/java/app/tivi/benchmark/BaselineProfileGenerator.kt rename to android-app/benchmark/src/main/kotlin/app/tivi/benchmark/BaselineProfileGenerator.kt diff --git a/android-app/benchmark/src/main/java/app/tivi/benchmark/StartupBenchmark.kt b/android-app/benchmark/src/main/kotlin/app/tivi/benchmark/StartupBenchmark.kt similarity index 100% rename from android-app/benchmark/src/main/java/app/tivi/benchmark/StartupBenchmark.kt rename to android-app/benchmark/src/main/kotlin/app/tivi/benchmark/StartupBenchmark.kt diff --git a/android-app/common-test/src/main/java/app/tivi/app/test/AppScenarios.kt b/android-app/common-test/src/main/kotlin/app/tivi/app/test/AppScenarios.kt similarity index 100% rename from android-app/common-test/src/main/java/app/tivi/app/test/AppScenarios.kt rename to android-app/common-test/src/main/kotlin/app/tivi/app/test/AppScenarios.kt diff --git a/build.gradle.kts b/build.gradle.kts index 0a31029ff0..0794aec806 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,10 +19,10 @@ plugins { alias(libs.plugins.gms.googleServices) apply false alias(libs.plugins.firebase.crashlytics) apply false alias(libs.plugins.spotless) apply false + alias(libs.plugins.composeMultiplatform) apply false } allprojects { - // Workaround for https://issuetracker.google.com/issues/268961156 tasks.withType { tasks.findByName("kspTestKotlin")?.let { diff --git a/common/imageloading/build.gradle.kts b/common/imageloading/build.gradle.kts index e6ee99f71c..90e61e566e 100644 --- a/common/imageloading/build.gradle.kts +++ b/common/imageloading/build.gradle.kts @@ -4,27 +4,32 @@ plugins { id("app.tivi.android.library") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } -android { - namespace = "app.tivi.common.imageloading" -} - -dependencies { - implementation(projects.core.base) - implementation(projects.core.logging) - implementation(projects.core.powercontroller) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.core.logging) + implementation(projects.core.powercontroller) - implementation(projects.data.models) - implementation(projects.data.episodes) - implementation(projects.data.showimages) + implementation(projects.data.models) + implementation(projects.data.episodes) + implementation(projects.data.showimages) - implementation(projects.api.tmdb) + implementation(projects.api.tmdb) - implementation(libs.androidx.core) + implementation(libs.kotlininject.runtime) - implementation(libs.kotlininject.runtime) + api(libs.imageloader) + } + } + } +} - api(libs.coil.coil) +android { + namespace = "app.tivi.common.imageloading" } diff --git a/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/AndroidImageLoaderFactory.kt b/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/AndroidImageLoaderFactory.kt new file mode 100644 index 0000000000..87e59d4c48 --- /dev/null +++ b/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/AndroidImageLoaderFactory.kt @@ -0,0 +1,39 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.imageloading + +import android.content.Context +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.ImageLoaderConfigBuilder +import com.seiko.imageloader.cache.memory.maxSizePercent +import com.seiko.imageloader.component.setupDefaultComponents +import com.seiko.imageloader.option.androidContext +import okio.Path.Companion.toOkioPath + +internal class AndroidImageLoaderFactory( + private val context: Context, +) : ImageLoaderFactory { + override fun create( + block: ImageLoaderConfigBuilder.() -> Unit, + ): ImageLoader = ImageLoader { + options { + androidContext(context.applicationContext) + } + components { + setupDefaultComponents() + } + interceptor { + memoryCacheConfig { + // Set the max size to 25% of the app's available memory. + maxSizePercent(context.applicationContext, 0.25) + } + diskCacheConfig { + directory(context.cacheDir.resolve("image_cache").toOkioPath()) + maxSizeBytes(512L * 1024 * 1024) // 512MB + } + } + + block() + } +} diff --git a/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt b/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt new file mode 100644 index 0000000000..aee64378bd --- /dev/null +++ b/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt @@ -0,0 +1,25 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.imageloading + +import android.app.Application +import app.tivi.util.Logger +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.intercept.Interceptor +import me.tatarka.inject.annotations.Provides + +actual interface ImageLoadingPlatformComponent { + @Provides + fun provideImageLoader( + application: Application, + interceptors: Set, + logger: Logger, + ): ImageLoader = AndroidImageLoaderFactory(application).create { + this.logger = logger.asImageLoaderLogger() + + interceptor { + addInterceptors(interceptors) + } + } +} diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt similarity index 67% rename from common/imageloading/src/main/java/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt rename to common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt index f4e8b78ffa..5e583878ac 100644 --- a/common/imageloading/src/main/java/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt +++ b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt @@ -3,24 +3,23 @@ package app.tivi.common.imageloading +import androidx.compose.ui.unit.Density import app.tivi.data.episodes.SeasonsEpisodesRepository import app.tivi.data.imagemodels.EpisodeImageModel import app.tivi.data.util.inPast import app.tivi.tmdb.TmdbImageUrlProvider -import coil.intercept.Interceptor -import coil.request.ImageRequest -import coil.request.ImageResult -import coil.size.Size -import coil.size.pxOrElse +import com.seiko.imageloader.intercept.Interceptor +import com.seiko.imageloader.model.ImageRequest +import com.seiko.imageloader.model.ImageResult +import kotlin.math.roundToInt import kotlin.time.Duration.Companion.days import me.tatarka.inject.annotations.Inject -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl @Inject class EpisodeCoilInterceptor( private val tmdbImageUrlProvider: Lazy, private val repository: SeasonsEpisodesRepository, + private val density: () -> Density, ) : Interceptor { override suspend fun intercept(chain: Interceptor.Chain): ImageResult { val request = when (val data = chain.request.data) { @@ -36,16 +35,16 @@ class EpisodeCoilInterceptor( } return repository.getEpisode(model.id)?.tmdbBackdropPath?.let { backdropPath -> - chain.request.newBuilder() - .data(map(backdropPath, chain.size)) - .build() - } ?: chain.request - } + val size = chain.options.sizeResolver.run { density().size() } - private fun map(backdropPath: String, size: Size): HttpUrl { - return tmdbImageUrlProvider.value.getBackdropUrl( - path = backdropPath, - imageWidth = size.width.pxOrElse { 0 }, - ).toHttpUrl() + chain.request.newBuilder { + data( + tmdbImageUrlProvider.value.getBackdropUrl( + path = backdropPath, + imageWidth = size.width.roundToInt(), + ), + ) + } + } ?: chain.request } } diff --git a/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoaderFactory.kt b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoaderFactory.kt new file mode 100644 index 0000000000..b69ce21df6 --- /dev/null +++ b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoaderFactory.kt @@ -0,0 +1,38 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.imageloading + +import app.tivi.util.Logger +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.ImageLoaderConfigBuilder +import com.seiko.imageloader.util.LogPriority + +internal fun interface ImageLoaderFactory { + fun create( + block: ImageLoaderConfigBuilder.() -> Unit, + ): ImageLoader +} + +internal fun Logger.asImageLoaderLogger(): com.seiko.imageloader.util.Logger { + return object : com.seiko.imageloader.util.Logger { + override fun isLoggable(priority: LogPriority): Boolean = true + + override fun log( + priority: LogPriority, + tag: String, + data: Any?, + throwable: Throwable?, + message: String, + ) { + when (priority) { + LogPriority.VERBOSE -> v(throwable) { message } + LogPriority.DEBUG -> d(throwable) { message } + LogPriority.INFO -> i(throwable) { message } + LogPriority.WARN -> i(throwable) { message } + LogPriority.ERROR -> e(throwable) { message } + LogPriority.ASSERT -> e(throwable) { message } + } + } + } +} diff --git a/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoadingComponent.kt b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoadingComponent.kt new file mode 100644 index 0000000000..351939245b --- /dev/null +++ b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoadingComponent.kt @@ -0,0 +1,28 @@ +// Copyright 2019, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.imageloading + +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.intercept.Interceptor +import me.tatarka.inject.annotations.IntoSet +import me.tatarka.inject.annotations.Provides + +expect interface ImageLoadingPlatformComponent + +interface ImageLoadingComponent : ImageLoadingPlatformComponent { + + val imageLoader: ImageLoader + + @Provides + @IntoSet + fun provideShowCoilInterceptor(interceptor: ShowCoilInterceptor): Interceptor = interceptor + + @Provides + @IntoSet + fun provideTmdbImageEntityCoilInterceptor(interceptor: TmdbImageEntityCoilInterceptor): Interceptor = interceptor + + @Provides + @IntoSet + fun provideEpisodeCoilInterceptor(interceptor: EpisodeCoilInterceptor): Interceptor = interceptor +} diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/ShowCoilInterceptor.kt b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ShowCoilInterceptor.kt similarity index 77% rename from common/imageloading/src/main/java/app/tivi/common/imageloading/ShowCoilInterceptor.kt rename to common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ShowCoilInterceptor.kt index 5e6f1a47ae..1a3c0ded2c 100644 --- a/common/imageloading/src/main/java/app/tivi/common/imageloading/ShowCoilInterceptor.kt +++ b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ShowCoilInterceptor.kt @@ -3,6 +3,7 @@ package app.tivi.common.imageloading +import androidx.compose.ui.unit.Density import app.tivi.data.imagemodels.ShowImageModel import app.tivi.data.models.ImageType import app.tivi.data.models.TmdbImageEntity @@ -10,10 +11,10 @@ import app.tivi.data.showimages.ShowImagesStore import app.tivi.tmdb.TmdbImageUrlProvider import app.tivi.util.PowerController import app.tivi.util.SaveData -import coil.intercept.Interceptor -import coil.request.ImageRequest -import coil.request.ImageResult -import coil.size.pxOrElse +import com.seiko.imageloader.intercept.Interceptor +import com.seiko.imageloader.model.ImageRequest +import com.seiko.imageloader.model.ImageResult +import kotlin.math.roundToInt import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.impl.extensions.get @@ -22,6 +23,7 @@ class ShowCoilInterceptor( private val tmdbImageUrlProvider: Lazy, private val showImagesStore: ShowImagesStore, private val powerController: PowerController, + private val density: () -> Density, ) : Interceptor { override suspend fun intercept(chain: Interceptor.Chain): ImageResult { val request = when (val data = chain.request.data) { @@ -40,15 +42,17 @@ class ShowCoilInterceptor( }.getOrNull() return if (entity != null) { + val size = chain.options.sizeResolver.run { density().size() } + val width = when (powerController.shouldSaveData()) { - is SaveData.Disabled -> chain.size.width.pxOrElse { 0 } + is SaveData.Disabled -> size.width.roundToInt() // If we can't download hi-res images, we load half-width images (so ~1/4 in size) - is SaveData.Enabled -> chain.size.width.pxOrElse { 0 } / 2 + is SaveData.Enabled -> size.width.roundToInt() / 2 } - chain.request.newBuilder() - .data(tmdbImageUrlProvider.value.buildUrl(entity, model.imageType, width)) - .build() + chain.request.newBuilder { + data(tmdbImageUrlProvider.value.buildUrl(entity, model.imageType, width)) + } } else { chain.request } diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt similarity index 67% rename from common/imageloading/src/main/java/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt rename to common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt index 84c7afcc89..b1346e5331 100644 --- a/common/imageloading/src/main/java/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt +++ b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt @@ -3,39 +3,44 @@ package app.tivi.common.imageloading +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.Density import app.tivi.data.models.TmdbImageEntity import app.tivi.tmdb.TmdbImageUrlProvider import app.tivi.util.PowerController import app.tivi.util.SaveData -import coil.intercept.Interceptor -import coil.request.ImageResult -import coil.size.Size -import coil.size.pxOrElse +import com.seiko.imageloader.intercept.Interceptor +import com.seiko.imageloader.model.ImageResult +import kotlin.math.roundToInt import me.tatarka.inject.annotations.Inject @Inject class TmdbImageEntityCoilInterceptor( private val tmdbImageUrlProvider: Lazy, private val powerController: PowerController, + private val density: () -> Density, ) : Interceptor { override suspend fun intercept(chain: Interceptor.Chain): ImageResult { + val size = chain.options.sizeResolver.run { density().size() } + val request = when (val data = chain.request.data) { is TmdbImageEntity -> { - chain.request.newBuilder() - .data(map(data, chain.size)) - .build() + chain.request.newBuilder { + data(map(data, size)) + } } else -> chain.request } + return chain.proceed(request) } private fun map(data: TmdbImageEntity, size: Size): String { val width = when (powerController.shouldSaveData()) { - is SaveData.Disabled -> size.width.pxOrElse { 0 } + is SaveData.Disabled -> size.width.roundToInt() // If we can't download hi-res images, we load half-width images (so ~1/4 in size) - is SaveData.Enabled -> size.width.pxOrElse { 0 } / 2 + is SaveData.Enabled -> size.width.roundToInt() / 2 } return tmdbImageUrlProvider.value.buildUrl(data, data.type, width) } diff --git a/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt b/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt new file mode 100644 index 0000000000..f8681eef22 --- /dev/null +++ b/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt @@ -0,0 +1,22 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.imageloading + +import app.tivi.util.Logger +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.intercept.Interceptor +import me.tatarka.inject.annotations.Provides + +actual interface ImageLoadingPlatformComponent { + @Provides + fun provideImageLoader( + interceptors: Set, + logger: Logger, + ): ImageLoader = IosImageLoaderFactory.create { + this.logger = logger.asImageLoaderLogger() + interceptor { + addInterceptors(interceptors) + } + } +} diff --git a/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/IosImageLoaderFactory.kt b/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/IosImageLoaderFactory.kt new file mode 100644 index 0000000000..8dcfe52aa7 --- /dev/null +++ b/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/IosImageLoaderFactory.kt @@ -0,0 +1,48 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.imageloading + +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.ImageLoaderConfigBuilder +import com.seiko.imageloader.cache.memory.maxSizePercent +import com.seiko.imageloader.component.setupDefaultComponents +import okio.Path +import okio.Path.Companion.toPath +import platform.Foundation.NSCachesDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSUserDomainMask + +internal object IosImageLoaderFactory : ImageLoaderFactory { + private val cacheDir: Path by lazy { + NSFileManager.defaultManager.URLForDirectory( + directory = NSCachesDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = true, + error = null, + )!!.path.orEmpty().toPath() + } + + override fun create( + block: ImageLoaderConfigBuilder.() -> Unit, + ): ImageLoader = ImageLoader { + // commonConfig() + + components { + setupDefaultComponents(imageScope) + } + interceptor { + memoryCacheConfig { + // Set the max size to 25% of the app's available memory. + maxSizePercent(0.25) + } + diskCacheConfig { + directory(cacheDir.resolve("image_cache")) + maxSizeBytes(512L * 1024 * 1024) // 512MB + } + } + + block() + } +} diff --git a/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/DesktopImageLoaderFactory.kt b/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/DesktopImageLoaderFactory.kt new file mode 100644 index 0000000000..3547c65301 --- /dev/null +++ b/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/DesktopImageLoaderFactory.kt @@ -0,0 +1,58 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.imageloading + +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.ImageLoaderConfigBuilder +import com.seiko.imageloader.cache.memory.maxSizePercent +import com.seiko.imageloader.component.setupDefaultComponents +import java.io.File +import okio.Path.Companion.toOkioPath + +internal object DesktopImageLoaderFactory : ImageLoaderFactory { + private fun getCacheDir(): File = when (currentOperatingSystem) { + OperatingSystem.Windows -> File(System.getenv("AppData"), "tivi/cache") + OperatingSystem.Linux -> File(System.getProperty("user.home"), ".cache/tivi") + OperatingSystem.MacOS -> File(System.getProperty("user.home"), "Library/Caches/tivi") + else -> throw IllegalStateException("Unsupported operating system") + } + + override fun create( + block: ImageLoaderConfigBuilder.() -> Unit, + ): ImageLoader = ImageLoader { + components { + setupDefaultComponents(imageScope) + } + interceptor { + memoryCacheConfig { + // Set the max size to 25% of the app's available memory. + maxSizePercent(0.25) + } + diskCacheConfig { + directory(getCacheDir().resolve("image_cache").toOkioPath()) + maxSizeBytes(512L * 1024 * 1024) // 512MB + } + } + + block() + } +} + +internal enum class OperatingSystem { + Windows, Linux, MacOS, Unknown +} + +private val currentOperatingSystem: OperatingSystem + get() { + val os = System.getProperty("os.name").lowercase() + return when { + os.contains("win") -> OperatingSystem.Windows + os.contains("nix") || os.contains("nux") || os.contains("aix") -> { + OperatingSystem.Linux + } + + os.contains("mac") -> OperatingSystem.MacOS + else -> OperatingSystem.Unknown + } + } diff --git a/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt b/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt new file mode 100644 index 0000000000..6665536301 --- /dev/null +++ b/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt @@ -0,0 +1,22 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.imageloading + +import app.tivi.util.Logger +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.intercept.Interceptor +import me.tatarka.inject.annotations.Provides + +actual interface ImageLoadingPlatformComponent { + @Provides + fun provideImageLoader( + interceptors: Set, + logger: Logger, + ): ImageLoader = DesktopImageLoaderFactory.create { + this.logger = logger.asImageLoaderLogger() + interceptor { + addInterceptors(interceptors) + } + } +} diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/CoilAppInitializer.kt b/common/imageloading/src/main/java/app/tivi/common/imageloading/CoilAppInitializer.kt deleted file mode 100644 index ce1955174f..0000000000 --- a/common/imageloading/src/main/java/app/tivi/common/imageloading/CoilAppInitializer.kt +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2019, Google LLC, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.common.imageloading - -import android.app.Application -import app.tivi.appinitializers.AppInitializer -import coil.Coil -import coil.ImageLoader -import me.tatarka.inject.annotations.Inject -import okhttp3.OkHttpClient - -@Inject -class CoilAppInitializer( - private val application: Application, - private val showImageInterceptor: ShowCoilInterceptor, - private val episodeEntityInterceptor: EpisodeCoilInterceptor, - private val tmdbImageEntityInterceptor: TmdbImageEntityCoilInterceptor, - private val okHttpClient: OkHttpClient, -) : AppInitializer { - override fun init() { - Coil.setImageLoader { - ImageLoader.Builder(application) - .components { - add(showImageInterceptor) - add(episodeEntityInterceptor) - add(tmdbImageEntityInterceptor) - } - .okHttpClient(okHttpClient) - .build() - } - } -} diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/ImageLoadingComponent.kt b/common/imageloading/src/main/java/app/tivi/common/imageloading/ImageLoadingComponent.kt deleted file mode 100644 index 7682cc33e4..0000000000 --- a/common/imageloading/src/main/java/app/tivi/common/imageloading/ImageLoadingComponent.kt +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2019, Google LLC, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.common.imageloading - -import app.tivi.appinitializers.AppInitializer -import me.tatarka.inject.annotations.IntoSet -import me.tatarka.inject.annotations.Provides - -interface ImageLoadingComponent { - @Provides - @IntoSet - fun provideCoilInitializer(bind: CoilAppInitializer): AppInitializer = bind -} diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/TrimTransparentEdgesTransformation.kt b/common/imageloading/src/main/java/app/tivi/common/imageloading/TrimTransparentEdgesTransformation.kt deleted file mode 100644 index b2b4537b4e..0000000000 --- a/common/imageloading/src/main/java/app/tivi/common/imageloading/TrimTransparentEdgesTransformation.kt +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2019, Google LLC, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.common.imageloading - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Rect -import androidx.core.graphics.alpha -import androidx.core.graphics.createBitmap -import coil.size.Size -import coil.transform.Transformation - -/** - * A [Transformation] that trims transparent edges from an image. - */ -object TrimTransparentEdgesTransformation : Transformation { - override val cacheKey: String = "TrimTransparentEdgesTransformation" - - override suspend fun transform(input: Bitmap, size: Size): Bitmap { - val inputWidth = input.width - val inputHeight = input.height - - var firstX = 0 - var firstY = 0 - var lastX = inputWidth - var lastY = inputHeight - - val pixels = IntArray(inputWidth * inputHeight) - input.getPixels(pixels, 0, inputWidth, 0, 0, inputWidth, inputHeight) - - loop@ - for (x in 0 until inputWidth) { - for (y in 0 until inputHeight) { - if (pixels[x + y * inputWidth].alpha > 0) { - firstX = x - break@loop - } - } - } - - loop@ - for (y in 0 until inputHeight) { - for (x in firstX until inputWidth) { - if (pixels[x + y * inputWidth].alpha > 0) { - firstY = y - break@loop - } - } - } - - loop@ - for (x in inputWidth - 1 downTo firstX) { - for (y in inputHeight - 1 downTo firstY) { - if (pixels[x + y * inputWidth].alpha > 0) { - lastX = x - break@loop - } - } - } - - loop@ - for (y in inputHeight - 1 downTo firstY) { - for (x in inputWidth - 1 downTo firstX) { - if (pixels[x + y * inputWidth].alpha > 0) { - lastY = y - break@loop - } - } - } - - if (firstX == 0 && firstY == 0 && lastX == inputWidth && lastY == inputHeight) { - return input - } - - val output = createBitmap( - width = 1 + lastX - firstX, - height = 1 + lastY - firstY, - config = Bitmap.Config.ARGB_8888, - ) - val canvas = Canvas(output) - - val src = Rect(firstX, firstY, firstX + output.width, firstY + output.height) - val dst = Rect(0, 0, output.width, output.height) - - canvas.drawBitmap(input, src, dst, null) - - return output - } -} diff --git a/common/ui/circuit-overlay/build.gradle.kts b/common/ui/circuit-overlay/build.gradle.kts index eee1660dcf..6c900c5c4e 100644 --- a/common/ui/circuit-overlay/build.gradle.kts +++ b/common/ui/circuit-overlay/build.gradle.kts @@ -4,24 +4,29 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.ui.overlays" } -dependencies { - implementation(projects.common.ui.compose) - implementation(projects.common.ui.screens) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.common.ui.compose) + implementation(projects.common.ui.screens) - implementation(platform(libs.compose.bom)) - implementation(libs.compose.material3.material3) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) - implementation(libs.androidx.activity.compose) + implementation(compose.material3) + implementation(compose.animation) - api(libs.circuit.foundation) - api(libs.circuit.overlay) + implementation(libs.materialdialogs.core) + + api(libs.circuit.foundation) + api(libs.circuit.overlay) + } + } + } } diff --git a/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/BottomSheetOverlay.kt b/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/BottomSheetOverlay.kt new file mode 100644 index 0000000000..d72e76421f --- /dev/null +++ b/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/BottomSheetOverlay.kt @@ -0,0 +1,62 @@ +// Copyright (C) 2022 Slack Technologies, LLC +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.overlays + +// @OptIn(ExperimentalMaterial3Api::class) +// class BottomSheetOverlay( +// private val model: Model, +// private val onDismiss: () -> Result, +// private val tonalElevation: Dp = BottomSheetDefaults.Elevation, +// private val scrimColor: Color = Color.Unspecified, +// private val content: @Composable (Model, OverlayNavigator) -> Unit, +// ) : Overlay { +// @Composable +// override fun Content(navigator: OverlayNavigator) { +// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) +// +// val coroutineScope = rememberCoroutineScope() +// BackHandler(enabled = sheetState.isVisible) { +// coroutineScope +// .launch { sheetState.hide() } +// .invokeOnCompletion { +// if (!sheetState.isVisible) { +// navigator.finish(onDismiss()) +// } +// } +// } +// +// ModalBottomSheet( +// modifier = Modifier.fillMaxWidth(), +// content = { +// // Delay setting the result until we've finished dismissing +// content(model) { result -> +// // This is the OverlayNavigator.finish() callback +// coroutineScope.launch { +// try { +// sheetState.hide() +// } finally { +// navigator.finish(result) +// } +// } +// } +// }, +// tonalElevation = tonalElevation, +// scrimColor = if (scrimColor.isSpecified) scrimColor else BottomSheetDefaults.ScrimColor, +// sheetState = sheetState, +// onDismissRequest = { navigator.finish(onDismiss()) }, +// ) +// +// LaunchedEffect(Unit) { sheetState.show() } +// } +// } +// +// suspend fun OverlayHost.showInBottomSheet( +// screen: Screen, +// ): Unit = show( +// BottomSheetOverlay(Unit, {}) { _, _ -> +// // We want to use `onNavEvent` here to finish the overlay but we're blocked by +// // https://github.com/slackhq/circuit/issues/653 +// CircuitContent(screen = screen) +// }, +// ) diff --git a/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/DialogOverlay.kt b/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/DialogOverlay.kt similarity index 58% rename from common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/DialogOverlay.kt rename to common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/DialogOverlay.kt index 9bb80e863c..8f82e30495 100644 --- a/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/DialogOverlay.kt +++ b/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/DialogOverlay.kt @@ -3,18 +3,14 @@ package app.tivi.overlays -import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import app.tivi.common.compose.rememberCoroutineScope -import app.tivi.common.compose.ui.androidMinWidthDialogSize import com.slack.circuit.foundation.CircuitContent import com.slack.circuit.overlay.Overlay import com.slack.circuit.overlay.OverlayHost import com.slack.circuit.overlay.OverlayNavigator import com.slack.circuit.runtime.Screen +import com.vanpra.composematerialdialogs.MaterialDialog import kotlinx.coroutines.launch class DialogOverlay( @@ -25,17 +21,14 @@ class DialogOverlay( @Composable override fun Content(navigator: OverlayNavigator) { val coroutineScope = rememberCoroutineScope() - Dialog( - onDismissRequest = { navigator.finish(onDismiss()) }, - properties = DialogProperties(usePlatformDefaultWidth = false), + MaterialDialog( + onCloseRequest = { navigator.finish(onDismiss()) }, ) { - Box(Modifier.androidMinWidthDialogSize(clampMaxWidth = true)) { - // Delay setting the result until we've finished dismissing - content(model) { result -> - // This is the OverlayNavigator.finish() callback - coroutineScope.launch { - navigator.finish(result) - } + // Delay setting the result until we've finished dismissing + content(model) { result -> + // This is the OverlayNavigator.finish() callback + coroutineScope.launch { + navigator.finish(result) } } } diff --git a/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/LocalNavigator.kt b/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/LocalNavigator.kt similarity index 100% rename from common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/LocalNavigator.kt rename to common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/LocalNavigator.kt diff --git a/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/BottomSheetOverlay.kt b/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/BottomSheetOverlay.kt deleted file mode 100644 index 2580463cc9..0000000000 --- a/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/BottomSheetOverlay.kt +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (C) 2022 Slack Technologies, LLC -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.overlays - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.isSpecified -import androidx.compose.ui.unit.Dp -import app.tivi.common.compose.rememberCoroutineScope -import com.slack.circuit.foundation.CircuitContent -import com.slack.circuit.overlay.Overlay -import com.slack.circuit.overlay.OverlayHost -import com.slack.circuit.overlay.OverlayNavigator -import com.slack.circuit.runtime.Screen -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class) -class BottomSheetOverlay( - private val model: Model, - private val onDismiss: () -> Result, - private val tonalElevation: Dp = BottomSheetDefaults.Elevation, - private val scrimColor: Color = Color.Unspecified, - private val content: @Composable (Model, OverlayNavigator) -> Unit, -) : Overlay { - @Composable - override fun Content(navigator: OverlayNavigator) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - val coroutineScope = rememberCoroutineScope() - BackHandler(enabled = sheetState.isVisible) { - coroutineScope - .launch { sheetState.hide() } - .invokeOnCompletion { - if (!sheetState.isVisible) { - navigator.finish(onDismiss()) - } - } - } - - ModalBottomSheet( - modifier = Modifier.fillMaxWidth(), - content = { - // Delay setting the result until we've finished dismissing - content(model) { result -> - // This is the OverlayNavigator.finish() callback - coroutineScope.launch { - try { - sheetState.hide() - } finally { - navigator.finish(result) - } - } - } - }, - tonalElevation = tonalElevation, - scrimColor = if (scrimColor.isSpecified) scrimColor else BottomSheetDefaults.ScrimColor, - sheetState = sheetState, - onDismissRequest = { navigator.finish(onDismiss()) }, - ) - - LaunchedEffect(Unit) { sheetState.show() } - } -} - -suspend fun OverlayHost.showInBottomSheet( - screen: Screen, -): Unit = show( - BottomSheetOverlay(Unit, {}) { _, _ -> - // We want to use `onNavEvent` here to finish the overlay but we're blocked by - // https://github.com/slackhq/circuit/issues/653 - CircuitContent(screen = screen) - }, -) diff --git a/common/ui/compose/build.gradle.kts b/common/ui/compose/build.gradle.kts index 7dd719cb77..e128728d4c 100644 --- a/common/ui/compose/build.gradle.kts +++ b/common/ui/compose/build.gradle.kts @@ -4,47 +4,49 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + api(projects.data.models) + api(projects.core.preferences) + api(projects.common.imageloading) + + api(projects.common.ui.screens) + api(libs.circuit.foundation) + + api(projects.common.ui.resources) + api(libs.moko.resourcesCompose) + + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.materialIconsExtended) + api(compose.material3) + api(libs.compose.material3.windowsizeclass) + implementation(compose.animation) + + api(libs.insetsx) + + implementation(libs.paging.compose) + } + } + + val androidMain by getting { + dependencies { + implementation(libs.androidx.activity.compose) + } + } + } } android { namespace = "app.tivi.common.compose" - buildFeatures { - buildConfig = true - } - lint { baseline = file("lint-baseline.xml") } } - -dependencies { - api(projects.data.models) - api(projects.core.preferences) - api(projects.common.imageloading) - - api(projects.common.ui.screens) - api(libs.circuit.foundation) - - api(projects.common.ui.resources) - api(projects.common.ui.resourcesCompose) - - implementation(libs.androidx.core) - - api(platform(libs.compose.bom)) - implementation(libs.compose.ui.ui) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material.iconsext) - api(libs.compose.material3.material3) - api(libs.compose.material3.windowsizeclass) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) - - implementation(libs.paging.compose) - - implementation(libs.coil.compose) -} diff --git a/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt b/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt new file mode 100644 index 0000000000..e0547c7d0a --- /dev/null +++ b/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt @@ -0,0 +1,11 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose + +import androidx.compose.runtime.Composable + +@Composable +actual fun ReportDrawnWhen(predicate: () -> Boolean) { + androidx.activity.compose.ReportDrawnWhen(predicate) +} diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/EntryGrid.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/EntryGrid.kt similarity index 94% rename from common/ui/compose/src/main/java/app/tivi/common/compose/EntryGrid.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/EntryGrid.kt index ee22ddccbf..02c3c6a8b9 100644 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/EntryGrid.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/EntryGrid.kt @@ -14,14 +14,16 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material.DismissValue import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.rememberDismissState import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DismissValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -29,12 +31,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -42,7 +42,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp -import androidx.paging.LoadState +import app.cash.paging.LoadState +import app.cash.paging.LoadStateLoading import app.cash.paging.compose.LazyPagingItems import app.tivi.common.compose.ui.PlaceholderPosterCard import app.tivi.common.compose.ui.PosterCard @@ -66,16 +67,14 @@ fun EntryGrid( val snackbarHostState = remember { SnackbarHostState() } val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() - val dismissSnackbarState = rememberDismissState( - confirmValueChange = { value -> - if (value != DismissValue.Default) { - snackbarHostState.currentSnackbarData?.dismiss() - true - } else { - false - } - }, - ) + val dismissSnackbarState = rememberDismissState { value -> + if (value != DismissValue.Default) { + snackbarHostState.currentSnackbarData?.dismiss() + true + } else { + false + } + } lazyPagingItems.loadState.prependErrorOrNull()?.let { message -> LaunchedEffect(message) { @@ -98,7 +97,7 @@ fun EntryGrid( EntryGridAppBar( title = title, onNavigateUp = onNavigateUp, - refreshing = lazyPagingItems.loadState.refresh == LoadState.Loading, + refreshing = lazyPagingItems.loadState.refresh == LoadStateLoading, onRefreshActionClick = { lazyPagingItems.refresh() }, modifier = Modifier.fillMaxWidth(), scrollBehavior = scrollBehavior, @@ -118,7 +117,7 @@ fun EntryGrid( }, modifier = modifier, ) { paddingValues -> - val refreshing = lazyPagingItems.loadState.refresh == LoadState.Loading + val refreshing = lazyPagingItems.loadState.refresh == LoadStateLoading val refreshState = rememberPullRefreshState( refreshing = refreshing, onRefresh = lazyPagingItems::refresh, @@ -159,7 +158,7 @@ fun EntryGrid( } } - if (lazyPagingItems.loadState.append == LoadState.Loading) { + if (lazyPagingItems.loadState.append == LoadStateLoading) { fullSpanItem { Box( Modifier diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/Layout.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/Layout.kt similarity index 96% rename from common/ui/compose/src/main/java/app/tivi/common/compose/Layout.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/Layout.kt index b5f41b7409..fcb839bffc 100644 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/Layout.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/Layout.kt @@ -9,13 +9,13 @@ import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.moriatsushi.insetsx.systemBars object Layout { val bodyMargin: Dp diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/LazyList.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/LazyList.kt similarity index 88% rename from common/ui/compose/src/main/java/app/tivi/common/compose/LazyList.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/LazyList.kt index 00a393aa47..ddea60e8d4 100644 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/LazyList.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/LazyList.kt @@ -5,9 +5,6 @@ package app.tivi.common.compose -import android.annotation.SuppressLint -import android.os.Parcel -import android.os.Parcelable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -107,23 +104,7 @@ fun LazyGridScope.items( } } -@SuppressLint("BanParcelableUsage") -internal data class PagingPlaceholderKey(private val index: Int) : Parcelable { - override fun writeToParcel(parcel: Parcel, flags: Int) = parcel.writeInt(index) - override fun describeContents(): Int = 0 - - companion object { - @Suppress("unused") - @JvmField - val CREATOR: Parcelable.Creator = - object : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel) = - PagingPlaceholderKey(parcel.readInt()) - - override fun newArray(size: Int) = arrayOfNulls(size) - } - } -} +internal data class PagingPlaceholderKey(private val index: Int) inline fun LazyGridScope.fullSpanItem( key: Any? = null, diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/LazyPagingExtensions.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/LazyPagingExtensions.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/LazyPagingExtensions.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/LazyPagingExtensions.kt diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt new file mode 100644 index 0000000000..0c7d89e67b --- /dev/null +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt @@ -0,0 +1,9 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose + +import androidx.compose.runtime.Composable + +@Composable +expect fun ReportDrawnWhen(predicate: () -> Boolean) diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/StableCoroutineScope.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/StableCoroutineScope.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/StableCoroutineScope.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/StableCoroutineScope.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/TiviCompositionLocals.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/TiviCompositionLocals.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/TiviCompositionLocals.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/TiviCompositionLocals.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/TiviPreferenceExtensions.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/TiviPreferenceExtensions.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/TiviPreferenceExtensions.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/TiviPreferenceExtensions.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/UiMessage.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/UiMessage.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/UiMessage.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/UiMessage.kt diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/WindowSizeClass.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/WindowSizeClass.kt new file mode 100644 index 0000000000..bc24f9c75d --- /dev/null +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/WindowSizeClass.kt @@ -0,0 +1,11 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose + +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.staticCompositionLocalOf + +val LocalWindowSizeClass = staticCompositionLocalOf { + error("No WindowSizeClass available") +} diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/theme/Color.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Color.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/theme/Color.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Color.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/theme/Shape.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Shape.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/theme/Shape.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Shape.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/theme/Theme.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Theme.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/theme/Theme.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Theme.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/theme/Type.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Type.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/theme/Type.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Type.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AppBar.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AppBar.kt similarity index 98% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/AppBar.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AppBar.kt index 3dce9f5a94..95fdfb6406 100644 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AppBar.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AppBar.kt @@ -51,7 +51,7 @@ fun TopAppBarWithBottomContent( title = title, navigationIcon = navigationIcon, actions = actions, - colors = TopAppBarDefaults.topAppBarColors( + colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = Color.Transparent, titleContentColor = LocalContentColor.current, actionIconContentColor = LocalContentColor.current, diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt similarity index 64% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt index 99522e5d7d..dadcccf7fe 100644 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt @@ -4,16 +4,12 @@ package app.tivi.common.compose.ui import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.coerceAtLeast import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min @@ -49,31 +45,3 @@ private val DefaultDiameter = 40.dp private val InternalPadding = 4.dp private val StrokeDiameterFraction = DefaultStrokeWidth / DefaultDiameter - -@Preview -@Composable -fun PreviewAutoSizedCircularProgressIndicator() { - Surface { - Column { - AutoSizedCircularProgressIndicator( - modifier = Modifier.size(16.dp), - ) - - AutoSizedCircularProgressIndicator( - modifier = Modifier.size(24.dp), - ) - - AutoSizedCircularProgressIndicator( - modifier = Modifier.size(48.dp), - ) - - AutoSizedCircularProgressIndicator( - modifier = Modifier.size(64.dp), - ) - - AutoSizedCircularProgressIndicator( - modifier = Modifier.size(128.dp), - ) - } - } -} diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Backdrop.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Backdrop.kt similarity index 98% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/Backdrop.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Backdrop.kt index 6f6b127a43..cf09cd326b 100644 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Backdrop.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Backdrop.kt @@ -41,7 +41,6 @@ fun Backdrop( if (imageModel != null) { AsyncImage( model = imageModel, - requestBuilder = { crossfade(true) }, contentDescription = stringResource(MR.strings.cd_show_poster), contentScale = ContentScale.Crop, modifier = Modifier diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/DateTimeTextFields.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt similarity index 61% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/DateTimeTextFields.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt index 6fd1980352..e567900876 100644 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/DateTimeTextFields.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt @@ -8,18 +8,12 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.DatePicker -import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TextFieldDefaults -import androidx.compose.material3.TimePicker -import androidx.compose.material3.rememberDatePickerState -import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -33,15 +27,14 @@ import androidx.compose.ui.unit.dp import app.tivi.common.compose.LocalTiviDateFormatter import app.tivi.common.ui.resources.MR import dev.icerock.moko.resources.compose.stringResource -import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant -import kotlinx.datetime.toLocalDateTime @OptIn(ExperimentalMaterial3Api::class) +@Suppress("UNUSED_PARAMETER") @Composable fun DateTextField( selectedDate: LocalDate?, @@ -64,39 +57,39 @@ fun DateTextField( ) if (showPicker) { - val datePickerState = rememberDatePickerState( - initialSelectedDateMillis = selectedDate?.let { date -> - remember { date.toEpochMillis() } - }, - ) - - DatePickerDialog( - onDismissRequest = { showPicker = false }, - confirmButton = { - TextButton( - onClick = { - showPicker = false - - datePickerState.selectedDateMillis?.let { millis -> - val date = Instant.fromEpochMilliseconds(millis) - .toLocalDateTime(TimeZone.currentSystemDefault()) - .date - onDateSelected(date) - } - }, - ) { - Text("Confirm") - } - }, - ) { - DatePicker( - state = datePickerState, - dateValidator = { epoch -> - // Only allow dates in the past - epoch < System.currentTimeMillis() - }, - ) - } +// val datePickerState = rememberDatePickerState( +// initialSelectedDateMillis = selectedDate?.let { date -> +// remember { date.toEpochMillis() } +// }, +// ) +// +// DatePickerDialog( +// onDismissRequest = { showPicker = false }, +// confirmButton = { +// TextButton( +// onClick = { +// showPicker = false +// +// datePickerState.selectedDateMillis?.let { millis -> +// val date = Instant.fromEpochMilliseconds(millis) +// .toLocalDateTime(TimeZone.currentSystemDefault()) +// .date +// onDateSelected(date) +// } +// }, +// ) { +// Text("Confirm") +// } +// }, +// ) { +// DatePicker( +// state = datePickerState, +// dateValidator = { epoch -> +// // Only allow dates in the past +// epoch < System.currentTimeMillis() +// }, +// ) +// } } } } @@ -110,6 +103,7 @@ private fun LocalDate.toEpochMillis(): Long { private val midday: LocalTime = LocalTime(12, 0, 0, 0) @OptIn(ExperimentalMaterial3Api::class) +@Suppress("UNUSED_PARAMETER") @Composable fun TimeTextField( selectedTime: LocalTime?, @@ -133,37 +127,37 @@ fun TimeTextField( ) if (showPicker) { - val timePickerState = rememberTimePickerState( - initialHour = selectedTime?.hour ?: 0, - initialMinute = selectedTime?.minute ?: 0, - is24Hour = is24Hour, - ) - - TimePickerDialog( - onDismissRequest = { showPicker = false }, - confirmButton = { - TextButton( - onClick = { - showPicker = false - - onTimeSelected( - LocalTime( - hour = timePickerState.hour, - minute = timePickerState.minute, - second = 0, - nanosecond = 0, - ), - ) - }, - ) { - Text(text = "Confirm") - } - }, - ) { - Box(Modifier.padding(24.dp)) { - TimePicker(state = timePickerState) - } - } +// val timePickerState = rememberTimePickerState( +// initialHour = selectedTime?.hour ?: 0, +// initialMinute = selectedTime?.minute ?: 0, +// is24Hour = is24Hour, +// ) +// +// TimePickerDialog( +// onDismissRequest = { showPicker = false }, +// confirmButton = { +// TextButton( +// onClick = { +// showPicker = false +// +// onTimeSelected( +// LocalTime( +// hour = timePickerState.hour, +// minute = timePickerState.minute, +// second = 0, +// nanosecond = 0, +// ), +// ) +// }, +// ) { +// Text(text = "Confirm") +// } +// }, +// ) { +// Box(Modifier.padding(24.dp)) { +// TimePicker(state = timePickerState) +// } +// } } } } diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Empty.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Empty.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/Empty.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Empty.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/ExpandingSummary.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/ExpandingSummary.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/ExpandingSummary.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/ExpandingSummary.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/GradientScrim.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/GradientScrim.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/GradientScrim.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/GradientScrim.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/IconButtonScrim.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/IconButtonScrim.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/IconButtonScrim.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/IconButtonScrim.kt diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt new file mode 100644 index 0000000000..e00093c303 --- /dev/null +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt @@ -0,0 +1,115 @@ +// Copyright 2022, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose.ui + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.LayoutModifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import com.seiko.imageloader.ImageRequestState +import com.seiko.imageloader.model.ImageRequest +import com.seiko.imageloader.model.ImageRequestBuilder +import com.seiko.imageloader.option.SizeResolver +import com.seiko.imageloader.rememberAsyncImagePainter +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull + +@Composable +fun AsyncImage( + model: Any?, + contentDescription: String?, + modifier: Modifier = Modifier, + onState: ((ImageRequestState) -> Unit)? = null, + requestBuilder: (ImageRequestBuilder.() -> ImageRequestBuilder)? = null, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, +) { + val sizeResolver = ConstraintsSizeResolver() + + val request = ImageRequest { + data(model) + size(sizeResolver) + requestBuilder?.invoke(this) + } + + val painter = rememberAsyncImagePainter( + request = request, + contentScale = contentScale, + filterQuality = filterQuality, + ) + + val lastOnState by rememberUpdatedState(onState) + LaunchedEffect(painter) { + snapshotFlow { painter.requestState } + .collect { lastOnState?.invoke(it) } + } + + Image( + painter = painter, + alignment = alignment, + contentDescription = contentDescription, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + modifier = modifier.then(sizeResolver), + ) +} + +/** A [SizeResolver] that computes the size from the constrains passed during the layout phase. */ +internal class ConstraintsSizeResolver : SizeResolver, LayoutModifier { + + private val _constraints = MutableStateFlow(Constraints()) + + override suspend fun Density.size(): Size { + return _constraints.mapNotNull(Constraints::toSizeOrNull).first() + } + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + // Cache the current constraints. + _constraints.value = constraints + + // Measure and layout the content. + val placeable = measurable.measure(constraints) + return layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } + + fun setConstraints(constraints: Constraints) { + _constraints.value = constraints + } +} + +@Stable +private fun Constraints.toSizeOrNull() = when { + isZero -> null + else -> Size( + width = if (hasBoundedWidth) maxWidth.toFloat() else 0f, + height = if (hasBoundedHeight) maxHeight.toFloat() else 0f, + ) +} diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/LoadingButton.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/LoadingButton.kt similarity index 86% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/LoadingButton.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/LoadingButton.kt index 360fea7a28..cac5b0f1bc 100644 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/LoadingButton.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/LoadingButton.kt @@ -6,7 +6,6 @@ package app.tivi.common.compose.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding @@ -15,12 +14,10 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonElevation -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import app.tivi.common.compose.Layout @@ -59,13 +56,3 @@ fun LoadingButton( content() } } - -@Preview -@Composable -fun PreviewLoadingButton() { - Column { - LoadingButton(showProgressIndicator = true, onClick = {}) { - Text("LoadingButton") - } - } -} diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/PaddingValues.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/PaddingValues.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/PaddingValues.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/PaddingValues.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Position.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Position.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/Position.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Position.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/PosterCard.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/PosterCard.kt similarity index 97% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/PosterCard.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/PosterCard.kt index 6225a10e0c..888a5c24b0 100644 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/PosterCard.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/PosterCard.kt @@ -55,7 +55,6 @@ private fun PosterCardContent(show: TiviShow) { ) AsyncImage( model = show.asImageModel(ImageType.POSTER), - requestBuilder = { crossfade(true) }, contentDescription = stringResource( MR.strings.cd_show_poster_image, show.title ?: "show", diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/RefreshButton.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/RefreshButton.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/RefreshButton.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/RefreshButton.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/SearchTextField.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SearchTextField.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/SearchTextField.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SearchTextField.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/SortChip.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SortChip.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/SortChip.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SortChip.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/SortMenuPopup.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SortMenuPopup.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/SortMenuPopup.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SortMenuPopup.kt diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TimePickerDialog.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TimePickerDialog.kt new file mode 100644 index 0000000000..bc28d9172b --- /dev/null +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TimePickerDialog.kt @@ -0,0 +1,41 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose.ui + +// @OptIn(ExperimentalMaterial3Api::class) +// @Composable +// fun TimePickerDialog( +// onDismissRequest: () -> Unit, +// confirmButton: @Composable () -> Unit, +// modifier: Modifier = Modifier, +// dismissButton: @Composable (() -> Unit)? = null, +// shape: Shape = MaterialTheme.shapes.extraLarge, +// tonalElevation: Dp = DatePickerDefaults.TonalElevation, +// properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false), +// content: @Composable () -> Unit, +// ) { +// AlertDialog( +// onDismissRequest = onDismissRequest, +// properties = properties, +// modifier = modifier, +// ) { +// Surface( +// shape = shape, +// tonalElevation = tonalElevation, +// ) { +// Column { +// content() +// +// Row( +// modifier = Modifier +// .align(Alignment.End) +// .padding(bottom = 8.dp, end = 6.dp), +// ) { +// dismissButton?.invoke() +// confirmButton() +// } +// } +// } +// } +// } diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/TiviAlertDialog.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TiviAlertDialog.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/TiviAlertDialog.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TiviAlertDialog.kt diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/UserProfileButton.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/UserProfileButton.kt similarity index 96% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/UserProfileButton.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/UserProfileButton.kt index 754f313615..61a66e5793 100644 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/UserProfileButton.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/UserProfileButton.kt @@ -33,7 +33,6 @@ fun UserProfileButton( loggedIn && user?.avatarUrl != null -> { AsyncImage( model = user.avatarUrl!!, - requestBuilder = { crossfade(true) }, contentDescription = stringResource( MR.strings.cd_profile_pic, user.name ?: user.username, diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/WindowInsets.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/WindowInsets.kt similarity index 100% rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/WindowInsets.kt rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/WindowInsets.kt diff --git a/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt b/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt new file mode 100644 index 0000000000..0b4b84e39a --- /dev/null +++ b/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt @@ -0,0 +1,10 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose + +import androidx.compose.runtime.Composable + +@Composable +actual fun ReportDrawnWhen(predicate: () -> Boolean) { +} diff --git a/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt b/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt new file mode 100644 index 0000000000..0b4b84e39a --- /dev/null +++ b/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt @@ -0,0 +1,10 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose + +import androidx.compose.runtime.Composable + +@Composable +actual fun ReportDrawnWhen(predicate: () -> Boolean) { +} diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/Debug.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/Debug.kt deleted file mode 100644 index d6dc547428..0000000000 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/Debug.kt +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2020, Google LLC, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -@file:Suppress("NOTHING_TO_INLINE") - -package app.tivi.common.compose - -import android.util.Log -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember - -class Ref(var value: Int) - -const val EnableDebugCompositionLogs = false - -/** - * An effect which logs the number compositions at the invoked point of the slot table. - * Thanks to [objcode](https://github.com/objcode) for this code. - * - * This is an inline function to act as like a C-style #include to the host composable function. - * That way we track it's compositions, not this function's compositions. - * - * @param tag Log tag used for [Log.d] - */ -@Composable -inline fun LogCompositions(tag: String) { - if (EnableDebugCompositionLogs && BuildConfig.DEBUG) { - val ref = remember { Ref(0) } - SideEffect { ref.value++ } - Log.d(tag, "Compositions: ${ref.value}") - } -} diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/WindowSizeClass.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/WindowSizeClass.kt deleted file mode 100644 index e156ba4338..0000000000 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/WindowSizeClass.kt +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.common.compose - -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.unit.DpSize - -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) -val LocalWindowSizeClass = staticCompositionLocalOf { - WindowSizeClass.calculateFromSize(DpSize.Zero) -} diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AndroidDialog.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AndroidDialog.kt deleted file mode 100644 index 7d23ce46bf..0000000000 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AndroidDialog.kt +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.common.compose.ui - -import android.content.res.Configuration -import androidx.compose.foundation.layout.widthIn -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import kotlin.math.roundToInt - -/** - * Implements a suitable min-width for Android dialog content. - * - * Matches the values used in the platform default dialog themes `Theme.Material.Dialog.MinWidth` - * and `Theme.Material.Dialog.Alert`. Unfortunately the necessary attributes used in the themes - * are private, so we can't read them from the theme (and AppCompat duplicates them too). - * - * The values in question can be found here: - * https://cs.android.com/search?q=dialog_min_width%20file:dimens.xml&sq=&ss=android%2Fplatform%2Fsuperproject:frameworks%2Fbase%2F - * - * This primarily exists to workaround https://issuetracker.google.com/issues/221643630, which - * requires the workaround of using `DialogProperties(usePlatformDefaultWidth = false)`. - * - * @param clampMaxWidth Whether to clamp the maximum width to the same value. This is useful for - * Compose content as fillMaxWidth() (or similar) is frequently used, which then stretches the - * dialog to fill the screen width. - */ -fun Modifier.androidMinWidthDialogSize( - clampMaxWidth: Boolean = false, -): Modifier = composed { - val configuration = LocalConfiguration.current - val density = LocalContext.current.resources.displayMetrics.density - - val displayWidth = (configuration.screenWidthDp * density).roundToInt() - val displayHeight = (configuration.screenHeightDp * density).roundToInt() - - val minWidthRatio: Float = when { - configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE) -> { - if (displayWidth > displayHeight) 0.45f else 0.72f - } - configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE) -> { - if (displayWidth > displayHeight) 0.55f else 0.8f - } - else -> { - if (displayWidth > displayHeight) 0.65f else 0.95f - } - } - - if (clampMaxWidth) { - Modifier.widthIn(max = ((displayWidth * minWidthRatio) / density).dp) - } else { - Modifier.widthIn(min = ((displayWidth * minWidthRatio) / density).dp) - } -} diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Image.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Image.kt deleted file mode 100644 index 4e820df439..0000000000 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Image.kt +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022, Google LLC, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.common.compose.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.DefaultAlpha -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import coil.compose.AsyncImagePainter -import coil.request.ImageRequest - -@Composable -fun AsyncImage( - model: Any?, - contentDescription: String?, - modifier: Modifier = Modifier, - transform: (AsyncImagePainter.State) -> AsyncImagePainter.State = AsyncImagePainter.DefaultTransform, - onState: ((AsyncImagePainter.State) -> Unit)? = null, - requestBuilder: (ImageRequest.Builder.() -> ImageRequest.Builder)? = null, - alignment: Alignment = Alignment.Center, - contentScale: ContentScale = ContentScale.Fit, - alpha: Float = DefaultAlpha, - colorFilter: ColorFilter? = null, - filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, -) { - coil.compose.AsyncImage( - model = requestBuilder?.let { builder -> - when (model) { - is ImageRequest -> model.newBuilder() - else -> ImageRequest.Builder(LocalContext.current).data(model) - }.apply { this.builder() }.build() - } ?: model, - contentDescription = contentDescription, - modifier = modifier, - transform = transform, - onState = onState, - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - colorFilter = colorFilter, - filterQuality = filterQuality, - ) -} diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/TimePickerDialog.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/ui/TimePickerDialog.kt deleted file mode 100644 index 85d46bb59a..0000000000 --- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/TimePickerDialog.kt +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.common.compose.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.DatePickerDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TimePickerDialog( - onDismissRequest: () -> Unit, - confirmButton: @Composable () -> Unit, - modifier: Modifier = Modifier, - dismissButton: @Composable (() -> Unit)? = null, - shape: Shape = MaterialTheme.shapes.extraLarge, - tonalElevation: Dp = DatePickerDefaults.TonalElevation, - properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false), - content: @Composable () -> Unit, -) { - AlertDialog( - onDismissRequest = onDismissRequest, - properties = properties, - modifier = modifier, - ) { - Surface( - shape = shape, - tonalElevation = tonalElevation, - ) { - Column { - content() - - Row( - modifier = Modifier - .align(Alignment.End) - .padding(bottom = 8.dp, end = 6.dp), - ) { - dismissButton?.invoke() - confirmButton() - } - } - } - } -} diff --git a/common/ui/resources-compose/build.gradle.kts b/common/ui/resources-compose/build.gradle.kts deleted file mode 100644 index 3dbbf13986..0000000000 --- a/common/ui/resources-compose/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - - -plugins { - id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") -} - -android { - namespace = "dev.icerock.moko.resources.compose" - - buildFeatures { - buildConfig = true - } -} - -dependencies { - api(platform(libs.compose.bom)) - implementation(libs.compose.foundation.foundation) - - api(libs.moko.resources) -} diff --git a/common/ui/resources-compose/src/main/AndroidManifest.xml b/common/ui/resources-compose/src/main/AndroidManifest.xml deleted file mode 100644 index 03649fd118..0000000000 --- a/common/ui/resources-compose/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/AssetResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/AssetResource.kt deleted file mode 100644 index 850639585e..0000000000 --- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/AssetResource.kt +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package dev.icerock.moko.resources.compose - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.produceState -import androidx.compose.ui.platform.LocalContext -import dev.icerock.moko.resources.AssetResource - -@Composable -fun AssetResource.readTextAsState(): State { - val context: Context = LocalContext.current - return produceState(null, this, context) { - value = readText(context) - } -} diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ColorResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ColorResource.kt deleted file mode 100644 index 8008c033a0..0000000000 --- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ColorResource.kt +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package dev.icerock.moko.resources.compose - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import dev.icerock.moko.resources.ColorResource - -@Composable -fun colorResource(resource: ColorResource): Color { - val context: Context = LocalContext.current - return Color(resource.getColor(context)) -} diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FileResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FileResource.kt deleted file mode 100644 index f9452b59a5..0000000000 --- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FileResource.kt +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package dev.icerock.moko.resources.compose - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.produceState -import androidx.compose.ui.platform.LocalContext -import dev.icerock.moko.resources.FileResource - -@Composable -fun FileResource.readTextAsState(): State { - val context: Context = LocalContext.current - return produceState(null, this, context) { - value = readText(context) - } -} diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FontResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FontResource.kt deleted file mode 100644 index b74f114068..0000000000 --- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FontResource.kt +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package dev.icerock.moko.resources.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import dev.icerock.moko.resources.FontResource - -@Suppress("RedundantNullableReturnType") -@Composable -fun FontResource.asFont( - weight: FontWeight = FontWeight.Normal, - style: FontStyle = FontStyle.Normal, -): Font? = remember(fontResourceId) { - Font( - resId = fontResourceId, - weight = weight, - style = style, - ) -} diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt deleted file mode 100644 index 0c39bad7ff..0000000000 --- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package dev.icerock.moko.resources.compose - -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.painter.Painter -import dev.icerock.moko.resources.ImageResource - -@Composable -fun painterResource(imageResource: ImageResource): Painter { - return androidx.compose.ui.res.painterResource(id = imageResource.drawableResId) -} diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringDescExt.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringDescExt.kt deleted file mode 100644 index 9f9728a6ba..0000000000 --- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringDescExt.kt +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2022, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package dev.icerock.moko.resources.compose - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import dev.icerock.moko.resources.desc.StringDesc - -@Composable -fun StringDesc.localized(): String { - val context: Context = LocalContext.current - return toString(context) -} diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringResource.kt deleted file mode 100644 index 3695c5be74..0000000000 --- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringResource.kt +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2021, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package dev.icerock.moko.resources.compose - -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import dev.icerock.moko.resources.PluralsResource -import dev.icerock.moko.resources.StringResource -import dev.icerock.moko.resources.desc.Plural -import dev.icerock.moko.resources.desc.PluralFormatted -import dev.icerock.moko.resources.desc.Resource -import dev.icerock.moko.resources.desc.ResourceFormatted -import dev.icerock.moko.resources.desc.StringDesc - -@Composable -fun stringResource(resource: StringResource): String = - StringDesc.Resource(resource).toString(LocalContext.current) - -@Composable -fun stringResource(resource: StringResource, vararg args: Any): String = - StringDesc.ResourceFormatted(resource, *args).toString(LocalContext.current) - -@Composable -fun stringResource(resource: PluralsResource, quantity: Int): String = - StringDesc.Plural(resource, quantity).toString(LocalContext.current) - -@Composable -fun stringResource(resource: PluralsResource, quantity: Int, vararg args: Any): String = - StringDesc.PluralFormatted(resource, quantity, *args).toString(LocalContext.current) diff --git a/gradle.properties b/gradle.properties index a3477a1dd2..ef9660623b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,3 +35,5 @@ android.defaults.buildFeatures.buildConfig=false kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidGradlePluginCompatibility.nowarn=true + +org.jetbrains.compose.experimental.uikit.enabled=true diff --git a/gradle/build-logic/convention/build.gradle.kts b/gradle/build-logic/convention/build.gradle.kts index a6b7a36d7d..f08607204b 100644 --- a/gradle/build-logic/convention/build.gradle.kts +++ b/gradle/build-logic/convention/build.gradle.kts @@ -49,10 +49,5 @@ gradlePlugin { id = "app.tivi.android.test" implementationClass = "app.tivi.gradle.AndroidTestConventionPlugin" } - - register("androidCompose") { - id = "app.tivi.android.compose" - implementationClass = "app.tivi.gradle.AndroidComposeConventionPlugin" - } } } diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidCompose.kt b/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidCompose.kt deleted file mode 100644 index 8ffa3d96e4..0000000000 --- a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidCompose.kt +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.gradle - -import com.android.build.api.dsl.CommonExtension -import org.gradle.api.Project -import org.gradle.kotlin.dsl.configure - -fun Project.configureAndroidCompose() { - android { - buildFeatures { - compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = libs.findVersion("composecompiler").get().toString() - } - } -} - -private fun Project.android(action: CommonExtension<*, *, *, *>.() -> Unit) = - extensions.configure(CommonExtension::class, action) diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidComposeConventionPlugin.kt b/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidComposeConventionPlugin.kt deleted file mode 100644 index 41990f5826..0000000000 --- a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidComposeConventionPlugin.kt +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2023, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.gradle - -import org.gradle.api.Plugin -import org.gradle.api.Project - -class AndroidComposeConventionPlugin : Plugin { - override fun apply(target: Project) = with(target) { - pluginManager.withPlugin("com.android.base") { - configureAndroidCompose() - } - } -} diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Android.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Android.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/Android.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Android.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidApplicationConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidApplicationConventionPlugin.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidApplicationConventionPlugin.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidApplicationConventionPlugin.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidApplicationLauncher.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidApplicationLauncher.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidApplicationLauncher.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidApplicationLauncher.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidLibraryConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidLibraryConventionPlugin.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidLibraryConventionPlugin.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidLibraryConventionPlugin.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidTestConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidTestConventionPlugin.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidTestConventionPlugin.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidTestConventionPlugin.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Java.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Java.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/Java.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Java.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Kotlin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Kotlin.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/Kotlin.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Kotlin.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/KotlinAndroidConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/KotlinAndroidConventionPlugin.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/KotlinAndroidConventionPlugin.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/KotlinAndroidConventionPlugin.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/KotlinMultiplatformConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/KotlinMultiplatformConventionPlugin.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/KotlinMultiplatformConventionPlugin.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/KotlinMultiplatformConventionPlugin.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/RootConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/RootConventionPlugin.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/RootConventionPlugin.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/RootConventionPlugin.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Spotless.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Spotless.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/Spotless.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Spotless.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/VersionCatalog.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/VersionCatalog.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/VersionCatalog.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/VersionCatalog.kt diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Versions.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Versions.kt similarity index 100% rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/Versions.kt rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Versions.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9aac88b4b0..64d07ddcf8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,12 +4,9 @@ androidxlifecycle = "2.6.1" androidxactivity = "1.7.2" chucker = "4.0.0" circuit = "0.10.0" -coil = "2.4.0" -compose-bom = "2023.03.00" -composecompiler = "1.4.7" coroutines = "1.7.2" debugdrawer = "0.9.8" -kotlin = "1.8.21" +kotlin = "1.8.20" kotlininject = "0.6.1" ktlint = "0.49.1" moko-resources = "0.23.0" @@ -124,7 +121,7 @@ kotlininject-runtime = { module = "me.tatarka.inject:kotlin-inject-runtime", ver # This isn't strictly used, but allows Renovate to see us using the ktlint artifact ktlint = { module = "com.pinterest:ktlint", version.ref = "ktlint" } -leakCanary = "com.squareup.leakcanary:leakcanary-android:2.12" +leakCanary = "com.squareup.leakcanary:leakcanary-android:2.11" moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko-resources" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0d3775d269..7bf465881c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,7 +57,6 @@ include( ":core:preferences", ":common:ui:circuit-overlay", ":common:ui:resources", - ":common:ui:resources-compose", ":common:ui:compose", ":common:ui:screens", ":common:imageloading", diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 360a4ce163..a62267e2ab 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -22,13 +22,23 @@ kotlin { api(projects.api.tmdb) api(projects.domain) api(projects.tasks) - } - } - val androidMain by getting { - dependencies { api(projects.common.imageloading) api(projects.common.ui.compose) + + // api(projects.ui.account) + api(projects.ui.discover) + api(projects.ui.episode.details) + api(projects.ui.episode.track) + api(projects.ui.library) + api(projects.ui.popular) + api(projects.ui.trending) + api(projects.ui.recommended) + api(projects.ui.search) + api(projects.ui.show.details) + api(projects.ui.show.seasons) + // api(projects.ui.settings) + api(projects.ui.upnext) } } } diff --git a/ui/account/build.gradle.kts b/ui/account/build.gradle.kts index 47588c3340..32e279fd96 100644 --- a/ui/account/build.gradle.kts +++ b/ui/account/build.gradle.kts @@ -4,8 +4,8 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") id("app.tivi.kotlin.android") + alias(libs.plugins.composeMultiplatform) } android { @@ -25,12 +25,9 @@ dependencies { // For registerForActivityResult implementation(libs.androidx.activity.compose) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material3.material3) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) - - implementation(libs.coil.compose) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(compose.animation) + implementation(compose.uiTooling) } diff --git a/ui/account/src/main/java/app/tivi/account/AccountComponent.kt b/ui/account/src/main/kotlin/app/tivi/account/AccountComponent.kt similarity index 100% rename from ui/account/src/main/java/app/tivi/account/AccountComponent.kt rename to ui/account/src/main/kotlin/app/tivi/account/AccountComponent.kt diff --git a/ui/account/src/main/java/app/tivi/account/AccountPresenter.kt b/ui/account/src/main/kotlin/app/tivi/account/AccountPresenter.kt similarity index 100% rename from ui/account/src/main/java/app/tivi/account/AccountPresenter.kt rename to ui/account/src/main/kotlin/app/tivi/account/AccountPresenter.kt diff --git a/ui/account/src/main/java/app/tivi/account/AccountUi.kt b/ui/account/src/main/kotlin/app/tivi/account/AccountUi.kt similarity index 98% rename from ui/account/src/main/java/app/tivi/account/AccountUi.kt rename to ui/account/src/main/kotlin/app/tivi/account/AccountUi.kt index 9805473e52..2f3f99bf32 100644 --- a/ui/account/src/main/java/app/tivi/account/AccountUi.kt +++ b/ui/account/src/main/kotlin/app/tivi/account/AccountUi.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import app.tivi.common.compose.ui.AsyncImage import app.tivi.common.ui.resources.MR @@ -174,7 +173,7 @@ private fun UserRow( if (avatarUrl != null) { AsyncImage( model = avatarUrl, - requestBuilder = { crossfade(true) }, + contentDescription = stringResource( MR.strings.cd_profile_pic, user.name @@ -234,7 +233,7 @@ private fun AppAction( } } -@Preview +// @Preview @Composable fun PreviewUserRow() { UserRow( diff --git a/ui/account/src/main/java/app/tivi/account/AccountUiState.kt b/ui/account/src/main/kotlin/app/tivi/account/AccountUiState.kt similarity index 100% rename from ui/account/src/main/java/app/tivi/account/AccountUiState.kt rename to ui/account/src/main/kotlin/app/tivi/account/AccountUiState.kt diff --git a/ui/discover/build.gradle.kts b/ui/discover/build.gradle.kts index 2ff5bf1d62..3598edbe5a 100644 --- a/ui/discover/build.gradle.kts +++ b/ui/discover/build.gradle.kts @@ -4,30 +4,32 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.home.discover" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) - api(projects.common.ui.screens) - api(projects.common.ui.circuitOverlay) - api(libs.circuit.foundation) + api(projects.common.ui.screens) + api(projects.common.ui.circuitOverlay) + api(libs.circuit.foundation) - implementation(libs.androidx.activity.compose) - - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material3.material3) - implementation(libs.compose.material3.windowsizeclass) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(libs.compose.material3.windowsizeclass) + implementation(compose.animation) + } + } + } } diff --git a/ui/discover/src/main/java/app/tivi/home/discover/Discover.kt b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/Discover.kt similarity index 96% rename from ui/discover/src/main/java/app/tivi/home/discover/Discover.kt rename to ui/discover/src/commonMain/kotlin/app/tivi/home/discover/Discover.kt index 938e230e3d..aa3e831fbb 100644 --- a/ui/discover/src/main/java/app/tivi/home/discover/Discover.kt +++ b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/Discover.kt @@ -6,7 +6,6 @@ package app.tivi.home.discover import android.os.Build -import androidx.activity.compose.ReportDrawnWhen import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider @@ -28,13 +27,15 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.DismissValue import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.rememberDismissState import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card -import androidx.compose.material3.DismissValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -42,10 +43,8 @@ import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface -import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -53,10 +52,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.FirstBaseline import androidx.compose.ui.platform.testTag -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import app.tivi.common.compose.Layout import app.tivi.common.compose.LocalTiviTextCreator +import app.tivi.common.compose.ReportDrawnWhen import app.tivi.common.compose.bodyWidth import app.tivi.common.compose.rememberCoroutineScope import app.tivi.common.compose.ui.AutoSizedCircularProgressIndicator @@ -139,16 +138,14 @@ internal fun Discover( ) { val snackbarHostState = remember { SnackbarHostState() } - val dismissSnackbarState = rememberDismissState( - confirmValueChange = { value -> - if (value != DismissValue.Default) { - snackbarHostState.currentSnackbarData?.dismiss() - true - } else { - false - } - }, - ) + val dismissSnackbarState = rememberDismissState { value -> + if (value != DismissValue.Default) { + snackbarHostState.currentSnackbarData?.dismiss() + true + } else { + false + } + } state.message?.let { message -> LaunchedEffect(message) { @@ -445,7 +442,7 @@ private fun Header( } } -@Preview +// @Preview @Composable private fun PreviewHeader() { Surface(Modifier.fillMaxWidth()) { diff --git a/ui/discover/src/main/java/app/tivi/home/discover/DiscoverComponent.kt b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverComponent.kt similarity index 100% rename from ui/discover/src/main/java/app/tivi/home/discover/DiscoverComponent.kt rename to ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverComponent.kt diff --git a/ui/discover/src/main/java/app/tivi/home/discover/DiscoverPresenter.kt b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverPresenter.kt similarity index 100% rename from ui/discover/src/main/java/app/tivi/home/discover/DiscoverPresenter.kt rename to ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverPresenter.kt diff --git a/ui/discover/src/main/java/app/tivi/home/discover/DiscoverUiState.kt b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverUiState.kt similarity index 100% rename from ui/discover/src/main/java/app/tivi/home/discover/DiscoverUiState.kt rename to ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverUiState.kt diff --git a/ui/episode/details/build.gradle.kts b/ui/episode/details/build.gradle.kts index 6d5668d030..b7d03e69ee 100644 --- a/ui/episode/details/build.gradle.kts +++ b/ui/episode/details/build.gradle.kts @@ -4,31 +4,33 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.episodedetails" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) - api(projects.common.ui.screens) - api(projects.common.ui.circuitOverlay) - api(libs.circuit.foundation) + api(projects.common.ui.screens) + api(projects.common.ui.circuitOverlay) + api(libs.circuit.foundation) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material3.material3) - implementation(libs.compose.material3.windowsizeclass) - implementation(libs.compose.material.iconsext) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) - - implementation(libs.coil.compose) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(libs.compose.material3.windowsizeclass) + implementation(compose.materialIconsExtended) + implementation(compose.animation) + } + } + } } diff --git a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetails.kt b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetails.kt similarity index 93% rename from ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetails.kt rename to ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetails.kt index bf0bcd52a8..73465a166b 100644 --- a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetails.kt +++ b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetails.kt @@ -18,10 +18,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.DismissDirection +import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.CalendarToday @@ -31,9 +34,8 @@ import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.rememberDismissState import androidx.compose.material3.Button -import androidx.compose.material3.DismissDirection -import androidx.compose.material3.DismissValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -44,11 +46,9 @@ import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface -import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -62,7 +62,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import app.tivi.common.compose.Layout import app.tivi.common.compose.LocalTiviDateFormatter @@ -80,9 +79,10 @@ import app.tivi.data.models.Episode import app.tivi.data.models.EpisodeWatchEntry import app.tivi.data.models.PendingAction import app.tivi.data.models.Season -import app.tivi.overlays.showInBottomSheet +import app.tivi.overlays.showInDialog import app.tivi.screens.EpisodeDetailsScreen import app.tivi.screens.EpisodeTrackScreen +import com.moriatsushi.insetsx.statusBars import com.slack.circuit.overlay.LocalOverlayHost import com.slack.circuit.runtime.CircuitContext import com.slack.circuit.runtime.Screen @@ -122,7 +122,7 @@ internal fun EpisodeDetails( onRemoveWatch = { id -> viewState.eventSink(EpisodeDetailsUiEvent.RemoveWatchEntry(id)) }, onAddWatch = { scope.launch { - overlayHost.showInBottomSheet(EpisodeTrackScreen(viewState.episode!!.id)) + overlayHost.showInDialog(EpisodeTrackScreen(viewState.episode!!.id)) } }, onMessageShown = { id -> viewState.eventSink(EpisodeDetailsUiEvent.ClearMessage(id)) }, @@ -130,7 +130,7 @@ internal fun EpisodeDetails( ) } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable internal fun EpisodeDetails( viewState: EpisodeDetailsUiState, @@ -144,16 +144,14 @@ internal fun EpisodeDetails( ) { val snackbarHostState = remember { SnackbarHostState() } - val dismissSnackbarState = rememberDismissState( - confirmValueChange = { value -> - if (value != DismissValue.Default) { - snackbarHostState.currentSnackbarData?.dismiss() - true - } else { - false - } - }, - ) + val dismissSnackbarState = rememberDismissState { value -> + if (value != DismissValue.Default) { + snackbarHostState.currentSnackbarData?.dismiss() + true + } else { + false + } + } viewState.message?.let { message -> LaunchedEffect(message) { @@ -249,16 +247,14 @@ internal fun EpisodeDetails( viewState.watches.forEach { watch -> key(watch.id) { - val dismissState = rememberDismissState( - confirmValueChange = { value -> - if (value != DismissValue.Default) { - onRemoveWatch(watch.id) - true - } else { - false - } - }, - ) + val dismissState = rememberDismissState { value -> + if (value != DismissValue.Default) { + onRemoveWatch(watch.id) + true + } else { + false + } + } SwipeToDismiss( state = dismissState, @@ -523,7 +519,7 @@ private fun EpisodeDetailsAppBar( modifier: Modifier = Modifier, ) { TopAppBar( - colors = TopAppBarDefaults.topAppBarColors( + colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = Color.Transparent, actionIconContentColor = LocalContentColor.current, ), @@ -558,7 +554,7 @@ private fun EpisodeDetailsAppBar( ) } -@Preview +// @Preview @Composable fun PreviewEpisodeDetails() { EpisodeDetails( diff --git a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsComponent.kt b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsComponent.kt similarity index 100% rename from ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsComponent.kt rename to ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsComponent.kt diff --git a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsPresenter.kt b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsPresenter.kt similarity index 100% rename from ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsPresenter.kt rename to ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsPresenter.kt diff --git a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsUiState.kt b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsUiState.kt similarity index 100% rename from ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsUiState.kt rename to ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsUiState.kt diff --git a/ui/episode/track/build.gradle.kts b/ui/episode/track/build.gradle.kts index 56c2ec4b65..fc2c67fc94 100644 --- a/ui/episode/track/build.gradle.kts +++ b/ui/episode/track/build.gradle.kts @@ -4,30 +4,32 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.episode.track" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) - api(projects.common.ui.screens) - api(libs.circuit.foundation) + api(projects.common.ui.screens) + api(libs.circuit.foundation) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material3.material3) - implementation(libs.compose.material3.windowsizeclass) - implementation(libs.compose.material.iconsext) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) - - implementation(libs.coil.compose) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(libs.compose.material3.windowsizeclass) + implementation(compose.materialIconsExtended) + implementation(compose.animation) + } + } + } } diff --git a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrack.kt b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrack.kt similarity index 94% rename from ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrack.kt rename to ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrack.kt index 5cc0ffa66a..f7f3935ed1 100644 --- a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrack.kt +++ b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrack.kt @@ -13,19 +13,19 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss +import androidx.compose.material.rememberDismissState import androidx.compose.material3.Card -import androidx.compose.material3.DismissValue import androidx.compose.material3.Divider -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -94,7 +94,7 @@ internal fun EpisodeTrack( ) } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterialApi::class) @Composable internal fun EpisodeTrack( viewState: EpisodeTrackUiState, @@ -109,16 +109,14 @@ internal fun EpisodeTrack( ) { val snackbarHostState = remember { SnackbarHostState() } - val dismissSnackbarState = rememberDismissState( - confirmValueChange = { value -> - if (value != DismissValue.Default) { - snackbarHostState.currentSnackbarData?.dismiss() - true - } else { - false - } - }, - ) + val dismissSnackbarState = rememberDismissState { value -> + if (value != DismissValue.Default) { + snackbarHostState.currentSnackbarData?.dismiss() + true + } else { + false + } + } viewState.message?.let { message -> LaunchedEffect(message) { @@ -198,7 +196,6 @@ private fun EpisodeHeader( ) { AsyncImage( model = episode.asImageModel(), - requestBuilder = { crossfade(true) }, contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, diff --git a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackComponent.kt b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackComponent.kt similarity index 100% rename from ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackComponent.kt rename to ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackComponent.kt diff --git a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackPresenter.kt b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackPresenter.kt similarity index 100% rename from ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackPresenter.kt rename to ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackPresenter.kt diff --git a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackUiState.kt b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackUiState.kt similarity index 100% rename from ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackUiState.kt rename to ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackUiState.kt diff --git a/ui/library/build.gradle.kts b/ui/library/build.gradle.kts index f41d0c13fb..e80ab9a6eb 100644 --- a/ui/library/build.gradle.kts +++ b/ui/library/build.gradle.kts @@ -4,34 +4,34 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.home.shows" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) - - api(projects.common.ui.screens) - api(projects.common.ui.circuitOverlay) - api(libs.circuit.foundation) - - implementation(libs.paging.compose) - - implementation(libs.androidx.core) - - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material.iconsext) - implementation(libs.compose.material3.material3) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) - - implementation(libs.coil.compose) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) + + api(projects.common.ui.screens) + api(projects.common.ui.circuitOverlay) + api(libs.circuit.foundation) + + implementation(libs.paging.compose) + + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.materialIconsExtended) + implementation(compose.material3) + implementation(compose.animation) + } + } + } } diff --git a/ui/library/src/main/java/app/tivi/home/library/Library.kt b/ui/library/src/commonMain/kotlin/app/tivi/home/library/Library.kt similarity index 97% rename from ui/library/src/main/java/app/tivi/home/library/Library.kt rename to ui/library/src/commonMain/kotlin/app/tivi/home/library/Library.kt index b2bfb745ee..8f8d05a5fc 100644 --- a/ui/library/src/main/java/app/tivi/home/library/Library.kt +++ b/ui/library/src/commonMain/kotlin/app/tivi/home/library/Library.kt @@ -24,14 +24,16 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.DismissValue import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Search import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.DismissValue +import androidx.compose.material.rememberDismissState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon @@ -42,10 +44,8 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -152,16 +152,14 @@ internal fun Library( ) { val snackbarHostState = remember { SnackbarHostState() } - val dismissSnackbarState = rememberDismissState( - confirmValueChange = { value -> - if (value != DismissValue.Default) { - snackbarHostState.currentSnackbarData?.dismiss() - true - } else { - false - } - }, - ) + val dismissSnackbarState = rememberDismissState { value -> + if (value != DismissValue.Default) { + snackbarHostState.currentSnackbarData?.dismiss() + true + } else { + false + } + } state.message?.let { message -> LaunchedEffect(message) { diff --git a/ui/library/src/main/java/app/tivi/home/library/LibraryComponent.kt b/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryComponent.kt similarity index 100% rename from ui/library/src/main/java/app/tivi/home/library/LibraryComponent.kt rename to ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryComponent.kt diff --git a/ui/library/src/main/java/app/tivi/home/library/LibraryPresenter.kt b/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryPresenter.kt similarity index 100% rename from ui/library/src/main/java/app/tivi/home/library/LibraryPresenter.kt rename to ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryPresenter.kt diff --git a/ui/library/src/main/java/app/tivi/home/library/LibraryUiState.kt b/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryUiState.kt similarity index 100% rename from ui/library/src/main/java/app/tivi/home/library/LibraryUiState.kt rename to ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryUiState.kt diff --git a/ui/popular/build.gradle.kts b/ui/popular/build.gradle.kts index 9f1bd5455b..64603f0ff2 100644 --- a/ui/popular/build.gradle.kts +++ b/ui/popular/build.gradle.kts @@ -4,27 +4,31 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.home.popular" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) - api(projects.common.ui.screens) - api(libs.circuit.foundation) + api(projects.common.ui.screens) + api(libs.circuit.foundation) - implementation(libs.paging.compose) + implementation(libs.paging.compose) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.animation) + } + } + } } diff --git a/ui/popular/src/main/java/app/tivi/home/popular/PopularShows.kt b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShows.kt similarity index 100% rename from ui/popular/src/main/java/app/tivi/home/popular/PopularShows.kt rename to ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShows.kt diff --git a/ui/popular/src/main/java/app/tivi/home/popular/PopularShowsComponent.kt b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsComponent.kt similarity index 100% rename from ui/popular/src/main/java/app/tivi/home/popular/PopularShowsComponent.kt rename to ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsComponent.kt diff --git a/ui/popular/src/main/java/app/tivi/home/popular/PopularShowsPresenter.kt b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsPresenter.kt similarity index 100% rename from ui/popular/src/main/java/app/tivi/home/popular/PopularShowsPresenter.kt rename to ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsPresenter.kt diff --git a/ui/popular/src/main/java/app/tivi/home/popular/PopularShowsUiState.kt b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsUiState.kt similarity index 100% rename from ui/popular/src/main/java/app/tivi/home/popular/PopularShowsUiState.kt rename to ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsUiState.kt diff --git a/ui/recommended/build.gradle.kts b/ui/recommended/build.gradle.kts index 30a7cef663..99a46f20f8 100644 --- a/ui/recommended/build.gradle.kts +++ b/ui/recommended/build.gradle.kts @@ -4,27 +4,31 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.home.recommended" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) - api(projects.common.ui.screens) - api(libs.circuit.foundation) + api(projects.common.ui.screens) + api(libs.circuit.foundation) - implementation(libs.paging.compose) + implementation(libs.paging.compose) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.animation) + } + } + } } diff --git a/ui/recommended/src/main/java/app/tivi/home/recommended/Recommended.kt b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/Recommended.kt similarity index 100% rename from ui/recommended/src/main/java/app/tivi/home/recommended/Recommended.kt rename to ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/Recommended.kt diff --git a/ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsComponent.kt b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsComponent.kt similarity index 100% rename from ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsComponent.kt rename to ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsComponent.kt diff --git a/ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsPresenter.kt b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsPresenter.kt similarity index 100% rename from ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsPresenter.kt rename to ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsPresenter.kt diff --git a/ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsUiState.kt b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsUiState.kt similarity index 100% rename from ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsUiState.kt rename to ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsUiState.kt diff --git a/ui/search/build.gradle.kts b/ui/search/build.gradle.kts index 311da49748..32c9f718d2 100644 --- a/ui/search/build.gradle.kts +++ b/ui/search/build.gradle.kts @@ -4,27 +4,31 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.home.search" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) - implementation(projects.common.imageloading) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) + implementation(projects.common.imageloading) - api(projects.common.ui.screens) - api(libs.circuit.foundation) + api(projects.common.ui.screens) + api(libs.circuit.foundation) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material3.material3) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(compose.animation) + } + } + } } diff --git a/ui/search/src/main/java/app/tivi/home/search/Search.kt b/ui/search/src/commonMain/kotlin/app/tivi/home/search/Search.kt similarity index 94% rename from ui/search/src/main/java/app/tivi/home/search/Search.kt rename to ui/search/src/commonMain/kotlin/app/tivi/home/search/Search.kt index 5cafa11151..a06809d1f0 100644 --- a/ui/search/src/main/java/app/tivi/home/search/Search.kt +++ b/ui/search/src/commonMain/kotlin/app/tivi/home/search/Search.kt @@ -20,7 +20,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.material3.DismissValue +import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss +import androidx.compose.material.rememberDismissState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -28,9 +31,7 @@ import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface -import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text -import androidx.compose.material3.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -89,7 +90,7 @@ internal fun Search( ) } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable internal fun Search( state: SearchUiState, @@ -100,16 +101,14 @@ internal fun Search( ) { val snackbarHostState = remember { SnackbarHostState() } - val dismissSnackbarState = rememberDismissState( - confirmValueChange = { value -> - if (value != DismissValue.Default) { - snackbarHostState.currentSnackbarData?.dismiss() - true - } else { - false - } - }, - ) + val dismissSnackbarState = rememberDismissState { value -> + if (value != DismissValue.Default) { + snackbarHostState.currentSnackbarData?.dismiss() + true + } else { + false + } + } state.message?.let { message -> LaunchedEffect(message) { diff --git a/ui/search/src/main/java/app/tivi/home/search/SearchComponent.kt b/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchComponent.kt similarity index 100% rename from ui/search/src/main/java/app/tivi/home/search/SearchComponent.kt rename to ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchComponent.kt diff --git a/ui/search/src/main/java/app/tivi/home/search/SearchPresenter.kt b/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchPresenter.kt similarity index 100% rename from ui/search/src/main/java/app/tivi/home/search/SearchPresenter.kt rename to ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchPresenter.kt diff --git a/ui/search/src/main/java/app/tivi/home/search/SearchUiState.kt b/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchUiState.kt similarity index 100% rename from ui/search/src/main/java/app/tivi/home/search/SearchUiState.kt rename to ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchUiState.kt diff --git a/ui/settings/src/main/java/app/tivi/settings/SettingsActivity.kt b/ui/settings/src/main/kotlin/app/tivi/settings/SettingsActivity.kt similarity index 100% rename from ui/settings/src/main/java/app/tivi/settings/SettingsActivity.kt rename to ui/settings/src/main/kotlin/app/tivi/settings/SettingsActivity.kt diff --git a/ui/settings/src/main/java/app/tivi/settings/SettingsPreferenceFragment.kt b/ui/settings/src/main/kotlin/app/tivi/settings/SettingsPreferenceFragment.kt similarity index 100% rename from ui/settings/src/main/java/app/tivi/settings/SettingsPreferenceFragment.kt rename to ui/settings/src/main/kotlin/app/tivi/settings/SettingsPreferenceFragment.kt diff --git a/ui/show/details/build.gradle.kts b/ui/show/details/build.gradle.kts index df2e1dc476..99cb46b3c3 100644 --- a/ui/show/details/build.gradle.kts +++ b/ui/show/details/build.gradle.kts @@ -4,29 +4,31 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.showdetails.details" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) - api(projects.common.ui.screens) - api(libs.circuit.foundation) + api(projects.common.ui.screens) + api(libs.circuit.foundation) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material3.material3) - implementation(libs.compose.material3.windowsizeclass) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) - - implementation(libs.coil.compose) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(libs.compose.material3.windowsizeclass) + implementation(compose.animation) + } + } + } } diff --git a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetails.kt b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetails.kt similarity index 93% rename from ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetails.kt rename to ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetails.kt index 66ce2baf7d..23a4a98a85 100644 --- a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetails.kt +++ b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetails.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -27,10 +26,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn @@ -39,13 +36,16 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Star -import androidx.compose.material3.DismissValue +import androidx.compose.material.rememberDismissState import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -54,7 +54,6 @@ import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults @@ -62,12 +61,10 @@ import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface -import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -88,7 +85,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import app.tivi.common.compose.Layout import app.tivi.common.compose.LocalTiviTextCreator -import app.tivi.common.compose.LogCompositions import app.tivi.common.compose.bodyWidth import app.tivi.common.compose.gutterSpacer import app.tivi.common.compose.itemSpacer @@ -97,7 +93,6 @@ import app.tivi.common.compose.ui.Backdrop import app.tivi.common.compose.ui.ExpandingText import app.tivi.common.compose.ui.PosterCard import app.tivi.common.compose.ui.RefreshButton -import app.tivi.common.imageloading.TrimTransparentEdgesTransformation import app.tivi.common.ui.resources.MR import app.tivi.data.compoundmodels.EpisodeWithSeason import app.tivi.data.compoundmodels.RelatedShowEntryWithShow @@ -108,10 +103,10 @@ import app.tivi.data.models.Genre import app.tivi.data.models.ImageType import app.tivi.data.models.Season import app.tivi.data.models.ShowStatus -import app.tivi.data.models.ShowTmdbImage import app.tivi.data.models.TiviShow import app.tivi.data.views.ShowsWatchStats import app.tivi.screens.ShowDetailsScreen +import com.moriatsushi.insetsx.navigationBars import com.slack.circuit.runtime.CircuitContext import com.slack.circuit.runtime.Screen import com.slack.circuit.runtime.ui.Ui @@ -160,6 +155,7 @@ internal fun ShowDetails( ) } +@OptIn(ExperimentalMaterialApi::class) @Composable internal fun ShowDetails( viewState: ShowDetailsUiState, @@ -180,16 +176,14 @@ internal fun ShowDetails( val snackbarHostState = remember { SnackbarHostState() } val listState = rememberLazyListState() - val dismissSnackbarState = rememberDismissState( - confirmValueChange = { value -> - if (value != DismissValue.Default) { - snackbarHostState.currentSnackbarData?.dismiss() - true - } else { - false - } - }, - ) + val dismissSnackbarState = rememberDismissState { value -> + if (value != DismissValue.Default) { + snackbarHostState.currentSnackbarData?.dismiss() + true + } else { + false + } + } viewState.message?.let { message -> LaunchedEffect(message) { @@ -242,8 +236,6 @@ internal fun ShowDetails( .exclude(WindowInsets.navigationBars), modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { contentPadding -> - LogCompositions("ShowDetails") - Surface(modifier = Modifier.bodyWidth()) { ShowDetailsScrollingContent( show = viewState.show, @@ -288,8 +280,6 @@ private fun ShowDetailsScrollingContent( contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { - LogCompositions("ShowDetailsScrollingContent") - val gutter = Layout.gutter val bodyMargin = Layout.bodyMargin @@ -425,7 +415,6 @@ private fun PosterInfoRow( Row(modifier.padding(horizontal = Layout.bodyMargin)) { AsyncImage( model = show.asImageModel(ImageType.POSTER), - requestBuilder = { crossfade(true) }, contentDescription = stringResource(MR.strings.cd_show_poster, show.title ?: ""), modifier = Modifier .weight(1f) @@ -447,7 +436,6 @@ private fun PosterInfoRow( private fun NetworkInfoPanel( networkName: String, modifier: Modifier = Modifier, - networkLogoPath: String? = null, ) { Column(modifier) { Text( @@ -457,32 +445,10 @@ private fun NetworkInfoPanel( Spacer(Modifier.height(4.dp)) - if (networkLogoPath != null) { - val tmdbImage = remember(networkLogoPath) { - ShowTmdbImage(path = networkLogoPath, type = ImageType.LOGO, showId = 0) - } - - AsyncImage( - model = tmdbImage, - requestBuilder = { - crossfade(true) - transformations(TrimTransparentEdgesTransformation) - }, - contentDescription = stringResource(MR.strings.cd_network_logo), - modifier = Modifier.sizeIn(maxWidth = 72.dp, maxHeight = 32.dp), - alignment = Alignment.TopStart, - contentScale = ContentScale.Fit, - colorFilter = when { - isSystemInDarkTheme() -> ColorFilter.tint(LocalContentColor.current) - else -> null - }, - ) - } else { - Text( - text = networkName, - style = MaterialTheme.typography.bodyMedium, - ) - } + Text( + text = networkName, + style = MaterialTheme.typography.bodyMedium, + ) } } @@ -663,8 +629,6 @@ private fun RelatedShows( openShowDetails: (showId: Long) -> Unit, modifier: Modifier = Modifier, ) { - LogCompositions("RelatedShows") - val lazyListState = rememberLazyListState() val contentPadding = PaddingValues(horizontal = Layout.bodyMargin, vertical = Layout.gutter) @@ -750,7 +714,6 @@ private fun InfoPanels( if (show.network != null) { NetworkInfoPanel( networkName = show.network!!, - networkLogoPath = show.networkLogoPath, modifier = itemMod, ) } @@ -976,8 +939,6 @@ private fun ShowDetailsAppBar( modifier: Modifier = Modifier, scrollBehavior: TopAppBarScrollBehavior? = null, ) { - LogCompositions("ShowDetailsAppBar") - TopAppBar( title = { Text(text = title) }, navigationIcon = { @@ -994,7 +955,6 @@ private fun ShowDetailsAppBar( refreshing = !isRefreshing, ) }, - colors = TopAppBarDefaults.topAppBarColors(), scrollBehavior = scrollBehavior, modifier = modifier, ) @@ -1007,8 +967,6 @@ private fun ToggleShowFollowFloatingActionButton( expanded: Boolean, modifier: Modifier = Modifier, ) { - LogCompositions("ToggleShowFollowFloatingActionButton") - ExtendedFloatingActionButton( onClick = onClick, icon = { diff --git a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsComponent.kt b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsComponent.kt similarity index 100% rename from ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsComponent.kt rename to ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsComponent.kt diff --git a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsPresenter.kt b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsPresenter.kt similarity index 100% rename from ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsPresenter.kt rename to ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsPresenter.kt diff --git a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsUiState.kt b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsUiState.kt similarity index 100% rename from ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsUiState.kt rename to ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsUiState.kt diff --git a/ui/show/seasons/build.gradle.kts b/ui/show/seasons/build.gradle.kts index 79536295c7..c8564b5d5f 100644 --- a/ui/show/seasons/build.gradle.kts +++ b/ui/show/seasons/build.gradle.kts @@ -4,30 +4,32 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.showdetails.seasons" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) - api(projects.common.ui.screens) - api(libs.circuit.foundation) + api(projects.common.ui.screens) + api(libs.circuit.foundation) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material3.material3) - implementation(libs.compose.material3.windowsizeclass) - implementation(libs.compose.material.iconsext) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) - - implementation(libs.coil.compose) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(libs.compose.material3.windowsizeclass) + implementation(compose.materialIconsExtended) + implementation(compose.animation) + } + } + } } diff --git a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasons.kt b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasons.kt similarity index 96% rename from ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasons.kt rename to ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasons.kt index 44d2f3559d..c645f8c437 100644 --- a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasons.kt +++ b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasons.kt @@ -23,12 +23,15 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.CloudUpload import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.DismissValue +import androidx.compose.material.rememberDismissState import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -40,13 +43,11 @@ import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Tab import androidx.compose.material3.TabPosition import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor -import androidx.compose.material3.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -113,7 +114,7 @@ internal fun ShowSeasons( ) } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @Composable internal fun ShowSeasons( state: ShowSeasonsUiState, @@ -125,16 +126,14 @@ internal fun ShowSeasons( ) { val snackbarHostState = remember { SnackbarHostState() } - val dismissSnackbarState = rememberDismissState( - confirmValueChange = { value -> - if (value != DismissValue.Default) { - snackbarHostState.currentSnackbarData?.dismiss() - true - } else { - false - } - }, - ) + val dismissSnackbarState = rememberDismissState { value -> + if (value != DismissValue.Default) { + snackbarHostState.currentSnackbarData?.dismiss() + true + } else { + false + } + } state.message?.let { message -> LaunchedEffect(message) { diff --git a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt similarity index 100% rename from ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt rename to ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt diff --git a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsPresenter.kt b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsPresenter.kt similarity index 100% rename from ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsPresenter.kt rename to ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsPresenter.kt diff --git a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsUiState.kt b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsUiState.kt similarity index 100% rename from ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsUiState.kt rename to ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsUiState.kt diff --git a/ui/trending/build.gradle.kts b/ui/trending/build.gradle.kts index 23697ab406..c1347483bf 100644 --- a/ui/trending/build.gradle.kts +++ b/ui/trending/build.gradle.kts @@ -4,27 +4,31 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.home.trending" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) - api(projects.common.ui.screens) - api(libs.circuit.foundation) + api(projects.common.ui.screens) + api(libs.circuit.foundation) - implementation(libs.paging.compose) + implementation(libs.paging.compose) - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.animation) + } + } + } } diff --git a/ui/trending/src/main/java/app/tivi/home/trending/Trending.kt b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/Trending.kt similarity index 100% rename from ui/trending/src/main/java/app/tivi/home/trending/Trending.kt rename to ui/trending/src/commonMain/kotlin/app/tivi/home/trending/Trending.kt diff --git a/ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsComponent.kt b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsComponent.kt similarity index 100% rename from ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsComponent.kt rename to ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsComponent.kt diff --git a/ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsPresenter.kt b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsPresenter.kt similarity index 100% rename from ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsPresenter.kt rename to ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsPresenter.kt diff --git a/ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsUiState.kt b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsUiState.kt similarity index 100% rename from ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsUiState.kt rename to ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsUiState.kt diff --git a/ui/upnext/build.gradle.kts b/ui/upnext/build.gradle.kts index c7ebd8b0e3..fe4c573b9a 100644 --- a/ui/upnext/build.gradle.kts +++ b/ui/upnext/build.gradle.kts @@ -4,36 +4,36 @@ plugins { id("app.tivi.android.library") - id("app.tivi.android.compose") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } android { namespace = "app.tivi.home.upnext" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) - - api(projects.common.ui.screens) - api(projects.common.ui.circuitOverlay) - api(libs.circuit.foundation) - - implementation(libs.paging.compose) - - implementation(libs.swipe) - - implementation(libs.androidx.core) - - implementation(libs.compose.foundation.foundation) - implementation(libs.compose.foundation.layout) - implementation(libs.compose.material.material) - implementation(libs.compose.material.iconsext) - implementation(libs.compose.material3.material3) - implementation(libs.compose.animation.animation) - implementation(libs.compose.ui.tooling) - - implementation(libs.coil.compose) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) + + api(projects.common.ui.screens) + api(projects.common.ui.circuitOverlay) + api(libs.circuit.foundation) + + implementation(libs.paging.compose) + + implementation(libs.swipe) + + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.materialIconsExtended) + implementation(compose.material3) + implementation(compose.animation) + } + } + } } diff --git a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNext.kt b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNext.kt similarity index 94% rename from ui/upnext/src/main/java/app/tivi/home/upnext/UpNext.kt rename to ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNext.kt index 38424b1a31..c0ccc4dd53 100644 --- a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNext.kt +++ b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNext.kt @@ -22,15 +22,17 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material.DismissValue import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.rememberDismissState import androidx.compose.material3.Card -import androidx.compose.material3.DismissValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon @@ -39,10 +41,8 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberDismissState import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -81,7 +81,7 @@ import app.tivi.data.traktauth.TraktAuthState import app.tivi.overlays.showInDialog import app.tivi.screens.AccountScreen import app.tivi.screens.UpNextScreen -import coil.compose.AsyncImagePainter +import com.seiko.imageloader.ImageRequestState import com.slack.circuit.overlay.LocalOverlayHost import com.slack.circuit.runtime.CircuitContext import com.slack.circuit.runtime.Screen @@ -152,16 +152,14 @@ internal fun UpNext( ) { val snackbarHostState = remember { SnackbarHostState() } - val dismissSnackbarState = rememberDismissState( - confirmValueChange = { value -> - if (value != DismissValue.Default) { - snackbarHostState.currentSnackbarData?.dismiss() - true - } else { - false - } - }, - ) + val dismissSnackbarState = rememberDismissState { value -> + if (value != DismissValue.Default) { + snackbarHostState.currentSnackbarData?.dismiss() + true + } else { + false + } + } state.message?.let { message -> LaunchedEffect(message) { @@ -383,13 +381,10 @@ private fun UpNextItem( AsyncImage( model = model, - requestBuilder = { crossfade(true) }, onState = { state -> - if (state is AsyncImagePainter.State.Error) { - if (state.result.request.data is EpisodeImageModel) { - // If the episode backdrop request failed, fallback to the show backdrop - model = show.asImageModel(ImageType.BACKDROP) - } + if (state is ImageRequestState.Failure && model is EpisodeImageModel) { + // If the episode backdrop request failed, fallback to the show backdrop + model = show.asImageModel(ImageType.BACKDROP) } }, contentDescription = null, diff --git a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNextComponent.kt b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextComponent.kt similarity index 100% rename from ui/upnext/src/main/java/app/tivi/home/upnext/UpNextComponent.kt rename to ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextComponent.kt diff --git a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNextPresenter.kt b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextPresenter.kt similarity index 100% rename from ui/upnext/src/main/java/app/tivi/home/upnext/UpNextPresenter.kt rename to ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextPresenter.kt diff --git a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNextUiState.kt b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextUiState.kt similarity index 100% rename from ui/upnext/src/main/java/app/tivi/home/upnext/UpNextUiState.kt rename to ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextUiState.kt From 6d828105c023ed83bf78d6b96c110d88789f30ce Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Wed, 28 Jun 2023 11:22:17 +0100 Subject: [PATCH 02/23] Migrate to github.com/benasher44/uuid --- common/ui/compose/build.gradle.kts | 2 ++ .../commonMain/kotlin/app/tivi/common/compose/UiMessage.kt | 6 +++--- gradle/libs.versions.toml | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/common/ui/compose/build.gradle.kts b/common/ui/compose/build.gradle.kts index e128728d4c..213ac4f62c 100644 --- a/common/ui/compose/build.gradle.kts +++ b/common/ui/compose/build.gradle.kts @@ -31,6 +31,8 @@ kotlin { api(libs.insetsx) + implementation(libs.uuid) + implementation(libs.paging.compose) } } diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/UiMessage.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/UiMessage.kt index be127a2365..0e723106be 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/UiMessage.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/UiMessage.kt @@ -3,7 +3,7 @@ package app.tivi.common.compose -import java.util.UUID +import com.benasher44.uuid.uuid4 import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged @@ -13,12 +13,12 @@ import kotlinx.coroutines.sync.withLock data class UiMessage( val message: String, - val id: Long = UUID.randomUUID().mostSignificantBits, + val id: Long = uuid4().mostSignificantBits, ) fun UiMessage( t: Throwable, - id: Long = UUID.randomUUID().mostSignificantBits, + id: Long = uuid4().mostSignificantBits, ): UiMessage = UiMessage( message = t.message ?: "Error occurred: $t", id = id, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64d07ddcf8..5cdaf9056a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -157,6 +157,8 @@ assertk = "com.willowtreeapps.assertk:assertk:0.26.1" turbine = "app.cash.turbine:turbine:1.0.0" +uuid = "com.benasher44:uuid:0.7.1" + # Build logic dependencies android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } From 8e1c181b63c2c7354a8ef9a87aa20a494f6544d8 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Wed, 28 Jun 2023 11:29:44 +0100 Subject: [PATCH 03/23] Move use of Android's dynamic color scheme to androidMain --- .../app/tivi/common/compose/theme/Platform.kt | 26 +++++++++++++++++++ .../app/tivi/common/compose/theme/Platform.kt | 16 ++++++++++++ .../app/tivi/common/compose/theme/Theme.kt | 15 +---------- .../app/tivi/common/compose/theme/Platform.kt | 16 ++++++++++++ .../app/tivi/common/compose/theme/Platform.kt | 16 ++++++++++++ 5 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/theme/Platform.kt create mode 100644 common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt create mode 100644 common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/theme/Platform.kt create mode 100644 common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/theme/Platform.kt diff --git a/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/theme/Platform.kt b/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/theme/Platform.kt new file mode 100644 index 0000000000..74b009de42 --- /dev/null +++ b/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/theme/Platform.kt @@ -0,0 +1,26 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose.theme + +import android.os.Build +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +@Composable +internal actual fun colorScheme( + useDarkColors: Boolean, + useDynamicColors: Boolean, +): ColorScheme = when { + Build.VERSION.SDK_INT >= 31 && useDynamicColors && useDarkColors -> { + dynamicDarkColorScheme(LocalContext.current) + } + Build.VERSION.SDK_INT >= 31 && useDynamicColors && !useDarkColors -> { + dynamicLightColorScheme(LocalContext.current) + } + useDarkColors -> TiviDarkColors + else -> TiviLightColors +} diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt new file mode 100644 index 0000000000..5afd83dac5 --- /dev/null +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt @@ -0,0 +1,16 @@ +// Copyright 2020, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +internal expect fun colorScheme( + useDarkColors: Boolean, + useDynamicColors: Boolean, +): ColorScheme diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Theme.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Theme.kt index 87e34c5add..dc51ccf6e8 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Theme.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Theme.kt @@ -3,13 +3,9 @@ package app.tivi.common.compose.theme -import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext @Composable fun TiviTheme( @@ -18,16 +14,7 @@ fun TiviTheme( content: @Composable () -> Unit, ) { MaterialTheme( - colorScheme = when { - Build.VERSION.SDK_INT >= 31 && useDynamicColors && useDarkColors -> { - dynamicDarkColorScheme(LocalContext.current) - } - Build.VERSION.SDK_INT >= 31 && useDynamicColors && !useDarkColors -> { - dynamicLightColorScheme(LocalContext.current) - } - useDarkColors -> TiviDarkColors - else -> TiviLightColors - }, + colorScheme = colorScheme(useDarkColors, useDynamicColors), typography = TiviTypography, shapes = TiviShapes, content = content, diff --git a/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/theme/Platform.kt b/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/theme/Platform.kt new file mode 100644 index 0000000000..ae74115409 --- /dev/null +++ b/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/theme/Platform.kt @@ -0,0 +1,16 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +@Composable +internal actual fun colorScheme( + useDarkColors: Boolean, + useDynamicColors: Boolean, +): ColorScheme = when { + useDarkColors -> TiviDarkColors + else -> TiviLightColors +} diff --git a/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/theme/Platform.kt b/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/theme/Platform.kt new file mode 100644 index 0000000000..ae74115409 --- /dev/null +++ b/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/theme/Platform.kt @@ -0,0 +1,16 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.common.compose.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +@Composable +internal actual fun colorScheme( + useDarkColors: Boolean, + useDynamicColors: Boolean, +): ColorScheme = when { + useDarkColors -> TiviDarkColors + else -> TiviLightColors +} From 522590e621db28a251169b3f0dc172564f005aac Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Wed, 28 Jun 2023 11:43:12 +0100 Subject: [PATCH 04/23] More migrations --- common/ui/compose/build.gradle.kts | 2 ++ .../common/compose/ui/DateTimeTextFields.kt | 18 +++++------ .../tivi/common/compose/ui/GradientScrim.kt | 5 ++-- .../tivi/common/compose/ui/IconButtonScrim.kt | 3 +- .../tivi/common/compose/ui/TiviAlertDialog.kt | 30 +++++++------------ .../kotlin/app/tivi/home/search/Search.kt | 2 +- .../tivi/showdetails/seasons/ShowSeasons.kt | 2 +- 7 files changed, 26 insertions(+), 36 deletions(-) diff --git a/common/ui/compose/build.gradle.kts b/common/ui/compose/build.gradle.kts index 213ac4f62c..7a21ed647d 100644 --- a/common/ui/compose/build.gradle.kts +++ b/common/ui/compose/build.gradle.kts @@ -31,6 +31,8 @@ kotlin { api(libs.insetsx) + implementation(libs.materialdialogs.core) + implementation(libs.uuid) implementation(libs.paging.compose) diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt index e567900876..1f84ee67ba 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt @@ -3,7 +3,6 @@ package app.tivi.common.compose.ui -import android.text.format.DateFormat import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -22,7 +21,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import app.tivi.common.compose.LocalTiviDateFormatter import app.tivi.common.ui.resources.MR @@ -109,7 +107,7 @@ fun TimeTextField( selectedTime: LocalTime?, onTimeSelected: (LocalTime) -> Unit, modifier: Modifier = Modifier, - is24Hour: Boolean = TimeTextFieldDefaults.is24Hour, + is24Hour: Boolean = true, // FIXME ) { var showPicker by remember { mutableStateOf(false) } @@ -162,13 +160,13 @@ fun TimeTextField( } } -object TimeTextFieldDefaults { - val is24Hour: Boolean - @Composable get() { - val context = LocalContext.current - return remember { DateFormat.is24HourFormat(context) } - } -} +//object TimeTextFieldDefaults { +// val is24Hour: Boolean +// @Composable get() { +// val context = LocalContext.current +// return remember { DateFormat.is24HourFormat(context) } +// } +//} @ExperimentalMaterial3Api @Composable diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/GradientScrim.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/GradientScrim.kt index 0b303ff813..69d50c6c36 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/GradientScrim.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/GradientScrim.kt @@ -3,7 +3,6 @@ package app.tivi.common.compose.ui -import androidx.annotation.FloatRange import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed @@ -26,8 +25,8 @@ fun Modifier.drawForegroundGradientScrim( color: Color, decay: Float = 1.5f, numStops: Int = 16, - @FloatRange(from = 0.0, to = 1.0) startY: Float = 0f, - @FloatRange(from = 0.0, to = 1.0) endY: Float = 1f, + startY: Float = 0f, + endY: Float = 1f, ): Modifier = composed { val colors = remember(color, numStops) { val baseAlpha = color.alpha diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/IconButtonScrim.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/IconButtonScrim.kt index 07136f925a..d0490fb1d6 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/IconButtonScrim.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/IconButtonScrim.kt @@ -3,7 +3,6 @@ package app.tivi.common.compose.ui -import androidx.annotation.FloatRange import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding @@ -47,7 +46,7 @@ fun ScrimmedIconButton( private fun ScrimSurface( modifier: Modifier = Modifier, showScrim: Boolean = true, - @FloatRange(from = 0.0, to = 1.0) alpha: Float = 0.3f, + alpha: Float = 0.3f, icon: @Composable () -> Unit, ) { Surface( diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TiviAlertDialog.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TiviAlertDialog.kt index f2169763d2..6eaa9fda71 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TiviAlertDialog.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TiviAlertDialog.kt @@ -3,11 +3,10 @@ package app.tivi.common.compose.ui -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import com.vanpra.composematerialdialogs.MaterialDialog +import com.vanpra.composematerialdialogs.message +import com.vanpra.composematerialdialogs.title @Composable fun TiviAlertDialog( @@ -18,19 +17,12 @@ fun TiviAlertDialog( dismissText: String, onDismissRequest: () -> Unit, ) { - AlertDialog( - title = { Text(text = title) }, - text = { Text(text = message) }, - confirmButton = { - OutlinedButton(onClick = { onConfirm() }) { - Text(text = confirmText) - } - }, - dismissButton = { - TextButton(onClick = { onDismissRequest() }) { - Text(text = dismissText) - } - }, - onDismissRequest = onDismissRequest, - ) + MaterialDialog( + onCloseRequest = { onDismissRequest() }, + ) { + title(title) + message(message) + dialogButtons.positiveButton(text = confirmText, onClick = onConfirm) + dialogButtons.negativeButton(text = dismissText, onClick = onDismissRequest) + } } diff --git a/ui/search/src/commonMain/kotlin/app/tivi/home/search/Search.kt b/ui/search/src/commonMain/kotlin/app/tivi/home/search/Search.kt index a06809d1f0..3ff192786b 100644 --- a/ui/search/src/commonMain/kotlin/app/tivi/home/search/Search.kt +++ b/ui/search/src/commonMain/kotlin/app/tivi/home/search/Search.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -52,6 +51,7 @@ import app.tivi.common.compose.ui.plus import app.tivi.common.ui.resources.MR import app.tivi.data.models.TiviShow import app.tivi.screens.SearchScreen +import com.moriatsushi.insetsx.statusBarsPadding import com.slack.circuit.runtime.CircuitContext import com.slack.circuit.runtime.Screen import com.slack.circuit.runtime.ui.Ui diff --git a/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasons.kt b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasons.kt index c645f8c437..1778e148a7 100644 --- a/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasons.kt +++ b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasons.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn @@ -74,6 +73,7 @@ import app.tivi.data.compoundmodels.SeasonWithEpisodesAndWatches import app.tivi.data.models.Episode import app.tivi.data.models.Season import app.tivi.screens.ShowSeasonsScreen +import com.moriatsushi.insetsx.statusBars import com.slack.circuit.runtime.CircuitContext import com.slack.circuit.runtime.Screen import com.slack.circuit.runtime.ui.Ui From 01867de881188280250ecc187347271420a909a3 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Wed, 28 Jun 2023 20:38:48 +0100 Subject: [PATCH 05/23] Import github.com/saket/swipe We need to build it again Compose MP --- .../main/kotlin/app/tivi/gradle/Spotless.kt | 9 +- gradle/libs.versions.toml | 2 - settings.gradle.kts | 1 + thirdparty/swipe/build.gradle.kts | 23 +++ .../kotlin/me/saket/swipe/ActionFinder.kt | 58 +++++++ .../kotlin/me/saket/swipe/SwipeAction.kt | 77 +++++++++ .../kotlin/me/saket/swipe/SwipeRipple.kt | 91 ++++++++++ .../me/saket/swipe/SwipeableActionsBox.kt | 162 ++++++++++++++++++ .../me/saket/swipe/SwipeableActionsState.kt | 62 +++++++ .../kotlin/me/saket/swipe/defaults.kt | 3 + ui/upnext/build.gradle.kts | 2 +- 11 files changed, 485 insertions(+), 5 deletions(-) create mode 100644 thirdparty/swipe/build.gradle.kts create mode 100644 thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/ActionFinder.kt create mode 100644 thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeAction.kt create mode 100644 thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeRipple.kt create mode 100644 thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsBox.kt create mode 100644 thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsState.kt create mode 100644 thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/defaults.kt diff --git a/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Spotless.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Spotless.kt index faa9a00a2d..c1056aebad 100644 --- a/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Spotless.kt +++ b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Spotless.kt @@ -8,6 +8,11 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.configure fun Project.configureSpotless() { + if (path.startsWith(":thirdparty")) { + println("Skipping Spotless") + return + } + val ktlintVersion = libs.findVersion("ktlint").get().requiredVersion with(pluginManager) { @@ -16,7 +21,7 @@ fun Project.configureSpotless() { spotless { kotlin { - target("**/*.kt") + target("src/**/*.kt") targetExclude("$buildDir/**/*.kt") targetExclude("bin/**/*.kt") ktlint(ktlintVersion) @@ -32,7 +37,7 @@ fun Project.configureSpotless() { } kotlinGradle { - target("**/*.kts") + target("src/**/*.kts") targetExclude("$buildDir/**/*.kts") ktlint(ktlintVersion) licenseHeaderFile(rootProject.file("spotless/google-copyright.txt"), "(^(?![\\/ ]\\**).*$)") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5cdaf9056a..737f302129 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -137,8 +137,6 @@ playservices-blockstore = "com.google.android.gms:play-services-auth-blockstore: robolectric = "org.robolectric:robolectric:4.10.3" -swipe = "me.saket.swipe:swipe:1.2.0" - store = "org.mobilenativefoundation.store:store5:5.0.0-beta01" sqldelight-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7bf465881c..f12ca9c51e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -98,4 +98,5 @@ include( ":android-app:app", ":android-app:benchmark", ":android-app:common-test", + ":thirdparty:swipe", ) diff --git a/thirdparty/swipe/build.gradle.kts b/thirdparty/swipe/build.gradle.kts new file mode 100644 index 0000000000..78d44f5d42 --- /dev/null +++ b/thirdparty/swipe/build.gradle.kts @@ -0,0 +1,23 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + + +plugins { + id("app.tivi.android.library") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(compose.foundation) + } + } + } +} + +android { + namespace = "me.saket.swipe" +} diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/ActionFinder.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/ActionFinder.kt new file mode 100644 index 0000000000..758536050b --- /dev/null +++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/ActionFinder.kt @@ -0,0 +1,58 @@ +package me.saket.swipe + +import kotlin.math.abs + +internal data class SwipeActionMeta( + val value: SwipeAction, + val isOnRightSide: Boolean, +) + +internal data class ActionFinder( + private val left: List, + private val right: List +) { + + fun actionAt(offset: Float, totalWidth: Int): SwipeActionMeta? { + if (offset == 0f) { + return null + } + + val isOnRightSide = offset < 0f + val actions = if (isOnRightSide) right else left + + val actionAtOffset = actions.actionAt( + offset = abs(offset).coerceAtMost(totalWidth.toFloat()), + totalWidth = totalWidth + ) + return actionAtOffset?.let { + SwipeActionMeta( + value = actionAtOffset, + isOnRightSide = isOnRightSide + ) + } + } + + private fun List.actionAt(offset: Float, totalWidth: Int): SwipeAction? { + if (isEmpty()) { + return null + } + + val totalWeights = this.sumOf { it.weight } + var offsetSoFar = 0.0 + + @Suppress("ReplaceManualRangeWithIndicesCalls") // Avoid allocating an Iterator for every pixel swiped. + for (i in 0 until size) { + val action = this[i] + val actionWidth = (action.weight / totalWeights) * totalWidth + val actionEndX = offsetSoFar + actionWidth + + if (offset <= actionEndX) { + return action + } + offsetSoFar += actionEndX + } + + // Precision error in the above loop maybe? + error("Couldn't find any swipe action. Width=$totalWidth, offset=$offset, actions=$this") + } +} diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeAction.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeAction.kt new file mode 100644 index 0000000000..5fc3986669 --- /dev/null +++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeAction.kt @@ -0,0 +1,77 @@ +package me.saket.swipe + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp + +/** + * Represents an action that can be shown in [SwipeableActionsBox]. + * + * @param background Color used as the background of [SwipeableActionsBox] while + * this action is visible. If this action is swiped, its background color is + * also used for drawing a ripple over the content for providing a visual + * feedback to the user. + * + * @param weight The proportional width to give to this element, as related + * to the total of all weighted siblings. [SwipeableActionsBox] will divide its + * horizontal space and distribute it to actions according to their weight. + * + * @param isUndo Determines the direction in which a ripple is drawn when this + * action is swiped. When false, the ripple grows from this action's position + * to consume the entire composable, and vice versa. This can be used for + * actions that can be toggled on and off. + */ +class SwipeAction( + val onSwipe: () -> Unit, + val icon: @Composable () -> Unit, + val background: Color, + val weight: Double = 1.0, + val isUndo: Boolean = false +) { + init { + require(weight > 0.0) { "invalid weight $weight; must be greater than zero" } + } + + fun copy( + onSwipe: () -> Unit = this.onSwipe, + icon: @Composable () -> Unit = this.icon, + background: Color = this.background, + weight: Double = this.weight, + isUndo: Boolean = this.isUndo, + ) = SwipeAction( + onSwipe = onSwipe, + icon = icon, + background = background, + weight = weight, + isUndo = isUndo + ) +} + +/** + * See [SwipeAction] for documentation. + */ +fun SwipeAction( + onSwipe: () -> Unit, + icon: Painter, + background: Color, + weight: Double = 1.0, + isUndo: Boolean = false +): SwipeAction { + return SwipeAction( + icon = { + Image( + modifier = Modifier.padding(16.dp), + painter = icon, + contentDescription = null + ) + }, + background = background, + weight = weight, + onSwipe = onSwipe, + isUndo = isUndo + ) +} diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeRipple.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeRipple.kt new file mode 100644 index 0000000000..b93e62c6b9 --- /dev/null +++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeRipple.kt @@ -0,0 +1,91 @@ +@file:Suppress("NAME_SHADOWING") + +package me.saket.swipe + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipRect +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +@Stable +internal class SwipeRippleState { + private var ripple = mutableStateOf(null) + + suspend fun animate( + action: SwipeActionMeta, + ) { + val drawOnRightSide = action.isOnRightSide + val action = action.value + + ripple.value = SwipeRipple( + isUndo = action.isUndo, + rightSide = drawOnRightSide, + color = action.background, + alpha = 0f, + progress = 0f + ) + + // Reverse animation feels faster (especially for larger swipe distances) so slow it down further. + val animationDurationMs = (animationDurationMs * (if (action.isUndo) 1.75f else 1f)).roundToInt() + + coroutineScope { + launch { + Animatable(initialValue = 0f).animateTo( + targetValue = 1f, + animationSpec = tween(durationMillis = animationDurationMs), + block = { + ripple.value = ripple.value!!.copy(progress = value) + } + ) + } + launch { + Animatable(initialValue = if (action.isUndo) 0f else 0.25f).animateTo( + targetValue = if (action.isUndo) 0.5f else 0f, + animationSpec = tween( + durationMillis = animationDurationMs, + delayMillis = if (action.isUndo) 0 else animationDurationMs / 2 + ), + block = { + ripple.value = ripple.value!!.copy(alpha = value) + } + ) + } + } + } + + fun draw(scope: DrawScope) { + ripple.value?.run { + scope.clipRect { + val size = scope.size + // Start the ripple with a radius equal to the available height so that it covers the entire edge. + val startRadius = if (isUndo) size.width + size.height else size.height + val endRadius = if (!isUndo) size.width + size.height else size.height + val radius = lerp(startRadius, endRadius, fraction = progress) + + drawCircle( + color = color, + radius = radius, + alpha = alpha, + center = this.center.copy(x = if (rightSide) this.size.width + this.size.height else 0f - this.size.height) + ) + } + } + } +} + +private data class SwipeRipple( + val isUndo: Boolean, + val rightSide: Boolean, + val color: Color, + val alpha: Float, + val progress: Float, +) + +private fun lerp(start: Float, stop: Float, fraction: Float) = + (start * (1 - fraction) + stop * fraction) diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsBox.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsBox.kt new file mode 100644 index 0000000000..91a5f2e0e5 --- /dev/null +++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsBox.kt @@ -0,0 +1,162 @@ +@file:Suppress("NAME_SHADOWING") + +package me.saket.swipe + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation.Horizontal +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.roundToInt + +/** + * A composable that can be swiped left or right for revealing actions. + * + * @param swipeThreshold Minimum drag distance before any [SwipeAction] is + * activated and can be swiped. + * + * @param backgroundUntilSwipeThreshold Color drawn behind the content until + * [swipeThreshold] is reached. When the threshold is passed, this color is + * replaced by the currently visible [SwipeAction]'s background. + */ +@Composable +fun SwipeableActionsBox( + modifier: Modifier = Modifier, + state: SwipeableActionsState = rememberSwipeableActionsState(), + startActions: List = emptyList(), + endActions: List = emptyList(), + swipeThreshold: Dp = 40.dp, + backgroundUntilSwipeThreshold: Color = Color.DarkGray, + content: @Composable BoxScope.() -> Unit +) = BoxWithConstraints(modifier) { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + val leftActions = if (isRtl) endActions else startActions + val rightActions = if (isRtl) startActions else endActions + val swipeThresholdPx = LocalDensity.current.run { swipeThreshold.toPx() } + + val ripple = remember { + SwipeRippleState() + } + val actions = remember(leftActions, rightActions) { + ActionFinder(left = leftActions, right = rightActions) + } + LaunchedEffect(state, actions) { + state.run { + canSwipeTowardsRight = { leftActions.isNotEmpty() } + canSwipeTowardsLeft = { rightActions.isNotEmpty() } + } + } + + val offset = state.offset.value + val thresholdCrossed = abs(offset) > swipeThresholdPx + + var swipedAction: SwipeActionMeta? by remember { + mutableStateOf(null) + } + val visibleAction: SwipeActionMeta? = remember(offset, actions) { + actions.actionAt(offset, totalWidth = constraints.maxWidth) + } + val backgroundColor: Color by animateColorAsState( + when { + swipedAction != null -> swipedAction!!.value.background + !thresholdCrossed -> backgroundUntilSwipeThreshold + visibleAction == null -> Color.Transparent + else -> visibleAction.value.background + } + ) + + val scope = rememberCoroutineScope() + Box( + modifier = Modifier + .absoluteOffset { IntOffset(x = offset.roundToInt(), y = 0) } + .drawOverContent { ripple.draw(scope = this) } + .draggable( + orientation = Horizontal, + enabled = !state.isResettingOnRelease, + onDragStopped = { + scope.launch { + if (thresholdCrossed && visibleAction != null) { + swipedAction = visibleAction + swipedAction!!.value.onSwipe() + ripple.animate(action = swipedAction!!) + } + } + scope.launch { + state.resetOffset() + swipedAction = null + } + }, + state = state.draggableState, + ), + content = content + ) + + (swipedAction ?: visibleAction)?.let { action -> + ActionIconBox( + modifier = Modifier.matchParentSize(), + action = action, + offset = offset, + backgroundColor = backgroundColor, + content = { action.value.icon() } + ) + } +} + +@Composable +private fun ActionIconBox( + action: SwipeActionMeta, + offset: Float, + backgroundColor: Color, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Row( + modifier = modifier + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(width = placeable.width, height = placeable.height) { + // Align icon with the left/right edge of the content being swiped. + val iconOffset = if (action.isOnRightSide) constraints.maxWidth + offset else offset - placeable.width + placeable.placeRelative(x = iconOffset.roundToInt(), y = 0) + } + } + .background(color = backgroundColor), + horizontalArrangement = if (action.isOnRightSide) Arrangement.Start else Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + content() + } +} + +private fun Modifier.drawOverContent(onDraw: DrawScope.() -> Unit): Modifier { + return drawWithContent { + drawContent() + onDraw(this) + } +} diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsState.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsState.kt new file mode 100644 index 0000000000..d449928757 --- /dev/null +++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsState.kt @@ -0,0 +1,62 @@ +package me.saket.swipe + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.DraggableState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +@Composable +fun rememberSwipeableActionsState(): SwipeableActionsState { + return remember { SwipeableActionsState() } +} + +/** + * The state of a [SwipeableActionsBox]. + */ +@Stable +class SwipeableActionsState internal constructor() { + /** + * The current position (in pixels) of a [SwipeableActionsBox]. + */ + val offset: State get() = offsetState + internal var offsetState = mutableStateOf(0f) + + /** + * Whether [SwipeableActionsBox] is currently animating to reset its offset after it was swiped. + */ + var isResettingOnRelease: Boolean by mutableStateOf(false) + private set + + internal lateinit var canSwipeTowardsRight: () -> Boolean + internal lateinit var canSwipeTowardsLeft: () -> Boolean + + internal val draggableState = DraggableState { delta -> + val targetOffset = offsetState.value + delta + val isAllowed = isResettingOnRelease + || targetOffset > 0f && canSwipeTowardsRight() + || targetOffset < 0f && canSwipeTowardsLeft() + + // Add some resistance if needed. + offsetState.value += if (isAllowed) delta else delta / 10 + } + + internal suspend fun resetOffset() { + draggableState.drag(MutatePriority.PreventUserInput) { + isResettingOnRelease = true + try { + Animatable(offsetState.value).animateTo(targetValue = 0f, tween(durationMillis = animationDurationMs)) { + dragBy(value - offsetState.value) + } + } finally { + isResettingOnRelease = false + } + } + } +} diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/defaults.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/defaults.kt new file mode 100644 index 0000000000..04065de5d4 --- /dev/null +++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/defaults.kt @@ -0,0 +1,3 @@ +package me.saket.swipe + +internal const val animationDurationMs = 4_00 diff --git a/ui/upnext/build.gradle.kts b/ui/upnext/build.gradle.kts index fe4c573b9a..56b73b2745 100644 --- a/ui/upnext/build.gradle.kts +++ b/ui/upnext/build.gradle.kts @@ -26,7 +26,7 @@ kotlin { implementation(libs.paging.compose) - implementation(libs.swipe) + implementation(projects.thirdparty.swipe) implementation(compose.foundation) implementation(compose.material) From 40090f5dfd3d43096ecc05653ee4e002f718ba2f Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Wed, 28 Jun 2023 20:53:53 +0100 Subject: [PATCH 06/23] Remove last Android SDK usage in common --- .../app/tivi/common/compose/ReportDrawnWhen.kt | 7 ++++++- .../kotlin/app/tivi/home/discover/Discover.kt | 15 +++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt b/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt index e0547c7d0a..cba637b33c 100644 --- a/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt +++ b/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt @@ -3,9 +3,14 @@ package app.tivi.common.compose +import android.os.Build import androidx.compose.runtime.Composable @Composable actual fun ReportDrawnWhen(predicate: () -> Boolean) { - androidx.activity.compose.ReportDrawnWhen(predicate) + // ReportDrawnWhen routinely causes crashes on API < 25: + // https://issuetracker.google.com/issues/260506820 + if (Build.VERSION.SDK_INT >= 25) { + androidx.activity.compose.ReportDrawnWhen(predicate) + } } diff --git a/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/Discover.kt b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/Discover.kt index aa3e831fbb..a070127abd 100644 --- a/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/Discover.kt +++ b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/Discover.kt @@ -5,7 +5,6 @@ package app.tivi.home.discover -import android.os.Build import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider @@ -155,15 +154,11 @@ internal fun Discover( } } - if (Build.VERSION.SDK_INT >= 25) { - // ReportDrawnWhen routinely causes crashes on API < 25: - // https://issuetracker.google.com/issues/260506820 - ReportDrawnWhen { - !state.popularRefreshing && - !state.trendingRefreshing && - state.popularItems.isNotEmpty() && - state.trendingItems.isNotEmpty() - } + ReportDrawnWhen { + !state.popularRefreshing && + !state.trendingRefreshing && + state.popularItems.isNotEmpty() && + state.trendingItems.isNotEmpty() } Scaffold( From 958c3e81b4a4615435af3ab39f76af0114218a05 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Wed, 28 Jun 2023 21:15:34 +0100 Subject: [PATCH 07/23] Build :shared on iOS! --- gradle.properties | 3 +++ shared/build.gradle.kts | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/gradle.properties b/gradle.properties index ef9660623b..a4d8b5d84a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,4 +36,7 @@ android.defaults.buildFeatures.buildConfig=false kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidGradlePluginCompatibility.nowarn=true +# https://github.com/JetBrains/compose-multiplatform/issues/2046 +kotlin.native.cacheKind=none + org.jetbrains.compose.experimental.uikit.enabled=true diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index a62267e2ab..fa5bf3abfb 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -2,12 +2,22 @@ // SPDX-License-Identifier: Apache-2.0 +import org.jetbrains.kotlin.gradle.plugin.mpp.Framework +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + plugins { id("app.tivi.android.library") id("app.tivi.kotlin.multiplatform") } kotlin { + targets.withType { + binaries.withType { + isStatic = true + baseName = "Tivi" + } + } + sourceSets { val commonMain by getting { dependencies { From cf84499b695d5ff15a1d4593ca9918b9a01e5858 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Wed, 28 Jun 2023 21:59:20 +0100 Subject: [PATCH 08/23] Move :ui:account to KMP --- .../inject/AndroidApplicationComponent.kt | 3 -- shared/build.gradle.kts | 3 +- .../tivi/inject/SharedApplicationComponent.kt | 14 +++++++- .../kotlin/app/tivi/inject/UiComponent.kt | 11 +++--- ui/account/build.gradle.kts | 36 ++++++++++--------- .../app/tivi/account/AccountComponent.kt | 0 .../app/tivi/account/AccountPresenter.kt | 0 .../kotlin/app/tivi/account/AccountUi.kt | 0 .../kotlin/app/tivi/account/AccountUiState.kt | 0 ui/account/src/main/AndroidManifest.xml | 19 ---------- 10 files changed, 39 insertions(+), 47 deletions(-) rename {android-app/app/src/main => shared/src/commonMain}/kotlin/app/tivi/inject/UiComponent.kt (88%) rename ui/account/src/{main => commonMain}/kotlin/app/tivi/account/AccountComponent.kt (100%) rename ui/account/src/{main => commonMain}/kotlin/app/tivi/account/AccountPresenter.kt (100%) rename ui/account/src/{main => commonMain}/kotlin/app/tivi/account/AccountUi.kt (100%) rename ui/account/src/{main => commonMain}/kotlin/app/tivi/account/AccountUiState.kt (100%) delete mode 100644 ui/account/src/main/AndroidManifest.xml diff --git a/android-app/app/src/main/kotlin/app/tivi/inject/AndroidApplicationComponent.kt b/android-app/app/src/main/kotlin/app/tivi/inject/AndroidApplicationComponent.kt index 98967b569a..6568f9b47f 100644 --- a/android-app/app/src/main/kotlin/app/tivi/inject/AndroidApplicationComponent.kt +++ b/android-app/app/src/main/kotlin/app/tivi/inject/AndroidApplicationComponent.kt @@ -13,7 +13,6 @@ import app.tivi.app.Flavor import app.tivi.appinitializers.AppInitializer import app.tivi.appinitializers.AppInitializers import app.tivi.appinitializers.EmojiInitializer -import app.tivi.common.imageloading.ImageLoadingComponent import app.tivi.home.ContentViewSetterComponent import app.tivi.tasks.TiviWorkerFactory import java.io.File @@ -32,8 +31,6 @@ import okhttp3.OkHttpClient abstract class AndroidApplicationComponent( @get:Provides val application: Application, ) : SharedApplicationComponent, - UiComponent, - ImageLoadingComponent, VariantAwareComponent, ContentViewSetterComponent { diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index fa5bf3abfb..98ac90f167 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -8,6 +8,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { id("app.tivi.android.library") id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) } kotlin { @@ -36,7 +37,7 @@ kotlin { api(projects.common.imageloading) api(projects.common.ui.compose) - // api(projects.ui.account) + api(projects.ui.account) api(projects.ui.discover) api(projects.ui.episode.details) api(projects.ui.episode.track) diff --git a/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt b/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt index 5b9906aeb5..aef2ffa303 100644 --- a/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt +++ b/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt @@ -3,8 +3,12 @@ package app.tivi.inject +<<<<<<< HEAD import app.tivi.appinitializers.AppInitializer import app.tivi.appinitializers.TmdbInitializer +======= +import app.tivi.common.imageloading.ImageLoadingComponent +>>>>>>> a8436cacd (Move :ui:account to KMP) import app.tivi.core.analytics.AnalyticsComponent import app.tivi.core.perf.PerformanceComponent import app.tivi.data.SqlDelightDatabaseComponent @@ -29,14 +33,19 @@ import app.tivi.util.LoggerComponent import app.tivi.util.PowerControllerComponent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO +<<<<<<< HEAD import me.tatarka.inject.annotations.IntoSet +======= +>>>>>>> a8436cacd (Move :ui:account to KMP) import me.tatarka.inject.annotations.Provides interface SharedApplicationComponent : ApiComponent, TasksComponent, CoreComponent, - DataComponent + DataComponent, + ImageLoadingComponent, + UiComponent interface ApiComponent : TmdbComponent, TraktComponent @@ -58,9 +67,12 @@ interface CoreComponent : main = Dispatchers.Main, ) +<<<<<<< HEAD @Provides @IntoSet fun provideTmdbInitializer(bind: TmdbInitializer): AppInitializer = bind +======= +>>>>>>> a8436cacd (Move :ui:account to KMP) } interface DataComponent : diff --git a/android-app/app/src/main/kotlin/app/tivi/inject/UiComponent.kt b/shared/src/commonMain/kotlin/app/tivi/inject/UiComponent.kt similarity index 88% rename from android-app/app/src/main/kotlin/app/tivi/inject/UiComponent.kt rename to shared/src/commonMain/kotlin/app/tivi/inject/UiComponent.kt index d908935516..73364dd9d3 100644 --- a/android-app/app/src/main/kotlin/app/tivi/inject/UiComponent.kt +++ b/shared/src/commonMain/kotlin/app/tivi/inject/UiComponent.kt @@ -33,15 +33,14 @@ interface UiComponent : ShowSeasonsComponent, TrendingShowsComponent, UpNextComponent { + @Provides @ApplicationScope fun provideCircuitConfig( uiFactories: Set, presenterFactories: Set, - ): CircuitConfig { - return CircuitConfig.Builder() - .addUiFactories(uiFactories) - .addPresenterFactories(presenterFactories) - .build() - } + ): CircuitConfig = CircuitConfig.Builder() + .addUiFactories(uiFactories) + .addPresenterFactories(presenterFactories) + .build() } diff --git a/ui/account/build.gradle.kts b/ui/account/build.gradle.kts index 32e279fd96..daf3293fad 100644 --- a/ui/account/build.gradle.kts +++ b/ui/account/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("app.tivi.android.library") - id("app.tivi.kotlin.android") + id("app.tivi.kotlin.multiplatform") alias(libs.plugins.composeMultiplatform) } @@ -12,22 +12,24 @@ android { namespace = "app.tivi.account" } -dependencies { - implementation(projects.core.base) - implementation(projects.domain) - implementation(projects.common.ui.compose) - implementation(projects.data.traktauth) // This should really be used through an interactor +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.domain) + implementation(projects.common.ui.compose) + implementation(projects.data.traktauth) // This should really be used through an interactor - api(projects.common.ui.screens) - api(projects.common.ui.circuitOverlay) // Only for LocalNavigator - api(libs.circuit.foundation) + api(projects.common.ui.screens) + api(projects.common.ui.circuitOverlay) // Only for LocalNavigator + api(libs.circuit.foundation) - // For registerForActivityResult - implementation(libs.androidx.activity.compose) - - implementation(compose.foundation) - implementation(compose.material) - implementation(compose.material3) - implementation(compose.animation) - implementation(compose.uiTooling) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(compose.animation) + } + } + } } diff --git a/ui/account/src/main/kotlin/app/tivi/account/AccountComponent.kt b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountComponent.kt similarity index 100% rename from ui/account/src/main/kotlin/app/tivi/account/AccountComponent.kt rename to ui/account/src/commonMain/kotlin/app/tivi/account/AccountComponent.kt diff --git a/ui/account/src/main/kotlin/app/tivi/account/AccountPresenter.kt b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountPresenter.kt similarity index 100% rename from ui/account/src/main/kotlin/app/tivi/account/AccountPresenter.kt rename to ui/account/src/commonMain/kotlin/app/tivi/account/AccountPresenter.kt diff --git a/ui/account/src/main/kotlin/app/tivi/account/AccountUi.kt b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountUi.kt similarity index 100% rename from ui/account/src/main/kotlin/app/tivi/account/AccountUi.kt rename to ui/account/src/commonMain/kotlin/app/tivi/account/AccountUi.kt diff --git a/ui/account/src/main/kotlin/app/tivi/account/AccountUiState.kt b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountUiState.kt similarity index 100% rename from ui/account/src/main/kotlin/app/tivi/account/AccountUiState.kt rename to ui/account/src/commonMain/kotlin/app/tivi/account/AccountUiState.kt diff --git a/ui/account/src/main/AndroidManifest.xml b/ui/account/src/main/AndroidManifest.xml deleted file mode 100644 index 3e33db4e7f..0000000000 --- a/ui/account/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - From 5e2b0cf739baa7301277a477d394530145f77991 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Wed, 28 Jun 2023 22:05:00 +0100 Subject: [PATCH 09/23] Apply spotless --- .../kotlin/app/tivi/common/compose/EntryGrid.kt | 1 - .../kotlin/app/tivi/common/compose/theme/Platform.kt | 3 --- .../app/tivi/common/compose/ui/DateTimeTextFields.kt | 4 ++-- .../kotlin/app/tivi/inject/SharedApplicationComponent.kt | 9 --------- 4 files changed, 2 insertions(+), 15 deletions(-) diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/EntryGrid.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/EntryGrid.kt index 02c3c6a8b9..1bbea723b8 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/EntryGrid.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/EntryGrid.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp -import app.cash.paging.LoadState import app.cash.paging.LoadStateLoading import app.cash.paging.compose.LazyPagingItems import app.tivi.common.compose.ui.PlaceholderPosterCard diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt index 5afd83dac5..e0d106cdf2 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt @@ -4,10 +4,7 @@ package app.tivi.common.compose.theme import androidx.compose.material3.ColorScheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color @Composable internal expect fun colorScheme( diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt index 1f84ee67ba..33409419e7 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt @@ -160,13 +160,13 @@ fun TimeTextField( } } -//object TimeTextFieldDefaults { +// object TimeTextFieldDefaults { // val is24Hour: Boolean // @Composable get() { // val context = LocalContext.current // return remember { DateFormat.is24HourFormat(context) } // } -//} +// } @ExperimentalMaterial3Api @Composable diff --git a/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt b/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt index aef2ffa303..fa6e2be9c0 100644 --- a/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt +++ b/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt @@ -3,12 +3,9 @@ package app.tivi.inject -<<<<<<< HEAD import app.tivi.appinitializers.AppInitializer import app.tivi.appinitializers.TmdbInitializer -======= import app.tivi.common.imageloading.ImageLoadingComponent ->>>>>>> a8436cacd (Move :ui:account to KMP) import app.tivi.core.analytics.AnalyticsComponent import app.tivi.core.perf.PerformanceComponent import app.tivi.data.SqlDelightDatabaseComponent @@ -33,10 +30,7 @@ import app.tivi.util.LoggerComponent import app.tivi.util.PowerControllerComponent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO -<<<<<<< HEAD import me.tatarka.inject.annotations.IntoSet -======= ->>>>>>> a8436cacd (Move :ui:account to KMP) import me.tatarka.inject.annotations.Provides interface SharedApplicationComponent : @@ -67,12 +61,9 @@ interface CoreComponent : main = Dispatchers.Main, ) -<<<<<<< HEAD @Provides @IntoSet fun provideTmdbInitializer(bind: TmdbInitializer): AppInitializer = bind -======= ->>>>>>> a8436cacd (Move :ui:account to KMP) } interface DataComponent : From 81e987ac2147569cec0216f8580a12033b89f491 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Wed, 28 Jun 2023 22:34:52 +0100 Subject: [PATCH 10/23] Move Home() and TiviContent() to :ui:root --- android-app/app/build.gradle.kts | 21 +---- .../main/kotlin/app/tivi/home/MainActivity.kt | 92 +++---------------- settings.gradle.kts | 1 + shared/build.gradle.kts | 1 + ui/root/build.gradle.kts | 35 +++++++ .../commonMain}/kotlin/app/tivi/home/Home.kt | 12 +-- .../kotlin/app/tivi/home/TiviContent.kt | 92 +++++++++++++++++++ 7 files changed, 150 insertions(+), 104 deletions(-) create mode 100644 ui/root/build.gradle.kts rename {android-app/app/src/main => ui/root/src/commonMain}/kotlin/app/tivi/home/Home.kt (95%) create mode 100644 ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts index 8254756123..168cd48cd2 100644 --- a/android-app/app/build.gradle.kts +++ b/android-app/app/build.gradle.kts @@ -148,37 +148,22 @@ androidComponents { dependencies { implementation(projects.shared) - - implementation(projects.ui.account) implementation(projects.ui.settings) - implementation(libs.circuit.overlay) - - implementation(libs.androidx.lifecycle.viewmodel.ktx) - implementation(libs.androidx.activity.activity) implementation(libs.androidx.activity.compose) - implementation(libs.androidx.emoji) - - implementation(compose.foundation) - implementation(compose.material) - implementation(compose.materialIconsExtended) - implementation(compose.material3) - implementation(libs.compose.material3.windowsizeclass) - implementation(compose.animation) - implementation(compose.uiTooling) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.profileinstaller) implementation(libs.kotlin.coroutines.android) - implementation(libs.androidx.profileinstaller) + implementation(libs.google.firebase.crashlytics) implementation(libs.okhttp.loggingInterceptor) ksp(libs.kotlininject.compiler) - implementation(libs.google.firebase.crashlytics) - qaImplementation(libs.chucker.library) qaImplementation(libs.debugdrawer.debugdrawer) diff --git a/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt b/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt index 20e33e95c8..5dd32714a2 100644 --- a/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt +++ b/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt @@ -8,13 +8,12 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.viewModels -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.core.view.WindowCompat import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner @@ -27,35 +26,20 @@ import app.tivi.ContentViewSetter import app.tivi.TiviActivity import app.tivi.common.compose.LocalTiviDateFormatter import app.tivi.common.compose.LocalTiviTextCreator -import app.tivi.common.compose.LocalWindowSizeClass -import app.tivi.common.compose.shouldUseDarkColors -import app.tivi.common.compose.shouldUseDynamicColors -import app.tivi.common.compose.theme.TiviTheme import app.tivi.core.analytics.Analytics import app.tivi.data.traktauth.LoginToTraktInteractor import app.tivi.data.traktauth.TraktAuthActivityComponent import app.tivi.inject.ActivityComponent import app.tivi.inject.ActivityScope import app.tivi.inject.AndroidApplicationComponent -import app.tivi.overlays.LocalNavigator -import app.tivi.screens.DiscoverScreen -import app.tivi.screens.SettingsScreen -import app.tivi.screens.TiviScreen import app.tivi.settings.SettingsActivity import app.tivi.settings.TiviPreferences import app.tivi.util.TiviDateFormatter import app.tivi.util.TiviTextCreator import com.seiko.imageloader.ImageLoader import com.seiko.imageloader.LocalImageLoader -import com.slack.circuit.backstack.SaveableBackStack -import com.slack.circuit.backstack.rememberSaveableBackStack import com.slack.circuit.foundation.CircuitCompositionLocals import com.slack.circuit.foundation.CircuitConfig -import com.slack.circuit.foundation.push -import com.slack.circuit.foundation.rememberCircuitNavigator -import com.slack.circuit.foundation.screen -import com.slack.circuit.runtime.Navigator -import com.slack.circuit.runtime.Screen import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides @@ -69,6 +53,7 @@ class MainActivity : TiviActivity() { } } + @OptIn(ExperimentalComposeUiApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) component = MainActivityComponent::class.create(this) @@ -89,9 +74,19 @@ class MainActivity : TiviActivity() { TiviContent( analytics = component.analytics, preferences = component.preferences, + onRootPop = { + if (onBackPressedDispatcher.hasEnabledCallbacks()) { + onBackPressedDispatcher.onBackPressed() + } + }, onOpenSettings = { context.startActivity(Intent(context, SettingsActivity::class.java)) }, + modifier = Modifier.semantics { + // Enables testTag -> UiAutomator resource id + // See https://developer.android.com/jetpack/compose/testing#uiautomator-interop + testTagsAsResourceId = true + }, ) } } @@ -108,63 +103,6 @@ class MainActivity : TiviActivity() { } } -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) -@Composable -private fun TiviContent( - onOpenSettings: () -> Unit, - analytics: Analytics, - preferences: TiviPreferences, -) { - val backstack: SaveableBackStack = rememberSaveableBackStack { push(DiscoverScreen) } - val circuitNavigator = rememberCircuitNavigator(backstack) - - val navigator: Navigator = remember(circuitNavigator) { - TiviNavigator(circuitNavigator, onOpenSettings) - } - - // Launch an effect to track changes to the current back stack entry, and push them - // as a screen views to analytics - LaunchedEffect(backstack.topRecord) { - val topScreen = backstack.topRecord?.screen as? TiviScreen - analytics.trackScreenView( - name = topScreen?.name ?: "unknown screen", - arguments = topScreen?.arguments, - ) - } - - CompositionLocalProvider( - LocalNavigator provides navigator, - LocalWindowSizeClass provides calculateWindowSizeClass(), - ) { - TiviTheme( - useDarkColors = preferences.shouldUseDarkColors(), - useDynamicColors = preferences.shouldUseDynamicColors(), - ) { - Home(backstack = backstack, navigator = navigator) - } - } -} - -private class TiviNavigator( - private val navigator: Navigator, - private val onOpenSettings: () -> Unit, -) : Navigator { - override fun goTo(screen: Screen) { - when (screen) { - is SettingsScreen -> onOpenSettings() - else -> navigator.goTo(screen) - } - } - - override fun pop(): Screen? { - return navigator.pop() - } - - override fun resetRoot(newRoot: Screen): List { - return navigator.resetRoot(newRoot) - } -} - @ActivityScope @Component abstract class MainActivityComponent( diff --git a/settings.gradle.kts b/settings.gradle.kts index f12ca9c51e..04beaeb66a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -95,6 +95,7 @@ include( ":ui:library", ":ui:account", ":ui:upnext", + ":ui:root", ":android-app:app", ":android-app:benchmark", ":android-app:common-test", diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 98ac90f167..c03bba961f 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -48,6 +48,7 @@ kotlin { api(projects.ui.search) api(projects.ui.show.details) api(projects.ui.show.seasons) + api(projects.ui.root) // api(projects.ui.settings) api(projects.ui.upnext) } diff --git a/ui/root/build.gradle.kts b/ui/root/build.gradle.kts new file mode 100644 index 0000000000..07d2178b72 --- /dev/null +++ b/ui/root/build.gradle.kts @@ -0,0 +1,35 @@ +// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + + +plugins { + id("app.tivi.android.library") + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) +} + +android { + namespace = "app.tivi.home" +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.core.base) + implementation(projects.core.analytics) + implementation(projects.common.ui.compose) + + implementation(projects.common.ui.screens) + implementation(libs.circuit.foundation) + implementation(libs.circuit.overlay) + implementation(projects.common.ui.circuitOverlay) + + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.materialIconsExtended) + implementation(compose.animation) + } + } + } +} diff --git a/android-app/app/src/main/kotlin/app/tivi/home/Home.kt b/ui/root/src/commonMain/kotlin/app/tivi/home/Home.kt similarity index 95% rename from android-app/app/src/main/kotlin/app/tivi/home/Home.kt rename to ui/root/src/commonMain/kotlin/app/tivi/home/Home.kt index 3664a024d4..87d798325a 100644 --- a/android-app/app/src/main/kotlin/app/tivi/home/Home.kt +++ b/ui/root/src/commonMain/kotlin/app/tivi/home/Home.kt @@ -42,11 +42,8 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.unit.dp import app.tivi.common.compose.LocalWindowSizeClass import app.tivi.common.ui.resources.MR @@ -66,11 +63,12 @@ import com.slack.circuit.runtime.Screen import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.compose.stringResource -@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun Home( backstack: SaveableBackStack, navigator: Navigator, + modifier: Modifier = Modifier, ) { val windowSizeClass = LocalWindowSizeClass.current val navigationType = remember(windowSizeClass) { @@ -99,11 +97,7 @@ internal fun Home( }, contentWindowInsets = ScaffoldDefaults.contentWindowInsets .exclude(WindowInsets.statusBars), // We let content handle the status bar - modifier = Modifier.semantics { - // Enables testTag -> UiAutomator resource id - // See https://developer.android.com/jetpack/compose/testing#uiautomator-interop - testTagsAsResourceId = true - }, + modifier = modifier, ) { paddingValues -> Row( modifier = Modifier diff --git a/ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt b/ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt new file mode 100644 index 0000000000..0c16b5c734 --- /dev/null +++ b/ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt @@ -0,0 +1,92 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.home + +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import app.tivi.common.compose.LocalWindowSizeClass +import app.tivi.common.compose.shouldUseDarkColors +import app.tivi.common.compose.shouldUseDynamicColors +import app.tivi.common.compose.theme.TiviTheme +import app.tivi.core.analytics.Analytics +import app.tivi.overlays.LocalNavigator +import app.tivi.screens.DiscoverScreen +import app.tivi.screens.SettingsScreen +import app.tivi.screens.TiviScreen +import app.tivi.settings.TiviPreferences +import com.slack.circuit.backstack.SaveableBackStack +import com.slack.circuit.backstack.rememberSaveableBackStack +import com.slack.circuit.foundation.push +import com.slack.circuit.foundation.rememberCircuitNavigator +import com.slack.circuit.foundation.screen +import com.slack.circuit.runtime.Navigator +import com.slack.circuit.runtime.Screen + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +fun TiviContent( + onRootPop: () -> Unit, + onOpenSettings: () -> Unit, + analytics: Analytics, + preferences: TiviPreferences, + modifier: Modifier = Modifier, +) { + val backstack: SaveableBackStack = rememberSaveableBackStack { push(DiscoverScreen) } + val circuitNavigator = rememberCircuitNavigator(backstack, onRootPop) + + val navigator: Navigator = remember(circuitNavigator) { + TiviNavigator(circuitNavigator, onOpenSettings) + } + + // Launch an effect to track changes to the current back stack entry, and push them + // as a screen views to analytics + LaunchedEffect(backstack.topRecord) { + val topScreen = backstack.topRecord?.screen as? TiviScreen + analytics.trackScreenView( + name = topScreen?.name ?: "unknown screen", + arguments = topScreen?.arguments, + ) + } + + CompositionLocalProvider( + LocalNavigator provides navigator, + LocalWindowSizeClass provides calculateWindowSizeClass(), + ) { + TiviTheme( + useDarkColors = preferences.shouldUseDarkColors(), + useDynamicColors = preferences.shouldUseDynamicColors(), + ) { + Home( + backstack = backstack, + navigator = navigator, + modifier = modifier, + ) + } + } +} + +private class TiviNavigator( + private val navigator: Navigator, + private val onOpenSettings: () -> Unit, +) : Navigator { + override fun goTo(screen: Screen) { + when (screen) { + is SettingsScreen -> onOpenSettings() + else -> navigator.goTo(screen) + } + } + + override fun pop(): Screen? { + return navigator.pop() + } + + override fun resetRoot(newRoot: Screen): List { + return navigator.resetRoot(newRoot) + } +} From 4dfbd285d6f4df73a6f133326d9fb6e5df6b79f9 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Jun 2023 14:22:44 +0100 Subject: [PATCH 11/23] Use Circuit fork for now We need https://github.com/slackhq/circuit/pull/704 for iOS and Native support. --- gradle/libs.versions.toml | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 737f302129..1f42b0e609 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,9 +25,10 @@ android-lint = { id = "com.android.lint", version.ref = "agp" } android-test = { id = "com.android.test", version.ref = "agp" } buildConfig = "com.github.gmazzo.buildconfig:4.1.1" cacheFixPlugin = { id = "org.gradle.android.cache-fix", version = "2.7.2" } +composeMultiplatform = { id = "org.jetbrains.compose", version = "1.4.1" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } -ksp = "com.google.devtools.ksp:1.8.21-1.0.11" +ksp = "com.google.devtools.ksp:1.8.20-1.0.11" moko-resources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko-resources" } gms-googleServices = "com.google.gms.google-services:4.3.15" firebase-crashlytics = "com.google.firebase.crashlytics:2.9.6" @@ -70,38 +71,24 @@ circuit-foundation = { module = "com.slack.circuit:circuit-foundation", version. circuit-overlay = { module = "com.slack.circuit:circuit-overlay", version.ref = "circuit" } circuit-runtime = { module = "com.slack.circuit:circuit-runtime", version.ref = "circuit" } -coil-coil = { module = "io.coil-kt:coil", version.ref = "coil" } -coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } -coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } +compose-material3-windowsizeclass = "dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.2.0" -tools-desugarjdklibs = "com.android.tools:desugar_jdk_libs:2.0.3" +materialdialogs-core = "ca.gosyer:compose-material-dialogs-core:0.9.3" -compose-bom = { module = "dev.chrisbanes.compose:compose-bom", version.ref = "compose-bom" } -compose-animation-animation = { module = "androidx.compose.animation:animation" } -compose-foundation-foundation = { module = "androidx.compose.foundation:foundation" } -compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } -compose-material-iconsext = { module = "androidx.compose.material:material-icons-extended" } -compose-material-material = { module = "androidx.compose.material:material" } -compose-material3-material3 = { module = "androidx.compose.material3:material3" } -compose-material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class" } -compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4" } -compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } -compose-ui-ui = { module = "androidx.compose.ui:ui" } -compose-ui-uitextfonts = { module = "androidx.compose.ui:ui-text-google-fonts" } -compose-ui-util = { module = "androidx.compose.ui:ui-util" } -compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" } - -# This isn't strictly used, but allows Renovate to see us using the Compose Compiler artifact -compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "composecompiler" } +tools-desugarjdklibs = "com.android.tools:desugar_jdk_libs:2.0.3" google-firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.3.0" google-firebase-crashlytics = "com.google.firebase:firebase-crashlytics-ktx:18.3.7" google-firebase-perf = "com.google.firebase:firebase-perf-ktx:20.3.3" +insetsx = "com.moriatsushi.insetsx:insetsx:0.1.0-alpha10" + debugdrawer-debugdrawer = { module = "au.com.gridstone.debugdrawer:debugdrawer", version.ref = "debugdrawer" } debugdrawer-timber = { module = "au.com.gridstone.debugdrawer:debugdrawer-timber", version.ref = "debugdrawer" } debugdrawer-okhttplogger = { module = "au.com.gridstone.debugdrawer:debugdrawer-okhttp-logger", version.ref = "debugdrawer" } +imageloader = "io.github.qdsfdhvh:image-loader:1.5.1" + junit = "junit:junit:4.13.2" kermit-kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } @@ -124,6 +111,7 @@ ktlint = { module = "com.pinterest:ktlint", version.ref = "ktlint" } leakCanary = "com.squareup.leakcanary:leakcanary-android:2.11" moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko-resources" } +moko-resourcesCompose = { module = "dev.icerock.moko:resources-compose", version.ref = "moko-resources" } okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } okhttp-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } From 020b3d9d4a576163e33e9d0c1c79d42528b92477 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Jun 2023 15:14:02 +0100 Subject: [PATCH 12/23] Create DI components for iOS Had to stub out some auth dependencies so that they were on the graph. --- .../kotlin/app/tivi/util/TiviDateFormatter.kt | 4 ++ .../kotlin/app/tivi/util/TiviTextCreator.kt | 4 ++ .../app/tivi/util/PowerControllerComponent.kt | 2 +- .../app/tivi/util/PowerControllerComponent.kt | 2 +- .../tivi/data/traktauth/TraktAuthComponent.kt | 2 +- .../traktauth/RefreshTraktTokensInteractor.kt | 2 +- .../app/tivi/data/traktauth/IosAuthStore.kt | 23 ++++++++++ .../traktauth/IosLoginToTraktInteractor.kt | 17 ++++++++ .../IosRefreshTraktTokensInteractor.kt | 14 ++++++ .../tivi/data/traktauth/TraktAuthComponent.kt | 18 +++++++- .../tivi/data/traktauth/DesktopAuthStore.kt | 23 ++++++++++ .../DesktopLoginToTraktInteractor.kt | 17 ++++++++ .../DesktopRefreshTraktTokensInteractor.kt | 14 ++++++ .../tivi/data/traktauth/TraktAuthComponent.kt | 18 +++++++- shared/build.gradle.kts | 8 ++++ .../tivi/inject/HomeUiControllerComponent.kt | 25 +++++++++++ .../tivi/inject/IosApplicationComponent.kt | 31 +++++++++++++ .../src/iosMain/kotlin/app/tivi/home/Home.kt | 43 +++++++++++++++++++ 18 files changed, 261 insertions(+), 6 deletions(-) create mode 100644 data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosAuthStore.kt create mode 100644 data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosLoginToTraktInteractor.kt create mode 100644 data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosRefreshTraktTokensInteractor.kt create mode 100644 data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopAuthStore.kt create mode 100644 data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopLoginToTraktInteractor.kt create mode 100644 data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopRefreshTraktTokensInteractor.kt create mode 100644 shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt create mode 100644 shared/src/iosMain/kotlin/app/tivi/inject/IosApplicationComponent.kt create mode 100644 ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt diff --git a/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviDateFormatter.kt b/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviDateFormatter.kt index d58997f096..c814468f61 100644 --- a/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviDateFormatter.kt +++ b/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviDateFormatter.kt @@ -3,6 +3,7 @@ package app.tivi.util +import app.tivi.inject.ActivityScope import kotlin.time.Duration.Companion.days import kotlinx.cinterop.convert import kotlinx.datetime.Instant @@ -10,6 +11,7 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime import kotlinx.datetime.toNSDate import kotlinx.datetime.toNSDateComponents +import me.tatarka.inject.annotations.Inject import platform.Foundation.NSCalendar import platform.Foundation.NSCalendar.Companion.currentCalendar import platform.Foundation.NSDate @@ -23,6 +25,8 @@ import platform.Foundation.NSRelativeDateTimeFormatter import platform.Foundation.NSRelativeDateTimeFormatterStyleNamed import platform.Foundation.autoupdatingCurrentLocale +@ActivityScope +@Inject actual class TiviDateFormatter( locale: NSLocale = NSLocale.autoupdatingCurrentLocale, ) { diff --git a/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviTextCreator.kt b/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviTextCreator.kt index 151fe353d6..c924d28594 100644 --- a/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviTextCreator.kt +++ b/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviTextCreator.kt @@ -5,6 +5,7 @@ package app.tivi.util import app.tivi.common.ui.resources.MR import app.tivi.data.models.TiviShow +import app.tivi.inject.ActivityScope import dev.icerock.moko.resources.desc.StringDesc import dev.icerock.moko.resources.format import kotlinx.cinterop.convert @@ -14,6 +15,7 @@ import kotlinx.datetime.isoDayNumber import kotlinx.datetime.toKotlinInstant import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toNSTimeZone +import me.tatarka.inject.annotations.Inject import platform.Foundation.NSCalendar import platform.Foundation.NSCalendarUnitHour import platform.Foundation.NSCalendarUnitMinute @@ -22,6 +24,8 @@ import platform.Foundation.NSCalendarUnitWeekday import platform.Foundation.NSDate import platform.Foundation.NSDateComponentsFormatter +@ActivityScope +@Inject actual class TiviTextCreator( override val dateFormatter: TiviDateFormatter, ) : CommonTiviTextCreator { diff --git a/core/powercontroller/src/iosMain/kotlin/app/tivi/util/PowerControllerComponent.kt b/core/powercontroller/src/iosMain/kotlin/app/tivi/util/PowerControllerComponent.kt index c2ecbf5048..6aaf8b029e 100644 --- a/core/powercontroller/src/iosMain/kotlin/app/tivi/util/PowerControllerComponent.kt +++ b/core/powercontroller/src/iosMain/kotlin/app/tivi/util/PowerControllerComponent.kt @@ -7,5 +7,5 @@ import me.tatarka.inject.annotations.Provides actual interface PowerControllerComponent { @Provides - fun providePowerController() = EmptyPowerController + fun providePowerController(): PowerController = EmptyPowerController } diff --git a/core/powercontroller/src/jvmMain/kotlin/app/tivi/util/PowerControllerComponent.kt b/core/powercontroller/src/jvmMain/kotlin/app/tivi/util/PowerControllerComponent.kt index c2ecbf5048..6aaf8b029e 100644 --- a/core/powercontroller/src/jvmMain/kotlin/app/tivi/util/PowerControllerComponent.kt +++ b/core/powercontroller/src/jvmMain/kotlin/app/tivi/util/PowerControllerComponent.kt @@ -7,5 +7,5 @@ import me.tatarka.inject.annotations.Provides actual interface PowerControllerComponent { @Provides - fun providePowerController() = EmptyPowerController + fun providePowerController(): PowerController = EmptyPowerController } diff --git a/data/traktauth/src/androidMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt b/data/traktauth/src/androidMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt index 081c9eac6b..7329aa9345 100644 --- a/data/traktauth/src/androidMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt +++ b/data/traktauth/src/androidMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt @@ -53,7 +53,7 @@ actual interface TraktAuthComponent { @ApplicationScope @Provides - fun provideAuthStore(manager: TiviAuthStore): AuthStore = manager + fun provideAuthStore(store: TiviAuthStore): AuthStore = store } interface TraktAuthActivityComponent { diff --git a/data/traktauth/src/commonMain/kotlin/app/tivi/data/traktauth/RefreshTraktTokensInteractor.kt b/data/traktauth/src/commonMain/kotlin/app/tivi/data/traktauth/RefreshTraktTokensInteractor.kt index b836b231fb..a7511b6bb1 100644 --- a/data/traktauth/src/commonMain/kotlin/app/tivi/data/traktauth/RefreshTraktTokensInteractor.kt +++ b/data/traktauth/src/commonMain/kotlin/app/tivi/data/traktauth/RefreshTraktTokensInteractor.kt @@ -3,6 +3,6 @@ package app.tivi.data.traktauth -fun interface RefreshTraktTokensInteractor { +interface RefreshTraktTokensInteractor { suspend operator fun invoke(): AuthState? } diff --git a/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosAuthStore.kt b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosAuthStore.kt new file mode 100644 index 0000000000..2dc2fbba75 --- /dev/null +++ b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosAuthStore.kt @@ -0,0 +1,23 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.data.traktauth + +import app.tivi.data.traktauth.store.AuthStore +import me.tatarka.inject.annotations.Inject + +@Inject +class IosAuthStore : AuthStore { + override suspend fun get(): AuthState? { + // TODO no-op for now + return null + } + + override suspend fun save(state: AuthState) { + // TODO no-op for now + } + + override suspend fun clear() { + // TODO no-op for now + } +} diff --git a/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosLoginToTraktInteractor.kt b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosLoginToTraktInteractor.kt new file mode 100644 index 0000000000..4b7056542e --- /dev/null +++ b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosLoginToTraktInteractor.kt @@ -0,0 +1,17 @@ +// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.data.traktauth + +import me.tatarka.inject.annotations.Inject + +@Inject +class IosLoginToTraktInteractor : LoginToTraktInteractor { + override fun register() { + // TODO + } + + override fun launch() { + // TODO + } +} diff --git a/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosRefreshTraktTokensInteractor.kt b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosRefreshTraktTokensInteractor.kt new file mode 100644 index 0000000000..c3542755ae --- /dev/null +++ b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosRefreshTraktTokensInteractor.kt @@ -0,0 +1,14 @@ +// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.data.traktauth + +import me.tatarka.inject.annotations.Inject + +@Inject +class IosRefreshTraktTokensInteractor : RefreshTraktTokensInteractor { + override suspend fun invoke(): AuthState? { + // TODO + return null + } +} diff --git a/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt index e7b355aabe..99b1fc874e 100644 --- a/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt +++ b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt @@ -3,4 +3,20 @@ package app.tivi.data.traktauth -actual interface TraktAuthComponent +import app.tivi.data.traktauth.store.AuthStore +import app.tivi.inject.ApplicationScope +import me.tatarka.inject.annotations.Provides + +actual interface TraktAuthComponent { + @ApplicationScope + @Provides + fun provideAuthStore(store: IosAuthStore): AuthStore = store + + @ApplicationScope + @Provides + fun provideRefreshTraktTokensInteractor(impl: IosRefreshTraktTokensInteractor): RefreshTraktTokensInteractor = impl + + @Provides + @ApplicationScope + fun provideLoginToTraktInteractor(impl: IosLoginToTraktInteractor): LoginToTraktInteractor = impl +} diff --git a/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopAuthStore.kt b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopAuthStore.kt new file mode 100644 index 0000000000..04ce99cb05 --- /dev/null +++ b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopAuthStore.kt @@ -0,0 +1,23 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.data.traktauth + +import app.tivi.data.traktauth.store.AuthStore +import me.tatarka.inject.annotations.Inject + +@Inject +class DesktopAuthStore : AuthStore { + override suspend fun get(): AuthState? { + // TODO no-op for now + return null + } + + override suspend fun save(state: AuthState) { + // TODO no-op for now + } + + override suspend fun clear() { + // TODO no-op for now + } +} diff --git a/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopLoginToTraktInteractor.kt b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopLoginToTraktInteractor.kt new file mode 100644 index 0000000000..8cc3421e63 --- /dev/null +++ b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopLoginToTraktInteractor.kt @@ -0,0 +1,17 @@ +// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.data.traktauth + +import me.tatarka.inject.annotations.Inject + +@Inject +class DesktopLoginToTraktInteractor : LoginToTraktInteractor { + override fun register() { + // TODO + } + + override fun launch() { + // TODO + } +} diff --git a/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopRefreshTraktTokensInteractor.kt b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopRefreshTraktTokensInteractor.kt new file mode 100644 index 0000000000..3d6776a120 --- /dev/null +++ b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopRefreshTraktTokensInteractor.kt @@ -0,0 +1,14 @@ +// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.data.traktauth + +import me.tatarka.inject.annotations.Inject + +@Inject +class DesktopRefreshTraktTokensInteractor : RefreshTraktTokensInteractor { + override suspend fun invoke(): AuthState? { + // TODO + return null + } +} diff --git a/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt index e7b355aabe..f2928e69b0 100644 --- a/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt +++ b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt @@ -3,4 +3,20 @@ package app.tivi.data.traktauth -actual interface TraktAuthComponent +import app.tivi.data.traktauth.store.AuthStore +import app.tivi.inject.ApplicationScope +import me.tatarka.inject.annotations.Provides + +actual interface TraktAuthComponent { + @ApplicationScope + @Provides + fun provideAuthStore(store: DesktopAuthStore): AuthStore = store + + @ApplicationScope + @Provides + fun provideRefreshTraktTokensInteractor(impl: DesktopRefreshTraktTokensInteractor): RefreshTraktTokensInteractor = impl + + @Provides + @ApplicationScope + fun provideLoginToTraktInteractor(impl: DesktopLoginToTraktInteractor): LoginToTraktInteractor = impl +} diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index c03bba961f..6ea35b2c2c 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 +import app.tivi.gradle.addKspDependencyForAllTargets import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget @@ -9,6 +10,7 @@ plugins { id("app.tivi.android.library") id("app.tivi.kotlin.multiplatform") alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.ksp) } kotlin { @@ -59,3 +61,9 @@ kotlin { android { namespace = "app.tivi.shared" } + +ksp { + arg("me.tatarka.inject.generateCompanionExtensions", "true") +} + +addKspDependencyForAllTargets(libs.kotlininject.compiler) diff --git a/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt b/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt new file mode 100644 index 0000000000..ed12305e0c --- /dev/null +++ b/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt @@ -0,0 +1,25 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.inject + +import app.tivi.core.analytics.Analytics +import app.tivi.settings.TiviPreferences +import app.tivi.util.TiviDateFormatter +import app.tivi.util.TiviTextCreator +import com.slack.circuit.foundation.CircuitConfig +import me.tatarka.inject.annotations.Component + +@ActivityScope +@Component +abstract class HomeUiControllerComponent( + @Component val applicationComponent: IosApplicationComponent, +) { + abstract val tiviDateFormatter: TiviDateFormatter + abstract val textCreator: TiviTextCreator + abstract val preferences: TiviPreferences + abstract val analytics: Analytics + abstract val circuitConfig: CircuitConfig + + companion object +} diff --git a/shared/src/iosMain/kotlin/app/tivi/inject/IosApplicationComponent.kt b/shared/src/iosMain/kotlin/app/tivi/inject/IosApplicationComponent.kt new file mode 100644 index 0000000000..97318e03c9 --- /dev/null +++ b/shared/src/iosMain/kotlin/app/tivi/inject/IosApplicationComponent.kt @@ -0,0 +1,31 @@ +// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.inject + +import androidx.compose.ui.unit.Density +import app.tivi.app.ApplicationInfo +import app.tivi.app.Flavor +import app.tivi.appinitializers.AppInitializers +import me.tatarka.inject.annotations.Component +import me.tatarka.inject.annotations.Provides +import platform.Foundation.NSBundle + +@Component +@ApplicationScope +abstract class IosApplicationComponent : SharedApplicationComponent { + abstract val initializers: AppInitializers + + @ApplicationScope + @Provides + fun provideApplicationId(): ApplicationInfo = ApplicationInfo( + packageName = NSBundle.mainBundle.bundleIdentifier ?: "empty.bundle.id", + debugBuild = Platform.isDebugBinary, + flavor = Flavor.Standard, + ) + + @Provides + fun provideDensity(): Density = Density(density = 1f) // FIXME + + companion object +} diff --git a/ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt b/ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt new file mode 100644 index 0000000000..0c13c64190 --- /dev/null +++ b/ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt @@ -0,0 +1,43 @@ +// Copyright 2020, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.home + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.window.ComposeUIViewController +import app.tivi.common.compose.LocalTiviDateFormatter +import app.tivi.common.compose.LocalTiviTextCreator +import app.tivi.core.analytics.Analytics +import app.tivi.settings.TiviPreferences +import app.tivi.util.TiviDateFormatter +import app.tivi.util.TiviTextCreator +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.LocalImageLoader +import com.slack.circuit.foundation.CircuitCompositionLocals +import com.slack.circuit.foundation.CircuitConfig + +fun HomeViewController( + onRootPop: () -> Unit, + onOpenSettings: () -> Unit, + imageLoader: ImageLoader, + tiviDateFormatter: TiviDateFormatter, + tiviTextCreator: TiviTextCreator, + circuitConfig: CircuitConfig, + analytics: Analytics, + preferences: TiviPreferences, +) = ComposeUIViewController { + CompositionLocalProvider( + LocalImageLoader provides imageLoader, + LocalTiviDateFormatter provides tiviDateFormatter, + LocalTiviTextCreator provides tiviTextCreator, + ) { + CircuitCompositionLocals(circuitConfig) { + TiviContent( + onRootPop = onRootPop, + onOpenSettings = onOpenSettings, + analytics = analytics, + preferences = preferences, + ) + } + } +} From 68915fcf91ee35e21bea9f39227d29448ae6ccf4 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Wed, 28 Jun 2023 21:31:46 +0100 Subject: [PATCH 13/23] Add basic Xcode project --- .gitignore | 96 +++++ .../app/tivi/settings/TiviPreferences.kt | 4 +- ios-app/Tivi/Tivi.xcodeproj/project.pbxproj | 397 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../Tivi/Tivi/Assets.xcassets/Contents.json | 6 + ios-app/Tivi/Tivi/ContentView.swift | 49 +++ .../Preview Assets.xcassets/Contents.json | 6 + ios-app/Tivi/Tivi/TiviApp.swift | 20 + settings.gradle.kts | 3 +- shared/build.gradle.kts | 30 +- .../tivi/inject/HomeUiControllerComponent.kt | 2 + .../src/iosMain/kotlin/app/tivi/home/Home.kt | 6 +- 15 files changed, 650 insertions(+), 8 deletions(-) create mode 100644 ios-app/Tivi/Tivi.xcodeproj/project.pbxproj create mode 100644 ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios-app/Tivi/Tivi/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ios-app/Tivi/Tivi/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios-app/Tivi/Tivi/Assets.xcassets/Contents.json create mode 100644 ios-app/Tivi/Tivi/ContentView.swift create mode 100644 ios-app/Tivi/Tivi/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 ios-app/Tivi/Tivi/TiviApp.swift diff --git a/.gitignore b/.gitignore index 81ec623178..fb35aea523 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,99 @@ org.eclipse.buildship.core.prefs .classpath .project bin/ + + +########################################################################################## +# Imported from https://github.com/github/gitignore/blob/main/Swift.gitignore +########################################################################################## + +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ diff --git a/core/preferences/src/commonMain/kotlin/app/tivi/settings/TiviPreferences.kt b/core/preferences/src/commonMain/kotlin/app/tivi/settings/TiviPreferences.kt index 8e9a79e3e4..5ea77e57f5 100644 --- a/core/preferences/src/commonMain/kotlin/app/tivi/settings/TiviPreferences.kt +++ b/core/preferences/src/commonMain/kotlin/app/tivi/settings/TiviPreferences.kt @@ -40,9 +40,7 @@ object EmptyTiviPreferences : TiviPreferences { override var theme: TiviPreferences.Theme = TiviPreferences.Theme.SYSTEM - override fun observeTheme(): Flow { - TODO("Not yet implemented") - } + override fun observeTheme(): Flow = emptyFlow() override var useDynamicColors: Boolean = false diff --git a/ios-app/Tivi/Tivi.xcodeproj/project.pbxproj b/ios-app/Tivi/Tivi.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..f3b5c8b15f --- /dev/null +++ b/ios-app/Tivi/Tivi.xcodeproj/project.pbxproj @@ -0,0 +1,397 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 833349382A4CCCEE00F464FE /* TiviApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833349372A4CCCEE00F464FE /* TiviApp.swift */; }; + 8333493A2A4CCCEE00F464FE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833349392A4CCCEE00F464FE /* ContentView.swift */; }; + 8333493C2A4CCCEF00F464FE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8333493B2A4CCCEF00F464FE /* Assets.xcassets */; }; + 8333493F2A4CCCEF00F464FE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8333493E2A4CCCEF00F464FE /* Preview Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 833349342A4CCCEE00F464FE /* Tivi.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tivi.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 833349372A4CCCEE00F464FE /* TiviApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TiviApp.swift; sourceTree = ""; }; + 833349392A4CCCEE00F464FE /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 8333493B2A4CCCEF00F464FE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8333493E2A4CCCEF00F464FE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 833349312A4CCCEE00F464FE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8333492B2A4CCCEE00F464FE = { + isa = PBXGroup; + children = ( + 833349362A4CCCEE00F464FE /* Tivi */, + 833349352A4CCCEE00F464FE /* Products */, + ); + sourceTree = ""; + }; + 833349352A4CCCEE00F464FE /* Products */ = { + isa = PBXGroup; + children = ( + 833349342A4CCCEE00F464FE /* Tivi.app */, + ); + name = Products; + sourceTree = ""; + }; + 833349362A4CCCEE00F464FE /* Tivi */ = { + isa = PBXGroup; + children = ( + 833349372A4CCCEE00F464FE /* TiviApp.swift */, + 833349392A4CCCEE00F464FE /* ContentView.swift */, + 8333493B2A4CCCEF00F464FE /* Assets.xcassets */, + 8333493D2A4CCCEF00F464FE /* Preview Content */, + ); + path = Tivi; + sourceTree = ""; + }; + 8333493D2A4CCCEF00F464FE /* Preview Content */ = { + isa = PBXGroup; + children = ( + 8333493E2A4CCCEF00F464FE /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 833349332A4CCCEE00F464FE /* Tivi */ = { + isa = PBXNativeTarget; + buildConfigurationList = 833349422A4CCCEF00F464FE /* Build configuration list for PBXNativeTarget "Tivi" */; + buildPhases = ( + 833349452A4CCD9F00F464FE /* ShellScript */, + 833349302A4CCCEE00F464FE /* Sources */, + 833349312A4CCCEE00F464FE /* Frameworks */, + 833349322A4CCCEE00F464FE /* Resources */, + 38282FFA2A4F242200E7929E /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Tivi; + productName = Tivi; + productReference = 833349342A4CCCEE00F464FE /* Tivi.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8333492C2A4CCCEE00F464FE /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + TargetAttributes = { + 833349332A4CCCEE00F464FE = { + CreatedOnToolsVersion = 14.3.1; + }; + }; + }; + buildConfigurationList = 8333492F2A4CCCEE00F464FE /* Build configuration list for PBXProject "Tivi" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8333492B2A4CCCEE00F464FE; + productRefGroup = 833349352A4CCCEE00F464FE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 833349332A4CCCEE00F464FE /* Tivi */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 833349322A4CCCEE00F464FE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8333493F2A4CCCEF00F464FE /* Preview Assets.xcassets in Resources */, + 8333493C2A4CCCEF00F464FE /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 38282FFA2A4F242200E7929E /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$SRCROOT/../..\"\n./gradlew :shared:copyFrameworkResourcesToApp \\\n -Pmoko.resources.PLATFORM_NAME=\"$PLATFORM_NAME\" \\\n -Pmoko.resources.CONFIGURATION=\"$CONFIGURATION\" \\\n -Pmoko.resources.ARCHS=\"$ARCHS\" \\\n -Pmoko.resources.BUILT_PRODUCTS_DIR=\"$BUILT_PRODUCTS_DIR\" \\\n -Pmoko.resources.CONTENTS_FOLDER_PATH=\"$CONTENTS_FOLDER_PATH\" \n"; + }; + 833349452A4CCD9F00F464FE /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$SRCROOT/../..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 833349302A4CCCEE00F464FE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8333493A2A4CCCEE00F464FE /* ContentView.swift in Sources */, + 833349382A4CCCEE00F464FE /* TiviApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 833349402A4CCCEF00F464FE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 833349412A4CCCEF00F464FE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 833349432A4CCCEF00F464FE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Tivi/Preview Content\""; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + TiviKt, + "-lsqlite3", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.tivi; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 833349442A4CCCEF00F464FE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Tivi/Preview Content\""; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + TiviKt, + "-lsqlite3", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.tivi; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8333492F2A4CCCEE00F464FE /* Build configuration list for PBXProject "Tivi" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 833349402A4CCCEF00F464FE /* Debug */, + 833349412A4CCCEF00F464FE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 833349422A4CCCEF00F464FE /* Build configuration list for PBXNativeTarget "Tivi" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 833349432A4CCCEF00F464FE /* Debug */, + 833349442A4CCCEF00F464FE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8333492C2A4CCCEE00F464FE /* Project object */; +} diff --git a/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios-app/Tivi/Tivi/Assets.xcassets/AccentColor.colorset/Contents.json b/ios-app/Tivi/Tivi/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/ios-app/Tivi/Tivi/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-app/Tivi/Tivi/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios-app/Tivi/Tivi/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..13613e3ee1 --- /dev/null +++ b/ios-app/Tivi/Tivi/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-app/Tivi/Tivi/Assets.xcassets/Contents.json b/ios-app/Tivi/Tivi/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/ios-app/Tivi/Tivi/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-app/Tivi/Tivi/ContentView.swift b/ios-app/Tivi/Tivi/ContentView.swift new file mode 100644 index 0000000000..b037085d72 --- /dev/null +++ b/ios-app/Tivi/Tivi/ContentView.swift @@ -0,0 +1,49 @@ +// +// ContentView.swift +// Tivi +// +// Created by Chris Banes on 28/06/2023. +// + +import SwiftUI +import TiviKt + +struct ContentView: View { + let component: HomeUiControllerComponent + + init(applicationComponent: IosApplicationComponent) { + self.component = HomeUiControllerComponent.companion.create(applicationComponent: applicationComponent) + } + + var body: some View { + ComposeView(component: self.component) + .ignoresSafeArea(.all, edges: .bottom) // Compose has own keyboard handler + } +} + +struct ComposeView: UIViewControllerRepresentable { + let component: HomeUiControllerComponent + + init(component: HomeUiControllerComponent) { + self.component = component + } + + func makeUIViewController(context: Context) -> UIViewController { + HomeKt.HomeViewController( + onRootPop: { + // todo + }, + onOpenSettings: { + // todo + }, + imageLoader: component.imageLoader, + tiviDateFormatter: component.tiviDateFormatter, + tiviTextCreator: component.textCreator, + circuitConfig: component.circuitConfig, + analytics: component.analytics, + preferences: component.preferences + ) + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} diff --git a/ios-app/Tivi/Tivi/Preview Content/Preview Assets.xcassets/Contents.json b/ios-app/Tivi/Tivi/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/ios-app/Tivi/Tivi/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-app/Tivi/Tivi/TiviApp.swift b/ios-app/Tivi/Tivi/TiviApp.swift new file mode 100644 index 0000000000..7b38ff3f00 --- /dev/null +++ b/ios-app/Tivi/Tivi/TiviApp.swift @@ -0,0 +1,20 @@ +// +// TiviApp.swift +// Tivi +// +// Created by Chris Banes on 28/06/2023. +// + +import SwiftUI +import TiviKt + +@main +struct TiviApp: App { + let applicationComponent = IosApplicationComponent.companion.create() + + var body: some Scene { + WindowGroup { + ContentView(applicationComponent: applicationComponent) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 04beaeb66a..bf3d878900 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,13 +38,12 @@ gradleEnterprise { buildScan { termsOfServiceUrl = "https://gradle.com/terms-of-service" termsOfServiceAgree = "yes" - publishAlways() } } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") // https://docs.gradle.org/7.6/userguide/configuration_cache.html#config_cache:stable -enableFeaturePreview("STABLE_CONFIGURATION_CACHE") +// enableFeaturePreview("STABLE_CONFIGURATION_CACHE") rootProject.name = "tivi" diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 6ea35b2c2c..6ade85afa2 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,7 +1,6 @@ // Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors // SPDX-License-Identifier: Apache-2.0 - import app.tivi.gradle.addKspDependencyForAllTargets import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget @@ -11,13 +10,16 @@ plugins { id("app.tivi.kotlin.multiplatform") alias(libs.plugins.composeMultiplatform) alias(libs.plugins.ksp) + alias(libs.plugins.moko.resources) } kotlin { targets.withType { binaries.withType { isStatic = true - baseName = "Tivi" + baseName = "TiviKt" + + export(projects.ui.root) } } @@ -67,3 +69,27 @@ ksp { } addKspDependencyForAllTargets(libs.kotlininject.compiler) + +multiplatformResources { + disableStaticFrameworkWarning = true + multiplatformResourcesPackage = "app.tivi" + multiplatformResourcesSourceSet = "iosMain" +} + +// Various fixes for moko-resources tasks +// iOS +afterEvaluate { + tasks.findByPath("kspKotlinIosArm64")?.apply { + dependsOn(tasks.getByPath("generateMRiosArm64Main")) + } + tasks.findByPath("kspKotlinIosSimulatorArm64")?.apply { + dependsOn(tasks.getByPath("generateMRiosSimulatorArm64Main")) + } +} +// Android +tasks.withType(com.android.build.gradle.tasks.MergeResources::class).configureEach { + dependsOn(tasks.getByPath("generateMRandroidMain")) +} +tasks.withType(com.android.build.gradle.tasks.MapSourceSetPathsTask::class).configureEach { + dependsOn(tasks.getByPath("generateMRandroidMain")) +} diff --git a/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt b/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt index ed12305e0c..028b60f87b 100644 --- a/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt +++ b/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt @@ -7,6 +7,7 @@ import app.tivi.core.analytics.Analytics import app.tivi.settings.TiviPreferences import app.tivi.util.TiviDateFormatter import app.tivi.util.TiviTextCreator +import com.seiko.imageloader.ImageLoader import com.slack.circuit.foundation.CircuitConfig import me.tatarka.inject.annotations.Component @@ -20,6 +21,7 @@ abstract class HomeUiControllerComponent( abstract val preferences: TiviPreferences abstract val analytics: Analytics abstract val circuitConfig: CircuitConfig + abstract val imageLoader: ImageLoader companion object } diff --git a/ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt b/ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt index 0c13c64190..e282123250 100644 --- a/ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt +++ b/ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt @@ -15,7 +15,11 @@ import com.seiko.imageloader.ImageLoader import com.seiko.imageloader.LocalImageLoader import com.slack.circuit.foundation.CircuitCompositionLocals import com.slack.circuit.foundation.CircuitConfig +import kotlin.experimental.ExperimentalObjCName +import platform.UIKit.UIViewController +@OptIn(ExperimentalObjCName::class) +@ObjCName("HomeViewController") fun HomeViewController( onRootPop: () -> Unit, onOpenSettings: () -> Unit, @@ -25,7 +29,7 @@ fun HomeViewController( circuitConfig: CircuitConfig, analytics: Analytics, preferences: TiviPreferences, -) = ComposeUIViewController { +): UIViewController = ComposeUIViewController { CompositionLocalProvider( LocalImageLoader provides imageLoader, LocalTiviDateFormatter provides tiviDateFormatter, From 83d17b3d9f3b7ea893e2aa0dbe43147a9e71848a Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Jun 2023 16:57:33 +0100 Subject: [PATCH 14/23] Allow iPhones to draw at frame rates other than 60Hz --- ios-app/Tivi/Tivi.xcodeproj/project.pbxproj | 4 ++++ ios-app/Tivi/Tivi/Info.plist | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 ios-app/Tivi/Tivi/Info.plist diff --git a/ios-app/Tivi/Tivi.xcodeproj/project.pbxproj b/ios-app/Tivi/Tivi.xcodeproj/project.pbxproj index f3b5c8b15f..73f657cf97 100644 --- a/ios-app/Tivi/Tivi.xcodeproj/project.pbxproj +++ b/ios-app/Tivi/Tivi.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 38282FFD2A4F318E00E7929E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 833349342A4CCCEE00F464FE /* Tivi.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tivi.app; sourceTree = BUILT_PRODUCTS_DIR; }; 833349372A4CCCEE00F464FE /* TiviApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TiviApp.swift; sourceTree = ""; }; 833349392A4CCCEE00F464FE /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -51,6 +52,7 @@ 833349362A4CCCEE00F464FE /* Tivi */ = { isa = PBXGroup; children = ( + 38282FFD2A4F318E00E7929E /* Info.plist */, 833349372A4CCCEE00F464FE /* TiviApp.swift */, 833349392A4CCCEE00F464FE /* ContentView.swift */, 8333493B2A4CCCEF00F464FE /* Assets.xcassets */, @@ -309,6 +311,7 @@ ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Tivi/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -345,6 +348,7 @@ ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Tivi/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/ios-app/Tivi/Tivi/Info.plist b/ios-app/Tivi/Tivi/Info.plist new file mode 100644 index 0000000000..c2e0e36112 --- /dev/null +++ b/ios-app/Tivi/Tivi/Info.plist @@ -0,0 +1,7 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + From ff9b0c26c5c94f8f1860dfdff913a881139f116b Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Jun 2023 17:19:52 +0100 Subject: [PATCH 15/23] Fix :shared iOS x64 build --- shared/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 6ade85afa2..57885411b7 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -85,6 +85,9 @@ afterEvaluate { tasks.findByPath("kspKotlinIosSimulatorArm64")?.apply { dependsOn(tasks.getByPath("generateMRiosSimulatorArm64Main")) } + tasks.findByPath("kspKotlinIosX64")?.apply { + dependsOn(tasks.getByPath("generateMRiosX64Main")) + } } // Android tasks.withType(com.android.build.gradle.tasks.MergeResources::class).configureEach { From ee7b0e6a40561c18b6cef1513d60ba231469f4d1 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Jun 2023 17:22:20 +0100 Subject: [PATCH 16/23] Fix jvmTest --- .../test/src/commonTest/kotlin/app/tivi/data/DatabaseTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/data/test/src/commonTest/kotlin/app/tivi/data/DatabaseTest.kt b/data/test/src/commonTest/kotlin/app/tivi/data/DatabaseTest.kt index 566ff1ee6b..61b0a128e6 100644 --- a/data/test/src/commonTest/kotlin/app/tivi/data/DatabaseTest.kt +++ b/data/test/src/commonTest/kotlin/app/tivi/data/DatabaseTest.kt @@ -6,6 +6,7 @@ package app.tivi.data import app.cash.sqldelight.db.SqlDriver import app.moviebase.tmdb.Tmdb3 import app.moviebase.trakt.Trakt +import app.tivi.data.traktauth.AuthState import app.tivi.data.traktauth.RefreshTraktTokensInteractor import app.tivi.data.traktauth.TraktAuthState import app.tivi.inject.ApplicationScope @@ -39,7 +40,9 @@ abstract class TestApplicationComponent : @Provides fun provideRefreshTraktTokensInteractor(): RefreshTraktTokensInteractor { - return RefreshTraktTokensInteractor { null } + return object : RefreshTraktTokensInteractor { + override suspend fun invoke(): AuthState? = null + } } @Provides From 0eddcd13d875b9f4aceb22600893d0aa9a6de957 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Jun 2023 17:31:15 +0100 Subject: [PATCH 17/23] Update CI iOS command We only need to link :shared --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1e5bcebeb..b8ac2e146f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -136,7 +136,7 @@ jobs: ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }} - name: Build iOS libraries - run: ./gradlew spotlessCheck linkIosX64 iosX64Test + run: ./gradlew spotlessCheck :shared:linkIosX64 iosX64Test - name: Clean secrets if: always() From b80becd8b0fa80c91e1c59b8f0437d9ac89bd1ef Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Jun 2023 20:51:19 +0100 Subject: [PATCH 18/23] Try and simplify iOS integration --- .../main/kotlin/app/tivi/home/MainActivity.kt | 59 ++++++------------- ios-app/Tivi/Tivi/ContentView.swift | 31 ++++------ ios-app/Tivi/Tivi/TiviApp.swift | 3 +- .../tivi/inject/HomeUiControllerComponent.kt | 23 ++++---- .../kotlin/app/tivi/home/TiviContent.kt | 50 ++++++++++++---- .../src/iosMain/kotlin/app/tivi/home/Home.kt | 47 --------------- .../app/tivi/home/TiviUiViewController.kt | 28 +++++++++ 7 files changed, 107 insertions(+), 134 deletions(-) delete mode 100644 ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt create mode 100644 ui/root/src/iosMain/kotlin/app/tivi/home/TiviUiViewController.kt diff --git a/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt b/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt index 5dd32714a2..1ea7fd2d99 100644 --- a/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt +++ b/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt @@ -8,7 +8,6 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.viewModels -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView @@ -24,22 +23,12 @@ import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import app.tivi.ContentViewSetter import app.tivi.TiviActivity -import app.tivi.common.compose.LocalTiviDateFormatter -import app.tivi.common.compose.LocalTiviTextCreator -import app.tivi.core.analytics.Analytics import app.tivi.data.traktauth.LoginToTraktInteractor import app.tivi.data.traktauth.TraktAuthActivityComponent import app.tivi.inject.ActivityComponent import app.tivi.inject.ActivityScope import app.tivi.inject.AndroidApplicationComponent import app.tivi.settings.SettingsActivity -import app.tivi.settings.TiviPreferences -import app.tivi.util.TiviDateFormatter -import app.tivi.util.TiviTextCreator -import com.seiko.imageloader.ImageLoader -import com.seiko.imageloader.LocalImageLoader -import com.slack.circuit.foundation.CircuitCompositionLocals -import com.slack.circuit.foundation.CircuitConfig import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides @@ -65,31 +54,21 @@ class MainActivity : TiviActivity() { val composeView = ComposeView(this).apply { setContent { - CompositionLocalProvider( - LocalImageLoader provides component.imageLoader, - LocalTiviDateFormatter provides component.tiviDateFormatter, - LocalTiviTextCreator provides component.textCreator, - ) { - CircuitCompositionLocals(component.circuitConfig) { - TiviContent( - analytics = component.analytics, - preferences = component.preferences, - onRootPop = { - if (onBackPressedDispatcher.hasEnabledCallbacks()) { - onBackPressedDispatcher.onBackPressed() - } - }, - onOpenSettings = { - context.startActivity(Intent(context, SettingsActivity::class.java)) - }, - modifier = Modifier.semantics { - // Enables testTag -> UiAutomator resource id - // See https://developer.android.com/jetpack/compose/testing#uiautomator-interop - testTagsAsResourceId = true - }, - ) - } - } + component.tiviContent( + onRootPop = { + if (onBackPressedDispatcher.hasEnabledCallbacks()) { + onBackPressedDispatcher.onBackPressed() + } + }, + onOpenSettings = { + context.startActivity(Intent(context, SettingsActivity::class.java)) + }, + modifier = Modifier.semantics { + // Enables testTag -> UiAutomator resource id + // See https://developer.android.com/jetpack/compose/testing#uiautomator-interop + testTagsAsResourceId = true + }, + ) } } @@ -110,14 +89,10 @@ abstract class MainActivityComponent( @Component val applicationComponent: AndroidApplicationComponent = AndroidApplicationComponent.from(activity), ) : ActivityComponent, TraktAuthActivityComponent { - abstract val tiviDateFormatter: TiviDateFormatter - abstract val textCreator: TiviTextCreator - abstract val preferences: TiviPreferences - abstract val analytics: Analytics + abstract val tiviContent: TiviContent + abstract val contentViewSetter: ContentViewSetter abstract val login: LoginToTraktInteractor - abstract val circuitConfig: CircuitConfig - abstract val imageLoader: ImageLoader abstract val viewModel: () -> MainActivityViewModel } diff --git a/ios-app/Tivi/Tivi/ContentView.swift b/ios-app/Tivi/Tivi/ContentView.swift index b037085d72..1a28ee3573 100644 --- a/ios-app/Tivi/Tivi/ContentView.swift +++ b/ios-app/Tivi/Tivi/ContentView.swift @@ -10,11 +10,11 @@ import TiviKt struct ContentView: View { let component: HomeUiControllerComponent - - init(applicationComponent: IosApplicationComponent) { - self.component = HomeUiControllerComponent.companion.create(applicationComponent: applicationComponent) + + init(component: HomeUiControllerComponent) { + self.component = component } - + var body: some View { ComposeView(component: self.component) .ignoresSafeArea(.all, edges: .bottom) // Compose has own keyboard handler @@ -23,26 +23,17 @@ struct ContentView: View { struct ComposeView: UIViewControllerRepresentable { let component: HomeUiControllerComponent - + init(component: HomeUiControllerComponent) { self.component = component } - + func makeUIViewController(context: Context) -> UIViewController { - HomeKt.HomeViewController( - onRootPop: { - // todo - }, - onOpenSettings: { - // todo - }, - imageLoader: component.imageLoader, - tiviDateFormatter: component.tiviDateFormatter, - tiviTextCreator: component.textCreator, - circuitConfig: component.circuitConfig, - analytics: component.analytics, - preferences: component.preferences - ) + component.uiViewController { + // onRootPop + } onOpenSettings: { + // no-op + } } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} diff --git a/ios-app/Tivi/Tivi/TiviApp.swift b/ios-app/Tivi/Tivi/TiviApp.swift index 7b38ff3f00..ea538fc8cb 100644 --- a/ios-app/Tivi/Tivi/TiviApp.swift +++ b/ios-app/Tivi/Tivi/TiviApp.swift @@ -14,7 +14,8 @@ struct TiviApp: App { var body: some Scene { WindowGroup { - ContentView(applicationComponent: applicationComponent) + let uiComponent = HomeUiControllerComponent.companion.create(applicationComponent: applicationComponent) + ContentView(component: uiComponent) } } } diff --git a/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt b/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt index 028b60f87b..588ce24ee9 100644 --- a/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt +++ b/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt @@ -3,25 +3,24 @@ package app.tivi.inject -import app.tivi.core.analytics.Analytics -import app.tivi.settings.TiviPreferences -import app.tivi.util.TiviDateFormatter -import app.tivi.util.TiviTextCreator -import com.seiko.imageloader.ImageLoader -import com.slack.circuit.foundation.CircuitConfig +import app.tivi.home.TiviUiViewController import me.tatarka.inject.annotations.Component +import platform.UIKit.UIViewController @ActivityScope @Component abstract class HomeUiControllerComponent( @Component val applicationComponent: IosApplicationComponent, ) { - abstract val tiviDateFormatter: TiviDateFormatter - abstract val textCreator: TiviTextCreator - abstract val preferences: TiviPreferences - abstract val analytics: Analytics - abstract val circuitConfig: CircuitConfig - abstract val imageLoader: ImageLoader + abstract val viewController: TiviUiViewController + + /** + * Function which makes [viewController] easier to call from Swift + */ + fun uiViewController( + onRootPop: () -> Unit, + onOpenSettings: () -> Unit, + ): UIViewController = viewController(onRootPop, onOpenSettings) companion object } diff --git a/ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt b/ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt index 0c16b5c734..7cef4bf3d4 100644 --- a/ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt +++ b/ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt @@ -10,6 +10,8 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import app.tivi.common.compose.LocalTiviDateFormatter +import app.tivi.common.compose.LocalTiviTextCreator import app.tivi.common.compose.LocalWindowSizeClass import app.tivi.common.compose.shouldUseDarkColors import app.tivi.common.compose.shouldUseDynamicColors @@ -20,22 +22,41 @@ import app.tivi.screens.DiscoverScreen import app.tivi.screens.SettingsScreen import app.tivi.screens.TiviScreen import app.tivi.settings.TiviPreferences +import app.tivi.util.TiviDateFormatter +import app.tivi.util.TiviTextCreator +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.LocalImageLoader import com.slack.circuit.backstack.SaveableBackStack import com.slack.circuit.backstack.rememberSaveableBackStack +import com.slack.circuit.foundation.CircuitCompositionLocals +import com.slack.circuit.foundation.CircuitConfig import com.slack.circuit.foundation.push import com.slack.circuit.foundation.rememberCircuitNavigator import com.slack.circuit.foundation.screen import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.Screen +import me.tatarka.inject.annotations.Assisted +import me.tatarka.inject.annotations.Inject +typealias TiviContent = @Composable ( + onRootPop: () -> Unit, + onOpenSettings: () -> Unit, + modifier: Modifier, +) -> Unit + +@Inject @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @Composable fun TiviContent( - onRootPop: () -> Unit, - onOpenSettings: () -> Unit, + @Assisted onRootPop: () -> Unit, + @Assisted onOpenSettings: () -> Unit, + circuitConfig: CircuitConfig, analytics: Analytics, + tiviDateFormatter: TiviDateFormatter, + tiviTextCreator: TiviTextCreator, preferences: TiviPreferences, - modifier: Modifier = Modifier, + imageLoader: ImageLoader, + @Assisted modifier: Modifier = Modifier, ) { val backstack: SaveableBackStack = rememberSaveableBackStack { push(DiscoverScreen) } val circuitNavigator = rememberCircuitNavigator(backstack, onRootPop) @@ -56,17 +77,22 @@ fun TiviContent( CompositionLocalProvider( LocalNavigator provides navigator, + LocalImageLoader provides imageLoader, + LocalTiviDateFormatter provides tiviDateFormatter, + LocalTiviTextCreator provides tiviTextCreator, LocalWindowSizeClass provides calculateWindowSizeClass(), ) { - TiviTheme( - useDarkColors = preferences.shouldUseDarkColors(), - useDynamicColors = preferences.shouldUseDynamicColors(), - ) { - Home( - backstack = backstack, - navigator = navigator, - modifier = modifier, - ) + CircuitCompositionLocals(circuitConfig) { + TiviTheme( + useDarkColors = preferences.shouldUseDarkColors(), + useDynamicColors = preferences.shouldUseDynamicColors(), + ) { + Home( + backstack = backstack, + navigator = navigator, + modifier = modifier, + ) + } } } } diff --git a/ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt b/ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt deleted file mode 100644 index e282123250..0000000000 --- a/ui/root/src/iosMain/kotlin/app/tivi/home/Home.kt +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2020, Google LLC, Christopher Banes and the Tivi project contributors -// SPDX-License-Identifier: Apache-2.0 - -package app.tivi.home - -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.window.ComposeUIViewController -import app.tivi.common.compose.LocalTiviDateFormatter -import app.tivi.common.compose.LocalTiviTextCreator -import app.tivi.core.analytics.Analytics -import app.tivi.settings.TiviPreferences -import app.tivi.util.TiviDateFormatter -import app.tivi.util.TiviTextCreator -import com.seiko.imageloader.ImageLoader -import com.seiko.imageloader.LocalImageLoader -import com.slack.circuit.foundation.CircuitCompositionLocals -import com.slack.circuit.foundation.CircuitConfig -import kotlin.experimental.ExperimentalObjCName -import platform.UIKit.UIViewController - -@OptIn(ExperimentalObjCName::class) -@ObjCName("HomeViewController") -fun HomeViewController( - onRootPop: () -> Unit, - onOpenSettings: () -> Unit, - imageLoader: ImageLoader, - tiviDateFormatter: TiviDateFormatter, - tiviTextCreator: TiviTextCreator, - circuitConfig: CircuitConfig, - analytics: Analytics, - preferences: TiviPreferences, -): UIViewController = ComposeUIViewController { - CompositionLocalProvider( - LocalImageLoader provides imageLoader, - LocalTiviDateFormatter provides tiviDateFormatter, - LocalTiviTextCreator provides tiviTextCreator, - ) { - CircuitCompositionLocals(circuitConfig) { - TiviContent( - onRootPop = onRootPop, - onOpenSettings = onOpenSettings, - analytics = analytics, - preferences = preferences, - ) - } - } -} diff --git a/ui/root/src/iosMain/kotlin/app/tivi/home/TiviUiViewController.kt b/ui/root/src/iosMain/kotlin/app/tivi/home/TiviUiViewController.kt new file mode 100644 index 0000000000..ec28b0cc31 --- /dev/null +++ b/ui/root/src/iosMain/kotlin/app/tivi/home/TiviUiViewController.kt @@ -0,0 +1,28 @@ +// Copyright 2020, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.home + +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.ComposeUIViewController +import me.tatarka.inject.annotations.Assisted +import me.tatarka.inject.annotations.Inject +import platform.UIKit.UIViewController + +typealias TiviUiViewController = ( + onRootPop: () -> Unit, + onOpenSettings: () -> Unit, +) -> UIViewController + +@Inject +fun TiviUiViewController( + @Assisted onRootPop: () -> Unit, + @Assisted onOpenSettings: () -> Unit, + tiviContent: TiviContent, +): UIViewController = ComposeUIViewController { + tiviContent( + onRootPop = onRootPop, + onOpenSettings = onOpenSettings, + modifier = Modifier, + ) +} From 7c2ab2c454893d5fc2bbcf2cf15656d4be4c9e09 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Jun 2023 20:51:19 +0100 Subject: [PATCH 19/23] Tidy up DI scoping UI should be scoped to the view, not application --- .../app/src/main/kotlin/app/tivi/TiviApplication.kt | 2 +- .../app/tivi/appinitializers/EmojiInitializer.kt | 2 +- .../app/src/main/kotlin/app/tivi/home/MainActivity.kt | 4 ++-- .../kotlin/app/tivi/appinitializers/AppInitializer.kt | 2 +- .../kotlin/app/tivi/util/LoggerInitializer.kt | 2 +- .../kotlin/app/tivi/settings/PreferencesInitializer.kt | 2 +- ios-app/Tivi/Tivi/TiviApp.swift | 10 ++++++++-- .../kotlin/app/tivi/appinitializers/AppInitializers.kt | 6 +++--- .../kotlin/app/tivi/appinitializers/TmdbInitializer.kt | 2 +- .../app/tivi/inject/SharedApplicationComponent.kt | 3 +-- .../commonMain/kotlin/app/tivi/inject/UiComponent.kt | 2 +- .../app/tivi/inject/HomeUiControllerComponent.kt | 2 +- .../kotlin/app/tivi/tasks/ShowTasksInitializer.kt | 2 +- .../kotlin/app/tivi/account/AccountComponent.kt | 6 +++--- .../kotlin/app/tivi/home/discover/DiscoverComponent.kt | 6 +++--- .../app/tivi/episodedetails/EpisodeDetailsComponent.kt | 6 +++--- .../app/tivi/episode/track/EpisodeTrackComponent.kt | 6 +++--- .../kotlin/app/tivi/home/library/LibraryComponent.kt | 6 +++--- .../app/tivi/home/popular/PopularShowsComponent.kt | 6 +++--- .../tivi/home/recommended/RecommendedShowsComponent.kt | 6 +++--- .../kotlin/app/tivi/home/search/SearchComponent.kt | 6 +++--- .../tivi/showdetails/details/ShowDetailsComponent.kt | 6 +++--- .../tivi/showdetails/seasons/ShowSeasonsComponent.kt | 6 +++--- .../app/tivi/home/trending/TrendingShowsComponent.kt | 6 +++--- .../kotlin/app/tivi/home/upnext/UpNextComponent.kt | 6 +++--- 25 files changed, 59 insertions(+), 54 deletions(-) diff --git a/android-app/app/src/main/kotlin/app/tivi/TiviApplication.kt b/android-app/app/src/main/kotlin/app/tivi/TiviApplication.kt index ecb8172a32..ce61d9f9ac 100644 --- a/android-app/app/src/main/kotlin/app/tivi/TiviApplication.kt +++ b/android-app/app/src/main/kotlin/app/tivi/TiviApplication.kt @@ -22,7 +22,7 @@ class TiviApplication : Application(), Configuration.Provider { workerFactory = component.workerFactory - component.initializers.init() + component.initializers.initialize() } override fun getWorkManagerConfiguration(): Configuration { diff --git a/android-app/app/src/main/kotlin/app/tivi/appinitializers/EmojiInitializer.kt b/android-app/app/src/main/kotlin/app/tivi/appinitializers/EmojiInitializer.kt index 03acf193ca..e60851f25f 100644 --- a/android-app/app/src/main/kotlin/app/tivi/appinitializers/EmojiInitializer.kt +++ b/android-app/app/src/main/kotlin/app/tivi/appinitializers/EmojiInitializer.kt @@ -13,7 +13,7 @@ import me.tatarka.inject.annotations.Inject class EmojiInitializer( private val application: Application, ) : AppInitializer { - override fun init() { + override fun initialize() { val fontRequest = FontRequest( "com.google.android.gms.fonts", "com.google.android.gms", diff --git a/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt b/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt index 1ea7fd2d99..df1f6011d9 100644 --- a/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt +++ b/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt @@ -28,6 +28,7 @@ import app.tivi.data.traktauth.TraktAuthActivityComponent import app.tivi.inject.ActivityComponent import app.tivi.inject.ActivityScope import app.tivi.inject.AndroidApplicationComponent +import app.tivi.inject.UiComponent import app.tivi.settings.SettingsActivity import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides @@ -87,8 +88,7 @@ class MainActivity : TiviActivity() { abstract class MainActivityComponent( @get:Provides override val activity: Activity, @Component val applicationComponent: AndroidApplicationComponent = AndroidApplicationComponent.from(activity), -) : ActivityComponent, - TraktAuthActivityComponent { +) : ActivityComponent, TraktAuthActivityComponent, UiComponent { abstract val tiviContent: TiviContent abstract val contentViewSetter: ContentViewSetter diff --git a/core/base/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializer.kt b/core/base/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializer.kt index 4879393e09..80c09a867f 100644 --- a/core/base/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializer.kt +++ b/core/base/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializer.kt @@ -4,5 +4,5 @@ package app.tivi.appinitializers fun interface AppInitializer { - fun init() + fun initialize() } diff --git a/core/logging/src/commonMain/kotlin/app/tivi/util/LoggerInitializer.kt b/core/logging/src/commonMain/kotlin/app/tivi/util/LoggerInitializer.kt index 8ab51bd7dc..b11551010f 100644 --- a/core/logging/src/commonMain/kotlin/app/tivi/util/LoggerInitializer.kt +++ b/core/logging/src/commonMain/kotlin/app/tivi/util/LoggerInitializer.kt @@ -13,7 +13,7 @@ class LoggerInitializer( private val logger: Logger, private val applicationInfo: ApplicationInfo, ) : AppInitializer { - override fun init() { + override fun initialize() { logger.setup( debugMode = when { applicationInfo.debugBuild -> true diff --git a/core/preferences/src/commonMain/kotlin/app/tivi/settings/PreferencesInitializer.kt b/core/preferences/src/commonMain/kotlin/app/tivi/settings/PreferencesInitializer.kt index 289bb556d4..7b032361b5 100644 --- a/core/preferences/src/commonMain/kotlin/app/tivi/settings/PreferencesInitializer.kt +++ b/core/preferences/src/commonMain/kotlin/app/tivi/settings/PreferencesInitializer.kt @@ -10,7 +10,7 @@ import me.tatarka.inject.annotations.Inject class PreferencesInitializer( private val prefs: TiviPreferences, ) : AppInitializer { - override fun init() { + override fun initialize() { prefs.setup() } } diff --git a/ios-app/Tivi/Tivi/TiviApp.swift b/ios-app/Tivi/Tivi/TiviApp.swift index ea538fc8cb..c802513c29 100644 --- a/ios-app/Tivi/Tivi/TiviApp.swift +++ b/ios-app/Tivi/Tivi/TiviApp.swift @@ -11,10 +11,16 @@ import TiviKt @main struct TiviApp: App { let applicationComponent = IosApplicationComponent.companion.create() - + + init() { + applicationComponent.initializers.initialize() + } + var body: some Scene { WindowGroup { - let uiComponent = HomeUiControllerComponent.companion.create(applicationComponent: applicationComponent) + let uiComponent = HomeUiControllerComponent.companion.create( + applicationComponent: applicationComponent + ) ContentView(component: uiComponent) } } diff --git a/shared/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializers.kt b/shared/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializers.kt index d804614b34..50106d07e7 100644 --- a/shared/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializers.kt +++ b/shared/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializers.kt @@ -10,11 +10,11 @@ import me.tatarka.inject.annotations.Inject class AppInitializers( private val initializers: Set, private val tracer: Tracer, -) { - fun init() { +) : AppInitializer { + override fun initialize() { tracer.trace("AppInitializers") { for (initializer in initializers) { - initializer.init() + initializer.initialize() } } } diff --git a/shared/src/commonMain/kotlin/app/tivi/appinitializers/TmdbInitializer.kt b/shared/src/commonMain/kotlin/app/tivi/appinitializers/TmdbInitializer.kt index ebd15ea18c..84fa30ceda 100644 --- a/shared/src/commonMain/kotlin/app/tivi/appinitializers/TmdbInitializer.kt +++ b/shared/src/commonMain/kotlin/app/tivi/appinitializers/TmdbInitializer.kt @@ -16,7 +16,7 @@ class TmdbInitializer( private val updateTmdbConfig: UpdateTmdbConfig, private val dispatchers: AppCoroutineDispatchers, ) : AppInitializer { - override fun init() { + override fun initialize() { @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(dispatchers.main) { updateTmdbConfig.invoke() diff --git a/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt b/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt index fa6e2be9c0..57820f5dcf 100644 --- a/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt +++ b/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt @@ -38,8 +38,7 @@ interface SharedApplicationComponent : TasksComponent, CoreComponent, DataComponent, - ImageLoadingComponent, - UiComponent + ImageLoadingComponent interface ApiComponent : TmdbComponent, TraktComponent diff --git a/shared/src/commonMain/kotlin/app/tivi/inject/UiComponent.kt b/shared/src/commonMain/kotlin/app/tivi/inject/UiComponent.kt index 73364dd9d3..b779152868 100644 --- a/shared/src/commonMain/kotlin/app/tivi/inject/UiComponent.kt +++ b/shared/src/commonMain/kotlin/app/tivi/inject/UiComponent.kt @@ -35,7 +35,7 @@ interface UiComponent : UpNextComponent { @Provides - @ApplicationScope + @ActivityScope fun provideCircuitConfig( uiFactories: Set, presenterFactories: Set, diff --git a/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt b/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt index 588ce24ee9..3e4d1a29a3 100644 --- a/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt +++ b/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt @@ -11,7 +11,7 @@ import platform.UIKit.UIViewController @Component abstract class HomeUiControllerComponent( @Component val applicationComponent: IosApplicationComponent, -) { +) : UiComponent { abstract val viewController: TiviUiViewController /** diff --git a/tasks/src/commonMain/kotlin/app/tivi/tasks/ShowTasksInitializer.kt b/tasks/src/commonMain/kotlin/app/tivi/tasks/ShowTasksInitializer.kt index 179400e9fa..1c6bd156f0 100644 --- a/tasks/src/commonMain/kotlin/app/tivi/tasks/ShowTasksInitializer.kt +++ b/tasks/src/commonMain/kotlin/app/tivi/tasks/ShowTasksInitializer.kt @@ -10,7 +10,7 @@ import me.tatarka.inject.annotations.Inject class ShowTasksInitializer( private val showTasks: Lazy, ) : AppInitializer { - override fun init() { + override fun initialize() { showTasks.value.setupNightSyncs() } } diff --git a/ui/account/src/commonMain/kotlin/app/tivi/account/AccountComponent.kt b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountComponent.kt index 1d40e32a1c..97acd6433c 100644 --- a/ui/account/src/commonMain/kotlin/app/tivi/account/AccountComponent.kt +++ b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountComponent.kt @@ -3,7 +3,7 @@ package app.tivi.account -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface AccountComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindAccountPresenterFactory(factory: AccountUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindAccountUiFactory(factory: AccountUiFactory): Ui.Factory = factory } diff --git a/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverComponent.kt b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverComponent.kt index 0cf1001a87..6b7652130b 100644 --- a/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverComponent.kt +++ b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverComponent.kt @@ -3,7 +3,7 @@ package app.tivi.home.discover -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface DiscoverComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindDiscoverPresenterFactory(factory: DiscoverUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindDiscoverUiFactoryFactory(factory: DiscoverUiFactory): Ui.Factory = factory } diff --git a/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsComponent.kt b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsComponent.kt index f63c42c86c..0262fa1a8e 100644 --- a/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsComponent.kt +++ b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsComponent.kt @@ -3,7 +3,7 @@ package app.tivi.episodedetails -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface EpisodeDetailsComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindEpisodeDetailsPresenterFactory(factory: EpisodeDetailsUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindEpisodeDetailsUiFactoryFactory(factory: EpisodeDetailsUiFactory): Ui.Factory = factory } diff --git a/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackComponent.kt b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackComponent.kt index 1575021bb2..70fc3f817b 100644 --- a/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackComponent.kt +++ b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackComponent.kt @@ -3,7 +3,7 @@ package app.tivi.episode.track -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface EpisodeTrackComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindEpisodeTrackPresenterFactory(factory: EpisodeTrackUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindEpisodeTrackUiFactoryFactory(factory: EpisodeTrackUiFactory): Ui.Factory = factory } diff --git a/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryComponent.kt b/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryComponent.kt index 0b22632edd..7054fcb1c6 100644 --- a/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryComponent.kt +++ b/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryComponent.kt @@ -3,7 +3,7 @@ package app.tivi.home.library -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface LibraryComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindLibraryPresenterFactory(factory: LibraryUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindLibraryUiFactoryFactory(factory: LibraryUiFactory): Ui.Factory = factory } diff --git a/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsComponent.kt b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsComponent.kt index d462600315..62707d203c 100644 --- a/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsComponent.kt +++ b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsComponent.kt @@ -3,7 +3,7 @@ package app.tivi.home.popular -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface PopularShowsComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindPopularShowsPresenterFactory(factory: PopularShowsUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindPopularShowsUiFactoryFactory(factory: PopularShowsUiFactory): Ui.Factory = factory } diff --git a/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsComponent.kt b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsComponent.kt index 484ab664c1..03cd8f7b5c 100644 --- a/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsComponent.kt +++ b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsComponent.kt @@ -3,7 +3,7 @@ package app.tivi.home.recommended -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface RecommendedShowsComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindRecommendedShowsPresenterFactory(factory: RecommendedShowsUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindRecommendedShowsUiFactoryFactory(factory: RecommendedShowsUiFactory): Ui.Factory = factory } diff --git a/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchComponent.kt b/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchComponent.kt index cf5f78e2c7..9cb086639a 100644 --- a/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchComponent.kt +++ b/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchComponent.kt @@ -3,7 +3,7 @@ package app.tivi.home.search -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface SearchComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindSearchPresenterFactory(factory: SearchUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindSearchUiFactoryFactory(factory: SearchUiFactory): Ui.Factory = factory } diff --git a/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsComponent.kt b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsComponent.kt index e723fa9b87..9975642092 100644 --- a/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsComponent.kt +++ b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsComponent.kt @@ -3,7 +3,7 @@ package app.tivi.showdetails.details -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface ShowDetailsComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindShowDetailsPresenterFactory(factory: ShowDetailsUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindShowDetailsUiFactoryFactory(factory: ShowDetailsUiFactory): Ui.Factory = factory } diff --git a/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt index aefbad1519..8d56c9b03b 100644 --- a/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt +++ b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt @@ -3,7 +3,7 @@ package app.tivi.showdetails.seasons -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface ShowSeasonsComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindShowSeasonsPresenterFactory(factory: ShowSeasonsUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindShowSeasonsUiFactoryFactory(factory: ShowSeasonsUiFactory): Ui.Factory = factory } diff --git a/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsComponent.kt b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsComponent.kt index e91e3b0fcc..538b22b470 100644 --- a/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsComponent.kt +++ b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsComponent.kt @@ -3,7 +3,7 @@ package app.tivi.home.trending -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface TrendingShowsComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindTrendingShowsPresenterFactory(factory: TrendingShowsUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindTrendingShowsUiFactoryFactory(factory: TrendingShowsUiFactory): Ui.Factory = factory } diff --git a/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextComponent.kt b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextComponent.kt index 2d75b8ac1d..65e05dd6f6 100644 --- a/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextComponent.kt +++ b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextComponent.kt @@ -3,7 +3,7 @@ package app.tivi.home.upnext -import app.tivi.inject.ApplicationScope +import app.tivi.inject.ActivityScope import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui import me.tatarka.inject.annotations.IntoSet @@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides interface UpNextComponent { @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindUpNextPresenterFactory(factory: UpNextUiPresenterFactory): Presenter.Factory = factory @IntoSet @Provides - @ApplicationScope + @ActivityScope fun bindUpNextUiFactoryFactory(factory: UpNextUiFactory): Ui.Factory = factory } From fb315d1ddd5644c0c42222fca624e4b9daa7ad4f Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Jun 2023 22:53:55 +0100 Subject: [PATCH 20/23] Add crossfade to AsyncImage() --- .../app/tivi/common/compose/ui/Image.kt | 118 +++++++++++++++--- 1 file changed, 104 insertions(+), 14 deletions(-) diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt index e00093c303..5f374ee4ce 100644 --- a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt +++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt @@ -1,14 +1,22 @@ // Copyright 2022, Google LLC, Christopher Banes and the Tivi project contributors // SPDX-License-Identifier: Apache-2.0 +@file:OptIn(ExperimentalCoroutinesApi::class) + package app.tivi.common.compose.ui +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -17,6 +25,8 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.LayoutModifier import androidx.compose.ui.layout.Measurable @@ -24,14 +34,23 @@ import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density +import com.seiko.imageloader.ImageLoader import com.seiko.imageloader.ImageRequestState +import com.seiko.imageloader.LocalImageLoader +import com.seiko.imageloader.asImageBitmap import com.seiko.imageloader.model.ImageRequest import com.seiko.imageloader.model.ImageRequestBuilder +import com.seiko.imageloader.model.ImageResult +import com.seiko.imageloader.option.Scale import com.seiko.imageloader.option.SizeResolver -import com.seiko.imageloader.rememberAsyncImagePainter +import com.seiko.imageloader.toPainter +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.withContext @Composable fun AsyncImage( @@ -40,6 +59,7 @@ fun AsyncImage( modifier: Modifier = Modifier, onState: ((ImageRequestState) -> Unit)? = null, requestBuilder: (ImageRequestBuilder.() -> ImageRequestBuilder)? = null, + imageLoader: ImageLoader = LocalImageLoader.current, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit, alpha: Float = DefaultAlpha, @@ -47,36 +67,101 @@ fun AsyncImage( filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, ) { val sizeResolver = ConstraintsSizeResolver() + var requestState: ImageRequestState by remember { mutableStateOf(ImageRequestState.Loading()) } + + val request by produceState(null, model, contentScale) { + value = ImageRequest { + data(model) + size(sizeResolver) + requestBuilder?.invoke(this) - val request = ImageRequest { - data(model) - size(sizeResolver) - requestBuilder?.invoke(this) + options { + if (scale == Scale.AUTO) { + scale = contentScale.toScale() + } + } + eventListener { event -> + requestState = ImageRequestState.Loading(event) + } + } } - val painter = rememberAsyncImagePainter( - request = request, - contentScale = contentScale, - filterQuality = filterQuality, - ) + var result by remember { mutableStateOf(null) } + LaunchedEffect(imageLoader) { + snapshotFlow { request } + .filterNotNull() + .mapLatest { + withContext(imageLoader.config.imageScope.coroutineContext) { + imageLoader.execute(it) + } + } + .collect { result = it } + } val lastOnState by rememberUpdatedState(onState) - LaunchedEffect(painter) { - snapshotFlow { painter.requestState } + LaunchedEffect(Unit) { + snapshotFlow { requestState } .collect { lastOnState?.invoke(it) } } + Crossfade( + targetState = result, + animationSpec = tween(durationMillis = 220), + label = "AsyncImage-Crossfade", + ) { r -> + ResultImage( + result = r, + alignment = alignment, + contentDescription = contentDescription, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + modifier = modifier.then(sizeResolver), + filterQuality = filterQuality, + ) + } +} + +@Composable +private fun ResultImage( + result: ImageResult?, + contentDescription: String?, + modifier: Modifier = Modifier, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, +) { Image( - painter = painter, + painter = when (result) { + is ImageResult.Bitmap -> { + BitmapPainter( + image = result.bitmap.asImageBitmap(), + filterQuality = filterQuality, + ) + } + + is ImageResult.Image -> result.image.toPainter(filterQuality) + is ImageResult.Painter -> result.painter + is ImageResult.Error -> TODO() + is ImageResult.Source -> TODO() + null -> EmptyPainter + }, alignment = alignment, contentDescription = contentDescription, contentScale = contentScale, alpha = alpha, colorFilter = colorFilter, - modifier = modifier.then(sizeResolver), + modifier = modifier, ) } +private object EmptyPainter : Painter() { + override val intrinsicSize get() = Size.Unspecified + override fun DrawScope.onDraw() = Unit +} + /** A [SizeResolver] that computes the size from the constrains passed during the layout phase. */ internal class ConstraintsSizeResolver : SizeResolver, LayoutModifier { @@ -113,3 +198,8 @@ private fun Constraints.toSizeOrNull() = when { height = if (hasBoundedHeight) maxHeight.toFloat() else 0f, ) } + +private fun ContentScale.toScale() = when (this) { + ContentScale.Fit, ContentScale.Inside -> Scale.FIT + else -> Scale.FILL +} From 0a47f4330c02b6198396ae1215193dec7f0d4fb5 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Jun 2023 18:06:43 +0100 Subject: [PATCH 21/23] Add Desktop app --- api/tmdb/build.gradle.kts | 2 +- api/trakt/build.gradle.kts | 2 +- desktop-app/build.gradle.kts | 34 ++++++++++++ .../src/jvmMain/kotlin/app/tivi/Main.kt | 37 +++++++++++++ settings.gradle.kts | 1 + shared/build.gradle.kts | 6 ++ .../inject/DesktopApplicationComponent.kt | 55 +++++++++++++++++++ .../kotlin/app/tivi/inject/WindowComponent.kt | 17 ++++++ 8 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 desktop-app/build.gradle.kts create mode 100644 desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt create mode 100644 shared/src/jvmMain/kotlin/app/tivi/inject/DesktopApplicationComponent.kt create mode 100644 shared/src/jvmMain/kotlin/app/tivi/inject/WindowComponent.kt diff --git a/api/tmdb/build.gradle.kts b/api/tmdb/build.gradle.kts index 8ea76a1e8c..deb4b7dc6d 100644 --- a/api/tmdb/build.gradle.kts +++ b/api/tmdb/build.gradle.kts @@ -31,7 +31,7 @@ kotlin { val jvmMain by getting { dependencies { - implementation(libs.okhttp.okhttp) + api(libs.okhttp.okhttp) implementation(libs.ktor.client.okhttp) } } diff --git a/api/trakt/build.gradle.kts b/api/trakt/build.gradle.kts index 9b08835f70..8b732bbf57 100644 --- a/api/trakt/build.gradle.kts +++ b/api/trakt/build.gradle.kts @@ -36,7 +36,7 @@ kotlin { val jvmMain by getting { dependencies { - implementation(libs.okhttp.okhttp) + api(libs.okhttp.okhttp) implementation(libs.ktor.client.okhttp) } } diff --git a/desktop-app/build.gradle.kts b/desktop-app/build.gradle.kts new file mode 100644 index 0000000000..a9fb3c7823 --- /dev/null +++ b/desktop-app/build.gradle.kts @@ -0,0 +1,34 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +plugins { + // We have to use KMP due to Moko-resources + // https://github.com/icerockdev/moko-resources/issues/263 + id("app.tivi.kotlin.multiplatform") + alias(libs.plugins.composeMultiplatform) +} + +kotlin { + sourceSets { + val jvmMain by getting { + dependencies { + implementation(projects.shared) + implementation(compose.desktop.currentOs) + } + } + } +} + +compose.desktop { + application { + mainClass = "app.tivi.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "app.tivi" + packageVersion = "1.0.0" + } + } +} diff --git a/desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt b/desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt new file mode 100644 index 0000000000..56b66a2043 --- /dev/null +++ b/desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt @@ -0,0 +1,37 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi + +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import app.tivi.inject.DesktopApplicationComponent +import app.tivi.inject.WindowComponent +import app.tivi.inject.create + +fun main() = application { + val applicationComponent = remember { + DesktopApplicationComponent.create() + } + + Window( + title = "Tivi", + onCloseRequest = ::exitApplication, + ) { + val component = remember(applicationComponent) { + WindowComponent.create(applicationComponent) + } + + component.tiviContent( + onRootPop = { + // TODO + }, + onOpenSettings = { + // TODO + }, + modifier = Modifier, + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index bf3d878900..96d705b1ff 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -98,5 +98,6 @@ include( ":android-app:app", ":android-app:benchmark", ":android-app:common-test", + ":desktop-app", ":thirdparty:swipe", ) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 57885411b7..c925722c15 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -57,6 +57,12 @@ kotlin { api(projects.ui.upnext) } } + + val jvmMain by getting { + dependencies { + api(libs.okhttp.okhttp) + } + } } } diff --git a/shared/src/jvmMain/kotlin/app/tivi/inject/DesktopApplicationComponent.kt b/shared/src/jvmMain/kotlin/app/tivi/inject/DesktopApplicationComponent.kt new file mode 100644 index 0000000000..52686f32d6 --- /dev/null +++ b/shared/src/jvmMain/kotlin/app/tivi/inject/DesktopApplicationComponent.kt @@ -0,0 +1,55 @@ +// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.inject + +import androidx.compose.ui.unit.Density +import app.tivi.app.ApplicationInfo +import app.tivi.app.Flavor +import app.tivi.appinitializers.AppInitializers +import java.util.concurrent.TimeUnit +import me.tatarka.inject.annotations.Component +import me.tatarka.inject.annotations.Provides +import okhttp3.ConnectionPool +import okhttp3.Dispatcher +import okhttp3.OkHttpClient + +@Component +@ApplicationScope +abstract class DesktopApplicationComponent : SharedApplicationComponent { + abstract val initializers: AppInitializers + + @ApplicationScope + @Provides + fun provideApplicationId(): ApplicationInfo = ApplicationInfo( + packageName = "app.tivi", + debugBuild = true, + flavor = Flavor.Standard, + ) + + @Provides + fun provideDensity(): Density = Density(density = 1f) // FIXME + + @ApplicationScope + @Provides + fun provideOkHttpClient( + // interceptors: Set, + ): OkHttpClient = OkHttpClient.Builder() + // .apply { interceptors.forEach(::addInterceptor) } + // Adjust the Connection pool to account for historical use of 3 separate clients + // but reduce the keepAlive to 2 minutes to avoid keeping radio open. + .connectionPool(ConnectionPool(10, 2, TimeUnit.MINUTES)) + .dispatcher( + Dispatcher().apply { + // Allow for increased number of concurrent image fetches on same host + maxRequestsPerHost = 10 + }, + ) + // Increase timeouts + .connectTimeout(20, TimeUnit.SECONDS) + .readTimeout(20, TimeUnit.SECONDS) + .writeTimeout(20, TimeUnit.SECONDS) + .build() + + companion object +} diff --git a/shared/src/jvmMain/kotlin/app/tivi/inject/WindowComponent.kt b/shared/src/jvmMain/kotlin/app/tivi/inject/WindowComponent.kt new file mode 100644 index 0000000000..93efdf53e3 --- /dev/null +++ b/shared/src/jvmMain/kotlin/app/tivi/inject/WindowComponent.kt @@ -0,0 +1,17 @@ +// Copyright 2023, Christopher Banes and the Tivi project contributors +// SPDX-License-Identifier: Apache-2.0 + +package app.tivi.inject + +import app.tivi.home.TiviContent +import me.tatarka.inject.annotations.Component + +@ActivityScope +@Component +abstract class WindowComponent( + @Component val applicationComponent: DesktopApplicationComponent, +) : UiComponent { + abstract val tiviContent: TiviContent + + companion object +} From 598417c20d0065934f2400eaf183fe66301e74bb Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Sat, 1 Jul 2023 10:05:47 +0100 Subject: [PATCH 22/23] Run AppInitializers on Desktop --- desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt b/desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt index 56b66a2043..d978e438ed 100644 --- a/desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt +++ b/desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt @@ -3,6 +3,7 @@ package app.tivi +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.window.Window @@ -16,6 +17,10 @@ fun main() = application { DesktopApplicationComponent.create() } + LaunchedEffect(applicationComponent) { + applicationComponent.initializers.initialize() + } + Window( title = "Tivi", onCloseRequest = ::exitApplication, From 06b2789a29317e57915a9c63b717836dc32f0d71 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Sat, 1 Jul 2023 10:18:59 +0100 Subject: [PATCH 23/23] Add Desktop build to CI --- .github/workflows/build.yml | 66 +++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8ac2e146f..ded24501d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,7 +51,6 @@ jobs: ./gradlew spotlessCheck \ :android-app:app:bundle \ :android-app:app:build \ - jvmTest \ lint \ -x :android-app:app:assembleStandardBenchmark \ -x :android-app:app:bundleStandardBenchmark @@ -102,7 +101,7 @@ jobs: path: | **/build/test-results/* - ios: + desktop: runs-on: macos-latest timeout-minutes: 60 env: @@ -110,9 +109,6 @@ jobs: ORG_GRADLE_PROJECT_TIVI_TVDB_API_KEY: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TVDB_API_KEY }} ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_ID: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_ID }} ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_SECRET: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_SECRET }} - ORG_GRADLE_PROJECT_TIVI_RELEASE_KEYSTORE_PWD: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_RELEASE_KEYSTORE_PWD }} - ORG_GRADLE_PROJECT_TIVI_RELEASE_KEY_PWD: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_RELEASE_KEY_PWD }} - ORG_GRADLE_PROJECT_TIVI_PLAY_PUBLISHER_ACCOUNT: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_PLAY_PUBLISHER_ACCOUNT }} steps: - uses: actions/checkout@v3 @@ -130,17 +126,59 @@ jobs: with: gradle-home-cache-cleanup: true - - name: Decrypt secrets - run: ./release/decrypt-secrets.sh - env: - ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }} + - name: Build Desktop App + run: ./gradlew spotlessCheck jvmTest :desktop-app:package - - name: Build iOS libraries - run: ./gradlew spotlessCheck :shared:linkIosX64 iosX64Test + - name: Upload build outputs + if: always() + uses: actions/upload-artifact@v3 + with: + name: desktop-build-binaries + path: desktop-app/build/compose/binaries - - name: Clean secrets + - name: Upload reports if: always() - run: ./release/clean-secrets.sh + uses: actions/upload-artifact@v3 + with: + name: desktop-reports + path: | + **/build/reports/* + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v3 + with: + name: desktop-test-results + path: | + **/build/test-results/* + + ios: + runs-on: macos-latest + timeout-minutes: 60 + env: + ORG_GRADLE_PROJECT_TIVI_TMDB_API_KEY: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TMDB_API_KEY }} + ORG_GRADLE_PROJECT_TIVI_TVDB_API_KEY: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TVDB_API_KEY }} + ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_ID: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_ID }} + ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_SECRET: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_SECRET }} + + steps: + - uses: actions/checkout@v3 + + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 17 + + - uses: gradle/gradle-build-action@v2 + with: + gradle-home-cache-cleanup: true + + - name: Build iOS libraries + run: ./gradlew spotlessCheck :shared:linkIosX64 iosX64Test - name: Upload reports if: always() @@ -160,7 +198,7 @@ jobs: publish: if: github.ref == 'refs/heads/main' - needs: [android, ios] + needs: [android, ios, desktop] runs-on: ubuntu-latest timeout-minutes: 20 env: